Files
fire_leave_tool/main.go
T
2026-07-03 20:55:15 +08:00

1114 lines
38 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"FireLeave_tool/connect"
"FireLeave_tool/gpio_monitor"
"FireLeave_tool/httpreader"
"FireLeave_tool/logger"
"FireLeave_tool/reporter"
"FireLeave_tool/tcpreader"
"FireLeave_tool/video_server"
"context"
"encoding/json"
"flag"
"fmt"
"log"
"math"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
)
type deviceAlarmState struct {
InCondition bool
StartTime int64
AlarmPauseUntil int64
FireLeaveForbidUntil int64
// fire_check火焰检测状态
FireCheckInCondition bool
FireCheckStartTime int64
}
// 全局 map:每个设备独立状态
var deviceStates sync.Map // map[string]*deviceAlarmState
var mergingAlarms sync.Map
// 全局上报管理器
var GlobalReporter = reporter.NewReporter()
const defaultFireLeaveForbidDuration = 15 * time.Minute
const buildVersion = "2026-05-13-logger-init-fix"
// DeviceManager 设备管理器
type DeviceManager struct {
mu sync.Mutex
devices map[string]*connect.DeviceData
}
// NewDeviceManager 初始化设备管理器
func NewDeviceManager() *DeviceManager {
return &DeviceManager{
devices: make(map[string]*connect.DeviceData),
}
}
func getDeviceState(deviceUUID string) *deviceAlarmState {
if s, ok := deviceStates.Load(deviceUUID); ok {
return s.(*deviceAlarmState)
}
s := &deviceAlarmState{}
deviceStates.Store(deviceUUID, s)
return s
}
func applyFireLeaveForbidWindow(duration time.Duration) {
if duration <= 0 {
return
}
now := time.Now().Unix()
until := now + int64(duration.Seconds())
applyFireLeaveForbidWindowUntil(now, until)
}
func applyFireLeaveForbidWindowUntil(startTime, endTime int64) {
if endTime <= time.Now().Unix() {
return
}
count := 0
GlobalDeviceManager.RangeDevices(func(uuid string, device *connect.DeviceData) bool {
state := getDeviceState(uuid)
state.FireLeaveForbidUntil = endTime
state.InCondition = false
count++
return true
})
logger.Logger.Printf("fire_leave reporting paused: start=%d, end=%d, devices=%d", startTime, endTime, count)
}
func clearFireLeaveForbidWindow() {
count := 0
GlobalDeviceManager.RangeDevices(func(uuid string, device *connect.DeviceData) bool {
state := getDeviceState(uuid)
state.FireLeaveForbidUntil = 0
state.InCondition = false
count++
return true
})
logger.Logger.Printf("fire_leave reporting pause cleared, devices=%d", count)
}
func persistAndApplyFireLeaveForbidWindow(minute string, duration time.Duration) {
if duration <= 0 {
if err := reporter.SaveForbidTimeWindow(minute, 0, 0); err != nil {
logger.Logger.Printf("Failed to clear forbid time config: %v", err)
}
clearFireLeaveForbidWindow()
return
}
startTime := time.Now().Unix()
endTime := startTime + int64(duration.Seconds())
if err := reporter.SaveForbidTimeWindow(minute, startTime, endTime); err != nil {
logger.Logger.Printf("Failed to save forbid time config: %v", err)
} else {
logger.Logger.Printf("Forbid time config saved successfully: Minute=%s, Start=%d, End=%d", minute, startTime, endTime)
}
applyFireLeaveForbidWindowUntil(startTime, endTime)
}
func restoreFireLeaveForbidWindow(deviceUUID string) {
config, err := reporter.LoadForbidTimeConfig()
if err != nil || config.EndTime <= time.Now().Unix() {
return
}
state := getDeviceState(deviceUUID)
state.FireLeaveForbidUntil = config.EndTime
state.InCondition = false
logger.Logger.Printf("Restored fire_leave forbid window: DeviceUUID=%s, Start=%d, End=%d", deviceUUID, config.StartTime, config.EndTime)
}
// AddDevice 添加设备
func (dm *DeviceManager) AddDevice(deviceData connect.DeviceData) {
dm.mu.Lock()
defer dm.mu.Unlock()
dm.devices[deviceData.DeviceUUID] = &deviceData
logger.Logger.Printf("add equipments: DeviceUUID=%s", deviceData.DeviceUUID)
}
// StopDevice 停止设备
func (dm *DeviceManager) StopDevice(deviceUUID string) {
dm.mu.Lock()
defer dm.mu.Unlock()
if _, exists := dm.devices[deviceUUID]; exists {
delete(dm.devices, deviceUUID)
logger.Logger.Printf("stop equipments: DeviceUUID=%s", deviceUUID)
}
}
// GetDevice 获取设备
func (dm *DeviceManager) GetDevice(deviceUUID string) (*connect.DeviceData, bool) {
dm.mu.Lock()
defer dm.mu.Unlock()
device, exists := dm.devices[deviceUUID]
return device, exists
}
// GlobalDeviceManager 全局设备管理器
var GlobalDeviceManager = NewDeviceManager()
// 工具函数
func waitForHTTPService(port int, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
address := fmt.Sprintf("127.0.0.1:%d", port)
logger.Logger.Printf("Start waiting for the HTTP service to start (Port=%d, Timeout=%v)", port, timeout)
attempt := 0
for time.Now().Before(deadline) {
attempt++
// 1. 先检查TCP连接
conn, err := net.DialTimeout("tcp", address, 2*time.Second)
if err != nil {
if attempt == 1 || attempt%5 == 0 {
logger.Logger.Printf("Wait for the TCP connection... (Port=%d, Number of attempts=%d, Error: %v)", port, attempt, err)
}
time.Sleep(1 * time.Second)
continue
}
conn.Close()
// 2. 再检查HTTP服务是否真正就绪
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://%s/health", address))
if err == nil {
resp.Body.Close()
if resp.StatusCode == http.StatusOK {
logger.Logger.Printf("The HTTP service has started successfully (Port=%d, Number of attempts=%d)", port, attempt)
return true
}
}
if attempt == 1 || attempt%5 == 0 {
logger.Logger.Printf("Wait for the HTTP service to be ready... (Port=%d, Total number of attempts=%d)", port, attempt)
}
time.Sleep(1 * time.Second)
}
logger.Logger.Printf("HTTP service startup timeout (Port=%d, Total number of attempts=%d)", port, attempt)
return false
}
// waitForTCPService 增强版本 - 支持更长的等待时间和更好的错误处理
func waitForTCPService(port int, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
address := fmt.Sprintf("127.0.0.1:%d", port)
logger.Logger.Printf("Start waiting for the TCP service to start (Port=%d, Timeout=%v)", port, timeout)
attempt := 0
lastError := ""
for time.Now().Before(deadline) {
attempt++
conn, err := net.DialTimeout("tcp", address, 2*time.Second)
if err == nil {
conn.Close()
logger.Logger.Printf("The TCP service has been successfully started (Port=%d, Number of attempts=%d)", port, attempt)
return true
}
currentError := err.Error()
if currentError != lastError {
logger.Logger.Printf("Wait for TCP service... (Port=%d, Number of attempts=%d, Error: %v)", port, attempt, err)
lastError = currentError
} else if attempt%5 == 0 {
// 每5次尝试记录一次相同的错误
logger.Logger.Printf("Wait for TCP service... (Port=%d, Number of attempts=%d, Error: %v)", port, attempt, err)
}
time.Sleep(2 * time.Second) // 增加等待间隔
}
logger.Logger.Printf("TCP service startup timeout (Port=%d, Total number of attempts=%d, Final error: %s)", port, attempt, lastError)
return false
}
func logProcessInfo(pid int, deviceUUID string) error {
cmd := exec.Command("ps", "-o", "pid,ppid,user,stat,start_time,cmd", "-p", fmt.Sprintf("%d", pid))
output, err := cmd.Output()
if err != nil {
return fmt.Errorf("Failed to obtain process information: %v", err)
}
logger.Logger.Printf("Detailed information about the reasoning program process (DeviceUUID=%s, PID=%d):\n%s", deviceUUID, pid, string(output))
return nil
}
func getIntFromNumber(num json.Number, def int) int {
if i, err := num.Int64(); err == nil {
return int(i)
}
return def
}
// processDevice 处理单个设备
func processDevice(ctx context.Context, deviceData connect.DeviceData) {
logger.Logger.Printf("Start processing the equipment: DeviceUUID=%s", deviceData.DeviceUUID)
debugDeviceData(deviceData)
// 首先将设备添加到设备管理器
GlobalDeviceManager.AddDevice(deviceData)
restoreFireLeaveForbidWindow(deviceData.DeviceUUID)
defer GlobalDeviceManager.StopDevice(deviceData.DeviceUUID)
// 创建带取消的子上下文
deviceCtx, cancel := context.WithCancel(ctx)
defer cancel()
// 分配端口 - fire_leave推理程序
portFireLeave, err := connect.GlobalPortManager.AllocatePort(deviceData.DeviceUUID)
if err != nil {
logger.Logger.Printf(" DeviceUUID=%s Port allocation failed: %v", deviceData.DeviceUUID, err)
return
}
defer connect.GlobalPortManager.ReleasePort(deviceData.DeviceUUID)
// 分配端口 - fire_check推理程序
portFireCheck, err := connect.GlobalPortManager.AllocatePort(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
if err != nil {
logger.Logger.Printf(" DeviceUUID=%s Fire check port allocation failed: %v", deviceData.DeviceUUID, err)
return
}
defer connect.GlobalPortManager.ReleasePort(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
logger.Logger.Printf("Port allocation successful: DeviceUUID=%s, FireLeavePort=%d, FireCheckPort=%d", deviceData.DeviceUUID, portFireLeave, portFireCheck)
// 创建fire_leave推理数据接收服务器
inferenceServerFireLeave := httpreader.NewInferenceServer(deviceData.DeviceUUID, portFireLeave)
if err := inferenceServerFireLeave.Start(); err != nil {
logger.Logger.Printf("Failed to start fire_leave inference server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 创建fire_check推理数据接收服务器
inferenceServerFireCheck := httpreader.NewInferenceServer(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), portFireCheck)
if err := inferenceServerFireCheck.Start(); err != nil {
logger.Logger.Printf("Failed to start fire_check inference server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 使用带上下文的停止
defer inferenceServerFireLeave.StopWithContext(deviceCtx)
defer inferenceServerFireCheck.StopWithContext(deviceCtx)
// 等待HTTP服务器启动
if !waitForHTTPService(portFireLeave, 10*time.Second) {
logger.Logger.Printf("Fire_leave inference server startup timeout (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, portFireLeave)
return
}
if !waitForHTTPService(portFireCheck, 10*time.Second) {
logger.Logger.Printf("Fire_check inference server startup timeout (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, portFireCheck)
return
}
logger.Logger.Printf("Inference servers started successfully: DeviceUUID=%s, FireLeavePort=%d, FireCheckPort=%d", deviceData.DeviceUUID, portFireLeave, portFireCheck)
// 处理检测区域为推理程序需要的格式
// 为fire_leave推理程序获取检测区域
fireLeaveDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireLeaveDetectArea)
logger.Logger.Printf("The processed fire_leave detection area: %s", fireLeaveDetectAreaStr)
// 为fire_check推理程序获取检测区域
fireCheckDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireCheckDetectArea)
logger.Logger.Printf("The processed fire_check detection area: %s", fireCheckDetectAreaStr)
// 启动fire_leave推理程序
inferenceCmdFireLeave := exec.Command(
"./yolov5",
"-s", deviceData.CameraRTSP,
"-m", "model",
"-c", deviceData.DeviceUUID,
"-p", fmt.Sprintf("%d", portFireLeave),
"-w", fireLeaveDetectAreaStr,
"-r", fmt.Sprintf("%d", deviceData.Confidence),
)
inferenceCmdFireLeave.Stdout = os.Stdout
inferenceCmdFireLeave.Stderr = os.Stderr
logger.Logger.Printf("Start fire_leave inference program command: %v", inferenceCmdFireLeave.Args)
if err := inferenceCmdFireLeave.Start(); err != nil {
logger.Logger.Printf("Fire_leave inference program startup failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 保存fire_leave进程对象到全局 map
connect.InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmdFireLeave)
logger.Logger.Printf("Fire_leave inference program started, PID=%d (DeviceUUID=%s)", inferenceCmdFireLeave.Process.Pid, deviceData.DeviceUUID)
// 启动fire_check推理程序
inferenceCmdFireCheck := exec.Command(
"./yolov5",
"-s", deviceData.CameraRTSP,
"-m", "fire_check",
"-c", fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID),
"-p", fmt.Sprintf("%d", portFireCheck),
"-w", fireCheckDetectAreaStr,
"-r", fmt.Sprintf("%d", deviceData.Confidence),
"-R",
)
inferenceCmdFireCheck.Stdout = os.Stdout
inferenceCmdFireCheck.Stderr = os.Stderr
logger.Logger.Printf("Start fire_check inference program command: %v", inferenceCmdFireCheck.Args)
if err := inferenceCmdFireCheck.Start(); err != nil {
logger.Logger.Printf("Fire_check inference program startup failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 保存fire_check进程对象到全局 map
connect.InferenceProcesses.Store(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), inferenceCmdFireCheck)
logger.Logger.Printf("Fire_check inference program started, PID=%d (DeviceUUID=%s)", inferenceCmdFireCheck.Process.Pid, deviceData.DeviceUUID)
// 初始化心跳
connect.UpdateInferenceHeartbeat(deviceData.DeviceUUID)
connect.UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
// 检查推理程序是否启动成功
time.Sleep(2 * time.Second)
// 检查fire_leave推理程序
if inferenceCmdFireLeave.Process == nil {
logger.Logger.Printf("Fire_leave inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID)
return
}
if err := inferenceCmdFireLeave.Process.Signal(syscall.Signal(0)); err != nil {
logger.Logger.Printf("Fire_leave inference program exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 检查fire_check推理程序
if inferenceCmdFireCheck.Process == nil {
logger.Logger.Printf("Fire_check inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID)
return
}
if err := inferenceCmdFireCheck.Process.Signal(syscall.Signal(0)); err != nil {
logger.Logger.Printf("Fire_check inference program exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
logger.Logger.Printf("Both inference programs started successfully: DeviceUUID=%s", deviceData.DeviceUUID)
if err := logProcessInfo(inferenceCmdFireLeave.Process.Pid, deviceData.DeviceUUID); err != nil {
logger.Logger.Printf("Failed to record fire_leave process information: %v", err)
}
if err := logProcessInfo(inferenceCmdFireCheck.Process.Pid, fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)); err != nil {
logger.Logger.Printf("Failed to record fire_check process information: %v", err)
}
defer func() {
logger.Logger.Printf("Start to stop inference programs (DeviceUUID=%s)", deviceData.DeviceUUID)
// 停止fire_leave推理程序
if inferenceCmdFireLeave.Process != nil {
if err := inferenceCmdFireLeave.Process.Kill(); err != nil {
logger.Logger.Printf("Fire_leave inference program termination failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
} else {
logger.Logger.Printf("Fire_leave inference program stopped (DeviceUUID=%s)", deviceData.DeviceUUID)
}
}
// 停止fire_check推理程序
if inferenceCmdFireCheck.Process != nil {
if err := inferenceCmdFireCheck.Process.Kill(); err != nil {
logger.Logger.Printf("Fire_check inference program termination failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
} else {
logger.Logger.Printf("Fire_check inference program stopped (DeviceUUID=%s)", deviceData.DeviceUUID)
}
}
}()
// 启动温度程序
// 在 processDevice 函数中修改温度程序启动部分
// 启动温度程序
tempPort := portFireLeave + 1000
tempCmdArgs := []string{
"-s", deviceData.CameraRTSP,
"-t", fmt.Sprintf("%d", tempPort),
}
if deviceData.CameraChannel > 0 {
tempCmdArgs = append(tempCmdArgs, "-c", fmt.Sprintf("%d", deviceData.CameraChannel))
}
tempCmd := exec.Command("./release/run_thermometry.sh", tempCmdArgs...)
tempCmd.Stdout = os.Stdout
tempCmd.Stderr = os.Stderr
logger.Logger.Printf("Start the temperature program command: %v", tempCmd.Args)
if err := tempCmd.Start(); err != nil {
logger.Logger.Printf("The temperature program failed to start (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 增强进程检查
time.Sleep(3 * time.Second) // 减少等待时间
if tempCmd.Process == nil || tempCmd.Process.Pid == 0 {
logger.Logger.Printf("The temperature program was not started correctly, and the process PID is 0 (DeviceUUID=%s)", deviceData.DeviceUUID)
return
}
// 检查进程是否真的在运行
if err := tempCmd.Process.Signal(syscall.Signal(0)); err != nil {
logger.Logger.Printf("The temperature program process check failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
logger.Logger.Printf("The temperature program has been successfully startedPID: %d (DeviceUUID=%s)", tempCmd.Process.Pid, deviceData.DeviceUUID)
// 增加TCP服务启动前的额外检查
logger.Logger.Printf("Wait for the temperature program to be initialized... (DeviceUUID=%s)", deviceData.DeviceUUID)
time.Sleep(2 * time.Second)
// 等待 TCP 服务器就绪
logger.Logger.Printf("Wait for the temperature program TCP server to start... (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, tempPort)
if !waitForTCPService(tempPort, 20*time.Second) { // 增加超时时间
logger.Logger.Printf("The TCP server of the temperature program has timed out during startup (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, tempPort)
// 检查温度程序是否还在运行
if tempCmd.Process != nil {
if err := tempCmd.Process.Signal(syscall.Signal(0)); err != nil {
logger.Logger.Printf("The temperature program has abnormally exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
} else {
logger.Logger.Printf("The temperature program is still running but the TCP service has not started. There might be a configuration issue(DeviceUUID=%s)", deviceData.DeviceUUID)
}
}
return
}
// 创建 TCP 客户端
tempClient, err := tcpreader.NewTCPClient(deviceData.DeviceUUID, tempPort)
if err != nil {
logger.Logger.Printf("Failed to create a TCP client (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
defer tempClient.Close()
logger.Logger.Printf("The TCP client has been successfully created (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, tempPort)
// 确保上传目录存在
if err := os.MkdirAll("/data/upload", 0755); err != nil {
logger.Logger.Printf("Failed to create the upload directory: %v", err)
return
}
logger.Logger.Printf("The directory upload has been successfully created: /data/upload")
// 主循环:从通道接收推理数据
logger.Logger.Printf("Start monitoring the equipment (DeviceUUID=%s)", deviceData.DeviceUUID)
//var lastPersonCount = -1
//var lastTemperature float64 = -1
monitorTicker := time.NewTicker(2 * time.Second)
defer monitorTicker.Stop()
for {
select {
case <-deviceCtx.Done():
logger.Logger.Printf("Equipment processing has been cancelled: DeviceUUID=%s", deviceData.DeviceUUID)
// 强制停止所有子进程
if inferenceCmdFireLeave.Process != nil {
inferenceCmdFireLeave.Process.Kill()
}
if inferenceCmdFireCheck.Process != nil {
inferenceCmdFireCheck.Process.Kill()
}
if tempCmd.Process != nil {
tempCmd.Process.Kill()
}
tempClient.Close()
return
case inferenceResult, ok := <-inferenceServerFireLeave.GetDataChan():
if !ok {
logger.Logger.Printf("The fire_leave inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID)
return
}
currentPersonCount := 0
for _, param := range inferenceResult.Params {
if param.ClassIdx == 1 {
currentPersonCount = param.Number
break
}
}
if currentPersonCount != deviceData.PersonCount {
deviceData.PersonCount = currentPersonCount
connect.UpdateDeviceData(deviceData)
logger.Logger.Printf("Personnel quantity update: %d (DeviceUUID=%s)", currentPersonCount, deviceData.DeviceUUID)
}
case fireCheckResult, ok := <-inferenceServerFireCheck.GetDataChan():
if !ok {
logger.Logger.Printf("The fire_check inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID)
return
}
// 处理fire_check推理结果
logger.Logger.Printf("Received fire_check inference data: DeviceUUID=%s", deviceData.DeviceUUID)
// 检查是否检测到火焰
fireDetected := false
for _, param := range fireCheckResult.Params {
if param.ClassIdx == 0 && param.Number > 0 { // 假设ClassIdx=0表示火焰
fireDetected = true
logger.Logger.Printf("Fire detected! DeviceUUID=%s, Number=%d", deviceData.DeviceUUID, param.Number)
break
}
}
// 处理火焰检测的连续时间判断
state := getDeviceState(deviceData.DeviceUUID)
now := time.Now().Unix()
// 解析detection_time配置
detectionSec := 2 // 默认2秒
if deviceData.FireCheckParam.DetectionTime != "" {
if sec, err := strconv.Atoi(deviceData.FireCheckParam.DetectionTime); err == nil && sec > 0 {
detectionSec = sec
}
}
if fireDetected {
if !state.FireCheckInCondition {
// 开始计时
state.FireCheckStartTime = now
state.FireCheckInCondition = true
logger.Logger.Printf("Fire detection started, timing for %ds: DeviceUUID=%s", detectionSec, deviceData.DeviceUUID)
} else if now-state.FireCheckStartTime >= int64(detectionSec) {
// 连续检测到火焰达到指定时间,触发上报
logger.Logger.Printf("Fire detected continuously for %ds, triggering alarm: DeviceUUID=%s", detectionSec, deviceData.DeviceUUID)
go func() {
alarmTime := time.Now().Unix()
logger.Logger.Printf("[%.3f] FIRE DETECTED ALARM TRIGGERED (DeviceUUID=%s)", time.Since(time.Now()).Seconds(), deviceData.DeviceUUID)
// 调用专门的火焰检测上报函数
GlobalReporter.TriggerFireCheckAlarm(deviceData, alarmTime)
}()
// 重置状态
state.FireCheckInCondition = false
}
} else {
// 没有检测到火焰,重置状态
if state.FireCheckInCondition {
logger.Logger.Printf("Fire detection interrupted: lasted %ds < %ds (DeviceUUID=%s)", now-state.FireCheckStartTime, detectionSec, deviceData.DeviceUUID)
state.FireCheckInCondition = false
}
}
case <-monitorTicker.C:
now := time.Now().Unix()
// === 获取当前设备独立状态 ===
state := getDeviceState(deviceData.DeviceUUID)
// === 读取温度(防抖)===
tempResult, err := tempClient.ReadTemperatureResult()
if err == nil {
if math.Abs(tempResult.Tmp-deviceData.Temperature) >= 0.5 {
oldTemp := deviceData.Temperature
deviceData.Temperature = tempResult.Tmp
connect.UpdateDeviceData(deviceData)
GlobalDeviceManager.AddDevice(deviceData)
logger.Logger.Printf("Temperature update: %.1f°C → %.1f°C (DeviceUUID=%s)",
oldTemp, tempResult.Tmp, deviceData.DeviceUUID)
}
}
// 获取参数
detectionSec := getIntFromNumber(deviceData.DetectionTime, 20)
alarmCooldown := getIntFromNumber(deviceData.AlarmTime, 300)
// === 防抖:无人 + 温度稳定超阈值 ===
noPerson := deviceData.PersonCount == 0
// 温度防抖
var tempStableHigh bool
threshold := deviceData.TemperatureThreshold
if deviceData.Temperature > threshold+0.5 {
tempStableHigh = true
} else if deviceData.Temperature < threshold-0.5 {
tempStableHigh = false
} else {
tempStableHigh = deviceData.Temperature > threshold
}
conditionMet := noPerson && tempStableHigh
if now < state.FireLeaveForbidUntil {
if state.InCondition {
state.InCondition = false
}
logger.Logger.Printf("fire_leave report paused: %ds remaining (DeviceUUID=%s)",
state.FireLeaveForbidUntil-now, deviceData.DeviceUUID)
continue
}
// 1. 冷却期检查
if now < state.AlarmPauseUntil {
if state.InCondition {
logger.Logger.Printf("Alarm in cooldown: %ds remaining (DeviceUUID=%s)",
state.AlarmPauseUntil-now, deviceData.DeviceUUID)
state.InCondition = false
}
continue
}
// 2. 条件满足
if conditionMet {
if !state.InCondition {
state.StartTime = now
state.InCondition = true
logger.Logger.Printf("Alarm condition met, start timing (%ds required): PersonCount=%d, Temp=%.1f°C > %.1f°C (DeviceUUID=%s)",
detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
} else if now-state.StartTime >= int64(detectionSec) {
currentTime := time.Now()
logger.Logger.Printf("[%d] ALARM TRIGGERED (after %ds): PersonCount=%d, Temp=%.1f°C > %.1f°C (DeviceUUID=%s)",
currentTime.Unix(), detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
// 异步处理报警,避免阻塞主线程
go func() {
alarmStartTime := time.Now()
logger.Logger.Printf("[%d] 开始异步处理报警 (DeviceUUID=%s, alarmTime=%d, cooldown=%d)",
alarmStartTime.Unix(), deviceData.DeviceUUID, state.StartTime, alarmCooldown)
// 捕获所有异常,确保goroutine不会因为panic而崩溃
defer func() {
if err := recover(); err != nil {
logger.Logger.Printf("[%d] 处理报警发生panic: %v (DeviceUUID=%s)",
time.Now().Unix(), err, deviceData.DeviceUUID)
}
}()
// 调用报警处理函数
GlobalReporter.TriggerAlarmWithDelay(deviceData, state.StartTime, alarmCooldown)
logger.Logger.Printf("[%d] 报警处理完成 (DeviceUUID=%s, 耗时:%.3fs)",
time.Now().Unix(), deviceData.DeviceUUID, time.Since(alarmStartTime).Seconds())
}()
// 更新状态
state.AlarmPauseUntil = now + int64(alarmCooldown)
state.InCondition = false
logger.Logger.Printf("[%d] 更新报警状态: 冷却时间设置为 %d 秒 (DeviceUUID=%s)",
time.Now().Unix(), alarmCooldown, deviceData.DeviceUUID)
// 更新设备数据
deviceDataCopy := deviceData
deviceDataCopy.AlarmPauseUntil = state.AlarmPauseUntil
logger.Logger.Printf("[%d] 更新设备数据 (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID)
connect.UpdateDeviceData(deviceDataCopy)
logger.Logger.Printf("[%d] 设备数据更新完成 (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID)
}
} else {
if state.InCondition {
logger.Logger.Printf("Alarm condition interrupted: lasted %ds < %ds (DeviceUUID=%s)",
now-state.StartTime, detectionSec, deviceData.DeviceUUID)
state.InCondition = false
}
}
}
}
}
func waitForShutdown(ctx context.Context, cancel context.CancelFunc, wg *sync.WaitGroup) {
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
select {
case sig := <-stop:
logger.Logger.Printf("Received stop signal: %v, start cleaning...", sig)
// 立即取消上下文,通知所有goroutine停止
cancel()
// 等待所有goroutine退出
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
logger.Logger.Printf("All goroutines exited normally")
case <-time.After(15 * time.Second):
logger.Logger.Printf("Waiting time exceeded, force exit")
// 强制终止所有推理进程
connect.InferenceProcesses.Range(func(key, value interface{}) bool {
if cmd, ok := value.(*exec.Cmd); ok && cmd.Process != nil {
logger.Logger.Printf("Force kill process: %v", key)
cmd.Process.Kill()
}
return true
})
}
case <-ctx.Done():
logger.Logger.Printf("The context has been cancelled.")
}
logger.Logger.Printf("The program exits successfully.")
os.Exit(0)
}
func initWebSocketAsync(ctx context.Context, wg *sync.WaitGroup) {
logger.Logger.Printf("Start the initialization of asynchronous WebSocket...")
if err := connect.InitWSChannel(); err != nil {
logger.Logger.Printf("Failed to initialize WebSocket channel: %v", err)
return
}
logger.Logger.Printf("Waiting for WebSocket connection to be established...")
containerName := "fireleave-container"
for i := 0; i < 30; i++ {
select {
case <-ctx.Done():
logger.Logger.Printf("Context cancelled, aborting WebSocket registration")
return
case <-time.After(1 * time.Second):
client := connect.GetWSClient()
if client != nil && client.IsConnected() {
goto register
}
}
}
logger.Logger.Printf("WebSocket connection timeout after 30 seconds")
return
register:
serviceID, err := connect.RegisterService(containerName)
if err != nil {
logger.Logger.Printf("Service registration failed: %v", err)
} else {
logger.Logger.Printf("Service registration successful: ServiceID=%s", serviceID)
}
// 更新GlobalReporter的WebSocket客户端
wsClient := connect.GetWSClient()
GlobalReporter.SetWSClient(wsClient)
logger.Logger.Printf("Updated GlobalReporter with WebSocket client: %v", wsClient != nil)
// 新增:订阅本地告警 topic
if err := connect.SubscribeTopic("/dhlr/alert", connect.HandleBuzzerAlert); err != nil {
logger.Logger.Printf("订阅告警topic失败 (/dhlr/alert): %v", err)
} else {
logger.Logger.Printf("成功订阅本地告警topic: /dhlr/alert")
}
// 新增:订阅禁止时间 topic
if err := connect.SubscribeTopic("/dhlr/forbid_time", handleForbidTimeMessage); err != nil {
logger.Logger.Printf("订阅禁止时间topic失败 (/dhlr/forbid_time): %v", err)
} else {
logger.Logger.Printf("成功订阅禁止时间topic: /dhlr/forbid_time")
}
}
func (dm *DeviceManager) GetAllDevices() map[string]*connect.DeviceData {
dm.mu.Lock()
defer dm.mu.Unlock()
result := make(map[string]*connect.DeviceData)
for k, v := range dm.devices {
result[k] = v
}
return result
}
func (dm *DeviceManager) RangeDevices(fn func(uuid string, device *connect.DeviceData) bool) {
dm.mu.Lock()
defer dm.mu.Unlock()
for uuid, device := range dm.devices {
if !fn(uuid, device) {
return
}
}
}
func main() {
fmt.Printf("FireLeave Tool build: %s\n", buildVersion)
var rtspURL string
var deviceUUID string
flag.StringVar(&rtspURL, "rtsp", "", "RTSP stream URL for testing")
flag.StringVar(&deviceUUID, "uuid", "", "Device UUID for testing")
flag.Parse()
if err := logger.InitLogger(); err != nil {
log.Fatalf("Logger initialization failed: %v", err)
}
defer logger.CloseLogger()
// 初始化上报管理器专用日志
if err := logger.InitReporterLogger(); err != nil {
logger.Logger.Printf("Reporter logger initialization failed: %v", err)
}
// 初始化GPIO监控专用日志
if err := logger.InitGPILogger(); err != nil {
logger.Logger.Printf("GPIO logger initialization failed: %v", err)
}
go func() {
for {
logger.CleanupOldLogs()
time.Sleep(24 * time.Hour)
}
}()
logger.Logger.Printf("=== Starting FireLeave Tool ===")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var wg sync.WaitGroup
logger.Logger.Printf("Step 1: Initialize device data from service config")
deviceDataList, err := connect.InitializeDevicesFromServiceConfig()
if err != nil {
logger.Logger.Printf("Device data initialization failed: %v", err)
logger.Logger.Printf("Will try to use existing device files...")
deviceDataList = connect.GetDeviceDataList()
}
logger.Logger.Printf("Step 2: Starting %d devices", len(deviceDataList))
for i, deviceData := range deviceDataList {
logger.Logger.Printf("Starting device[%d]: UUID=%s", i, deviceData.DeviceUUID)
wg.Add(1)
go func(deviceData connect.DeviceData, index int) {
defer wg.Done()
defer GlobalDeviceManager.StopDevice(deviceData.DeviceUUID)
logger.Logger.Printf("Device processing goroutine started: UUID=%s, Index=%d", deviceData.DeviceUUID, index)
processDevice(ctx, deviceData)
}(deviceData, i)
}
logger.Logger.Printf("Step 3: All devices started")
logger.Logger.Printf("Step 4: Asynchronously initialize WebSocket connection")
go initWebSocketAsync(ctx, &wg)
logger.Logger.Printf("Step 5: Start GPIO button monitoring")
go func() {
// 创建GPIO按钮监视器
monitor := gpio_monitor.NewGPIOButtonMonitor(gpio_monitor.ButtonGPIO)
monitor.SetOnPress(func() {
minute := reporter.GetForbidTimeConfig()
duration := reporter.ParseForbidDuration(minute, defaultFireLeaveForbidDuration)
persistAndApplyFireLeaveForbidWindow(minute, duration)
})
// 启动GPIO监控
if err := monitor.Start(ctx); err != nil {
logger.Logger.Printf("Failed to start GPIO button monitor: %v", err)
} else {
logger.Logger.Printf("GPIO button monitor started successfully")
}
}()
logger.Logger.Printf("=== Main program startup completed ===")
logger.Logger.Printf("System status: Running devices=%d", len(deviceDataList))
// Add device status monitoring
go monitorDeviceStatus(ctx, deviceDataList)
go func() {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
for range ticker.C {
GlobalDeviceManager.RangeDevices(func(uuid string, device *connect.DeviceData) bool {
// 检查fire_leave推理程序心跳
if connect.ShouldRestartInference(uuid) {
logger.Logger.Printf("fire_leave推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid)
connect.StopInferenceProcess(uuid)
time.Sleep(6 * time.Second)
connect.StartInferenceProcess(device)
connect.UpdateInferenceHeartbeat(uuid)
}
// 检查fire_check推理程序心跳
fireCheckUUID := fmt.Sprintf("%s_fire_check", uuid)
if connect.ShouldRestartInference(fireCheckUUID) {
logger.Logger.Printf("fire_check推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid)
connect.StopInferenceProcess(fireCheckUUID)
time.Sleep(6 * time.Second)
connect.StartInferenceProcess(device)
connect.UpdateInferenceHeartbeat(fireCheckUUID)
}
return true
})
}
}()
go func() {
ticker := time.NewTicker(5 * time.Minute)
for range ticker.C {
for _, device := range GlobalDeviceManager.GetAllDevices() {
video_server.CleanupOldRawVideos(device.DeviceUUID)
}
}
}()
waitForShutdown(ctx, cancel, &wg)
}
func printGlobalDeviceStatus() {
now := time.Now().Unix()
activeDevices := GlobalDeviceManager.GetAllDevices()
if len(activeDevices) == 0 {
logger.Logger.Printf("=== Global Device Status monitoring === No running devices for the time being")
return
}
logger.Logger.Printf("=== Global Device Status monitoring === Currently running %d", len(activeDevices))
i := 1
for uuid, data := range activeDevices {
state := getDeviceState(uuid)
cooldownLeft := int64(0)
if now < state.AlarmPauseUntil {
cooldownLeft = state.AlarmPauseUntil - now
}
logger.Logger.Printf(" [%d] DeviceUUID=%s | person:%d | temp:%.1f°C | threshold value:%.1f°C | cooling time:%ds",
i,
uuid,
data.PersonCount,
data.Temperature,
data.TemperatureThreshold,
cooldownLeft,
)
i++
}
}
func monitorDeviceStatus(ctx context.Context, initialDevices []connect.DeviceData) {
ticker := time.NewTicker(30 * time.Second)
defer ticker.Stop()
logger.Logger.Printf("Start global device status monitoring (print once every 30 seconds)")
for {
select {
case <-ticker.C:
printGlobalDeviceStatus()
if files, err := filepath.Glob("/data/devices/device_*.json"); err == nil {
logger.Logger.Printf("Device file status: A total of %d device files", len(files))
for i, file := range files {
if i < 3 {
logger.Logger.Printf(" device file[%d]: %s", i, filepath.Base(file))
}
}
}
logger.Logger.Printf("Operating equipment %d ", len(GlobalDeviceManager.GetAllDevices()))
case <-ctx.Done():
logger.Logger.Printf("Global device status monitoring has stopped")
return
}
}
}
func debugDeviceData(deviceData connect.DeviceData) {
logger.Logger.Printf("Debug device data: UUID=%s", deviceData.DeviceUUID)
logger.Logger.Printf(" TaskID: %q", deviceData.TaskID)
logger.Logger.Printf(" CameraRTSP: %q", deviceData.CameraRTSP)
logger.Logger.Printf(" CameraIP: %q", deviceData.CameraIP)
logger.Logger.Printf(" UploadURL: %q", deviceData.UploadURL)
logger.Logger.Printf(" DetectAreaStr: %q", deviceData.DetectAreaStr)
logger.Logger.Printf(" PersonCount: %d", deviceData.PersonCount)
logger.Logger.Printf(" Temperature: %f", deviceData.Temperature)
}
// handleForbidTimeMessage 处理禁止时间配置消息
func handleForbidTimeMessage(message []byte) {
logger.Logger.Printf("Received forbid time message: %s", string(message))
// 解析消息
var msg struct {
UUID string `json:"uuid"`
Topic string `json:"topic"`
QoS int `json:"qos"`
Content json.RawMessage `json:"content"`
Timeout int `json:"timeout"`
}
if err := json.Unmarshal(message, &msg); err != nil {
logger.Logger.Printf("Failed to parse forbid time message: %v", err)
return
}
// 解析content字段
var content struct {
Minute string `json:"minute"`
FireLeaveTimeout string `json:"fire_leave_timeout,omitempty"`
FireCheckTimeout string `json:"fire_check_timeout,omitempty"`
}
if err := json.Unmarshal(msg.Content, &content); err != nil {
logger.Logger.Printf("Failed to parse content: %v", err)
return
}
// 处理禁止时间配置
if content.Minute != "" {
logger.Logger.Printf("Forbid time config received: Minute=%s", content.Minute)
persistAndApplyFireLeaveForbidWindow(content.Minute, reporter.ParseForbidDuration(content.Minute, 0))
}
// 处理事件周期时限配置
if content.FireLeaveTimeout != "" || content.FireCheckTimeout != "" {
// 解析时间
fireLeaveTimeout, err1 := time.ParseDuration(content.FireLeaveTimeout)
fireCheckTimeout, err2 := time.ParseDuration(content.FireCheckTimeout)
// 如果解析失败,使用默认值
if err1 != nil {
fireLeaveTimeout = 30 * time.Minute
logger.Logger.Printf("Invalid fire_leave_timeout, using default: 30m0s")
}
if err2 != nil {
fireCheckTimeout = 30 * time.Minute
logger.Logger.Printf("Invalid fire_check_timeout, using default: 30m0s")
}
logger.Logger.Printf("Event cycle timeout config received: FireLeaveTimeout=%v, FireCheckTimeout=%v", fireLeaveTimeout, fireCheckTimeout)
// 保存到缓存文件
if err := reporter.SaveEventCycleTimeoutConfig(fireLeaveTimeout, fireCheckTimeout); err != nil {
logger.Logger.Printf("Failed to save event cycle timeout config: %v", err)
} else {
logger.Logger.Printf("Event cycle timeout config saved successfully: FireLeaveTimeout=%v, FireCheckTimeout=%v", fireLeaveTimeout, fireCheckTimeout)
}
}
}