diff --git a/bin/fireleave_tool b/bin/fireleave_tool index ff98ef5..7f8317d 100644 Binary files a/bin/fireleave_tool and b/bin/fireleave_tool differ diff --git a/connect/device_storage.go b/connect/device_storage.go index 23f52e6..f00ac90 100644 --- a/connect/device_storage.go +++ b/connect/device_storage.go @@ -270,6 +270,15 @@ func NormalizeDeviceRuntimeFields(deviceData *DeviceData) { } } +func GetFireCheckConfidence(deviceData DeviceData) int { + if confidenceText := strings.TrimSpace(deviceData.FireCheckParam.Confidence); confidenceText != "" { + if confidence, err := strconv.Atoi(confidenceText); err == nil && confidence > 0 { + return confidence + } + } + return deviceData.Confidence +} + // ProcessDetectAreaForInference processes detection area data, converts to format required by inference program func ProcessDetectAreaForInference(areas []Area) string { if len(areas) == 0 { diff --git a/connect/heartbeat.go b/connect/heartbeat.go index 2095a63..e823b3e 100644 --- a/connect/heartbeat.go +++ b/connect/heartbeat.go @@ -12,126 +12,100 @@ import ( ) var ( - InferenceProcesses sync.Map // key: deviceUUID string, value: *exec.Cmd - - // 记录最后一次收到心跳的时间 - inferenceLastHeartbeat sync.Map // key: deviceUUID string, value: time.Time + InferenceProcesses sync.Map // key: process UUID, value: *exec.Cmd + inferenceLastHeartbeat sync.Map // key: process UUID, value: time.Time ) -// UpdateInferenceHeartbeat 每收到一帧 /video/post 就调用这个 -func UpdateInferenceHeartbeat(deviceUUID string) { - inferenceLastHeartbeat.Store(deviceUUID, time.Now()) - // 输出心跳日志 - if strings.Contains(deviceUUID, "_fire_check") { - logger.Logger.Printf("推理心跳收到 | 时间: %s | UUID: %s | 类型: fire_check", time.Now().Format("2006/01/02 15:04:05.000"), deviceUUID) +// UpdateInferenceHeartbeat records the latest heartbeat time for one inference process. +func UpdateInferenceHeartbeat(processUUID string) { + inferenceLastHeartbeat.Store(processUUID, time.Now()) + if strings.Contains(processUUID, "_fire_check") { + logger.Logger.Printf("推理心跳收到 | 时间: %s | UUID: %s | 类型: fire_check", time.Now().Format("2006/01/02 15:04:05.000"), processUUID) } else { - logger.Logger.Printf("推理心跳收到 | 时间: %s | UUID: %s | 类型: fire_leave", time.Now().Format("2006/01/02 15:04:05.000"), deviceUUID) + logger.Logger.Printf("推理心跳收到 | 时间: %s | UUID: %s | 类型: fire_leave", time.Now().Format("2006/01/02 15:04:05.000"), processUUID) } } -// ShouldRestartInference 判断是否需要重启推理程序 -func ShouldRestartInference(deviceUUID string) bool { - val, ok := inferenceLastHeartbeat.Load(deviceUUID) +// ShouldRestartInference reports whether one inference process should be restarted. +func ShouldRestartInference(processUUID string) bool { + val, ok := inferenceLastHeartbeat.Load(processUUID) if !ok { - return true // 从未收到过心跳,肯定要启动 + return true } last := val.(time.Time) return time.Since(last) > 85*time.Second } -// StopInferenceProcess 停止推理进程(供 main 和 DeviceManager 调用) -func StopInferenceProcess(deviceUUID string) { - if raw, ok := InferenceProcesses.Load(deviceUUID); ok { +// StopInferenceProcess stops one inference process. +func StopInferenceProcess(processUUID string) { + if raw, ok := InferenceProcesses.Load(processUUID); ok { if cmd, ok := raw.(*exec.Cmd); ok && cmd.Process != nil { - logger.Logger.Printf("正在停止推理进程 (DeviceUUID=%s, PID=%d)", deviceUUID, cmd.Process.Pid) - cmd.Process.Kill() - cmd.Wait() + logger.Logger.Printf("正在停止推理进程 (ProcessUUID=%s, PID=%d)", processUUID, cmd.Process.Pid) + _ = cmd.Process.Kill() + _ = cmd.Wait() } - InferenceProcesses.Delete(deviceUUID) + InferenceProcesses.Delete(processUUID) } } -// StartInferenceProcess 重启推理程序(保持原端口!) +// StartInferenceProcess keeps the old "restart both" behavior for compatibility. func StartInferenceProcess(deviceData *DeviceData) { - - // 重启fire_leave推理程序 - StopInferenceProcess(deviceData.DeviceUUID) - - portFireLeave, err := GlobalPortManager.GetPort(deviceData.DeviceUUID) - if err != nil { - logger.Logger.Printf("获取fire_leave端口失败,无法重启推理程序 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) - return - } - - // 重启fire_check推理程序 - StopInferenceProcess(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)) - - portFireCheck, err := GlobalPortManager.GetPort(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)) - if err != nil { - logger.Logger.Printf("获取fire_check端口失败,无法重启推理程序 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) - return - } - - // 为fire_leave推理程序获取检测区域 - fireLeaveDetectAreaStr := ProcessDetectAreaForInference(deviceData.FireLeaveDetectArea) - - // 启动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("正在重启fire_leave推理程序 (使用原端口 %d): %v", portFireLeave, inferenceCmdFireLeave.Args) - - if err := inferenceCmdFireLeave.Start(); err != nil { + if err := RestartInferenceProcess(deviceData, deviceData.DeviceUUID); err != nil { logger.Logger.Printf("fire_leave推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) - return } - - // 保存fire_leave进程对象 - InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmdFireLeave) - logger.Logger.Printf("fire_leave推理程序已成功重启,PID=%d (DeviceUUID=%s, Port=%d)", - inferenceCmdFireLeave.Process.Pid, deviceData.DeviceUUID, portFireLeave) - - // 为fire_check推理程序获取检测区域 - fireCheckDetectAreaStr := ProcessDetectAreaForInference(deviceData.FireCheckDetectArea) - - // 启动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("正在重启fire_check推理程序 (使用原端口 %d): %v", portFireCheck, inferenceCmdFireCheck.Args) - - if err := inferenceCmdFireCheck.Start(); err != nil { + if err := RestartInferenceProcess(deviceData, fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)); err != nil { logger.Logger.Printf("fire_check推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) - return + } +} + +// RestartInferenceProcess restarts only the requested inference process UUID. +func RestartInferenceProcess(deviceData *DeviceData, processUUID string) error { + if deviceData == nil { + return fmt.Errorf("device data is nil") } - // 保存fire_check进程对象 - InferenceProcesses.Store(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), inferenceCmdFireCheck) - logger.Logger.Printf("fire_check推理程序已成功重启,PID=%d (DeviceUUID=%s, Port=%d)", - inferenceCmdFireCheck.Process.Pid, deviceData.DeviceUUID, portFireCheck) + port, err := GlobalPortManager.GetPort(processUUID) + if err != nil { + return fmt.Errorf("get port failed: %w", err) + } - // 重启后立即刷新一次心跳,防止 30s 检测又触发 - UpdateInferenceHeartbeat(deviceData.DeviceUUID) - UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)) + modelName := "model" + detectAreaStr := ProcessDetectAreaForInference(deviceData.FireLeaveDetectArea) + args := []string{ + "-s", deviceData.CameraRTSP, + "-m", modelName, + "-c", processUUID, + "-p", fmt.Sprintf("%d", port), + "-w", detectAreaStr, + "-r", fmt.Sprintf("%d", deviceData.Confidence), + } + + if strings.HasSuffix(processUUID, "_fire_check") { + modelName = "fire_check" + detectAreaStr = ProcessDetectAreaForInference(deviceData.FireCheckDetectArea) + confidence := GetFireCheckConfidence(*deviceData) + args = []string{ + "-s", deviceData.CameraRTSP, + "-m", modelName, + "-c", processUUID, + "-p", fmt.Sprintf("%d", port), + "-w", detectAreaStr, + "-r", fmt.Sprintf("%d", confidence), + } + } + + StopInferenceProcess(processUUID) + cmd := exec.Command("./yolov5", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + logger.Logger.Printf("正在重启推理程序 (ProcessUUID=%s, Port=%d): %v", processUUID, port, cmd.Args) + if err := cmd.Start(); err != nil { + return err + } + + InferenceProcesses.Store(processUUID, cmd) + UpdateInferenceHeartbeat(processUUID) + logger.Logger.Printf("推理程序已成功重启,PID=%d (ProcessUUID=%s, Port=%d)", cmd.Process.Pid, processUUID, port) + return nil } diff --git a/connect/ws_channel.go b/connect/ws_channel.go index c2abb73..47e0bcc 100644 --- a/connect/ws_channel.go +++ b/connect/ws_channel.go @@ -132,6 +132,7 @@ type WSClient struct { subscribers map[string]func([]byte) closeChan chan struct{} reconnectFlag bool + reconnecting bool } // NewWSClient creates WebSocket client @@ -185,7 +186,6 @@ func InitWSChannel() error { } isConnecting = true - defer func() { isConnecting = false }() logger.Logger.Printf("Starting WebSocket connection initialization: %s", wsURL) @@ -194,15 +194,21 @@ func InitWSChannel() error { client, err := NewWSClient(wsURL) if err != nil { logger.Logger.Printf("Failed to create WebSocket client: %v", err) + wsMutex.Lock() + isConnecting = false + wsMutex.Unlock() return } + wsMutex.Lock() + defer wsMutex.Unlock() if ConfigUpdateChan == nil { ConfigUpdateChan = make(chan DeviceData, 100) } // Note: No longer calling LoadDeviceData() here to avoid duplicate loading wsClient = client + isConnecting = false logger.Logger.Printf("WebSocket connection initialized successfully (asynchronous)") }() @@ -228,7 +234,8 @@ func GetServiceID() string { // RegisterService service registration - modified: now requires providing own server_id func RegisterService(containerName string) (string, error) { - if wsClient == nil { + client := GetWSClient() + if client == nil { return "", fmt.Errorf("WebSocket client not initialized") } @@ -243,7 +250,7 @@ func RegisterService(containerName string) (string, error) { }, } logger.Logger.Printf("Starting service registration: ContainerName=%s, ServiceID=%s", containerName, serviceID) - response, err := wsClient.sendRequest("register", params, true) + response, err := client.sendRequest("register", params, true) if err != nil { logger.Logger.Printf("Service registration failed: %v", err) return "", err @@ -258,21 +265,24 @@ func RegisterService(containerName string) (string, error) { return "", fmt.Errorf("Service registration failed: Server returned abnormal response") } - wsClient.serviceID = serviceID + client.mutex.Lock() + client.serviceID = serviceID + client.mutex.Unlock() logger.Logger.Printf("Service registration successful: ServiceID=%s", serviceID) return serviceID, nil } // SubscribeTopic topic subscription func SubscribeTopic(topic string, callback func([]byte)) error { - if wsClient == nil { + client := GetWSClient() + if client == nil { return fmt.Errorf("WebSocket client not initialized") } params := SubscribeParams{ Topic: topic, } logger.Logger.Printf("Starting topic subscription: Topic=%s", topic) - response, err := wsClient.sendRequest("subscribe", params, true) + response, err := client.sendRequest("subscribe", params, true) if err != nil { logger.Logger.Printf("Topic subscription failed: %v", err) return err @@ -281,9 +291,9 @@ func SubscribeTopic(topic string, callback func([]byte)) error { logger.Logger.Printf("Topic subscription error: Code=%d, Message=%s", response.Error.Code, response.Error.Message) return fmt.Errorf("Topic subscription error: %s", response.Error.Message) } - wsClient.mutex.Lock() - wsClient.subscribers[topic] = callback - wsClient.mutex.Unlock() + client.mutex.Lock() + client.subscribers[topic] = callback + client.mutex.Unlock() logger.Logger.Printf("Topic subscription successful: Topic=%s", topic) return nil } @@ -344,7 +354,7 @@ func (c *WSClient) sendBinaryFrame(frameType byte, payload []byte) error { if err != nil { logger.Logger.Printf("WebSocket message sending failed: %v", err) c.connected = false - go c.reconnect() + c.startReconnectLocked() return fmt.Errorf("Message sending failed: %v", err) } return nil @@ -431,7 +441,7 @@ func (c *WSClient) readLoop() { c.mutex.Lock() c.connected = false c.mutex.Unlock() - go c.reconnect() // Trigger reconnection + c.startReconnect() // Trigger reconnection return } @@ -569,7 +579,7 @@ func (c *WSClient) heartbeatLoop() { c.mutex.Lock() c.connected = false c.mutex.Unlock() - go c.reconnect() + c.startReconnect() } else { logger.Logger.Printf("Ping sent successfully") } @@ -596,7 +606,13 @@ func (c *WSClient) reRegisterAndResubscribe() { logger.Logger.Printf("Re-registration successful: ServiceID=%s", serviceID) // 3. 重新订阅 + c.mutex.Lock() + subscribers := make(map[string]func([]byte), len(c.subscribers)) for topic, callback := range c.subscribers { + subscribers[topic] = callback + } + c.mutex.Unlock() + for topic, callback := range subscribers { if err := SubscribeTopic(topic, callback); err != nil { logger.Logger.Printf("Failed to re-subscribe topic %s: %v", topic, err) } else { @@ -605,9 +621,32 @@ func (c *WSClient) reRegisterAndResubscribe() { } } +func (c *WSClient) startReconnect() { + c.mutex.Lock() + defer c.mutex.Unlock() + c.startReconnectLocked() +} + +func (c *WSClient) startReconnectLocked() { + if !c.reconnectFlag || c.reconnecting { + return + } + c.reconnecting = true + go c.reconnect() +} + // connect/wschannel.go → reconnect complete fix func (c *WSClient) reconnect() { - if !c.reconnectFlag { + defer func() { + c.mutex.Lock() + c.reconnecting = false + c.mutex.Unlock() + }() + + c.mutex.Lock() + reconnectFlag := c.reconnectFlag + c.mutex.Unlock() + if !reconnectFlag { return } @@ -636,12 +675,13 @@ func (c *WSClient) reconnect() { // 重启读写循环 go c.readLoop() - go c.heartbeatLoop() return } logger.Logger.Printf("Reconnection failed after 12 attempts") + c.mutex.Lock() c.reconnectFlag = false + c.mutex.Unlock() } func (d *DeviceData) ToInferenceParams(port int) InferenceParams { return InferenceParams{ diff --git a/docs/2026-07-05_2240_global_status_log_fix.md b/docs/2026-07-05_2240_global_status_log_fix.md new file mode 100644 index 0000000..74f0792 --- /dev/null +++ b/docs/2026-07-05_2240_global_status_log_fix.md @@ -0,0 +1,66 @@ +# 2026-07-05 22:40 Global Status Log Fix + +## Problem + +In fire_check-only mode, the global status monitor still printed values like: + +```text +person:1 | temp:0.0°C | threshold value:70.0°C +``` + +Those fields belong to fire_leave/thermometry logic. When thermometry startup fails and fire_leave is disabled, these values are not active runtime measurements and should not be shown as real current data. + +## Change + +- Added latest fire_check result state in `main.go`: + - `FireCheckHasResult` + - `FireCheckLastNumber` +- Updated the fire_check result branch to record the latest detected fire target count. +- Updated `printGlobalDeviceStatus()`: + - prints `fire:` for fire_check target count; + - prints `fire:null` before any fire_check result is received; + - prints `person:null`, `temp:null`, and `threshold value:null` when fire_leave is disabled; + - keeps `person/temp/threshold` only when fire_leave is active; + - adds `fire_candidate`, `fire_last_seen_age`, and `fire_leave_enabled`. + +## Expected Logs + +Before any fire_check inference result is received: + +```text +[1] DeviceUUID=xxx | fire:null | fire_candidate:false | fire_last_seen_age:null | fire_leave_enabled:false | person:null | temp:null | threshold value:null | cooling time:0s +``` + +After fire_check reports one fire target: + +```text +[1] DeviceUUID=xxx | fire:1 | fire_candidate:true | fire_last_seen_age:500ms | fire_leave_enabled:false | person:null | temp:null | threshold value:null | cooling time:0s +``` + +If fire_check has reported a result but no fire target is present: + +```text +[1] DeviceUUID=xxx | fire:0 | fire_candidate:false | fire_last_seen_age:null | fire_leave_enabled:false | person:null | temp:null | threshold value:null | cooling time:0s +``` + +## Impact + +- Does not change fire_check alarm upload/report logic. +- Does not change heartbeat JSON format. +- Does not change inference startup arguments. +- Only changes runtime state tracking and global monitor log output. +- fire_leave logs remain available when fire_leave and thermometry are active. + +## Verification + +```text +gofmt -w main.go +go test ./... +go build -o bin/fireleave_tool.new . +``` + +The new binary was installed as: + +```text +bin/fireleave_tool +``` diff --git a/docs/2026-07-05_fix_and_build_changelog.md b/docs/2026-07-05_fix_and_build_changelog.md new file mode 100644 index 0000000..f3d5b8a --- /dev/null +++ b/docs/2026-07-05_fix_and_build_changelog.md @@ -0,0 +1,241 @@ +# 2026-07-05 修改日志 + +本文记录本次代码审查后实际落地的修复、影响范围、验证结果和构建产物。 + +## 1. 本次目标 + +修复代码审查中确认的 1、2、4、5、8 号问题,并重新构建新的程序到 `bin/fireleave_tool`。 + +涉及的问题包括: + +- `fire_check` 报警上传/上报失败时仍被当作成功处理 +- 心跳超时重启会扩大影响,可能同时重启两个推理进程 +- 视频时间戳解析和合并异常处理不可靠 +- WebSocket 重连可能并发启动多个重连循环 +- `copyFile` 超时返回后后台 goroutine 仍可能继续写文件 + +## 2. 修改文件 + +主要修改: + +- `main.go` +- `connect/heartbeat.go` +- `connect/ws_channel.go` +- `reporter/reporter.go` +- `video_server/video_merge.go` +- `bin/fireleave_tool` + +构建/环境相关: + +- 本机安装了 Go 编译器 +- 当前构建使用 `go1.26.4` +- 新二进制目标为 `linux/arm64` + +## 3. 详细修改内容 + +### 3.1 `fire_check` 上报成功语义修复 + +原问题: + +`fire_check` 报警触发后,上传视频和 WebSocket 上报在异步流程中执行。调用方无法知道最终是否真正上传成功、是否真正写入 WebSocket,因此主流程可能提前清理 `FireCheckCandidate` 状态。这样会造成“报警没有报上去,但本地状态认为已经处理完成”的风险。 + +修改后: + +- `reporter.UploadVideoAndReportFireCheck()` 改为同步执行。 +- 视频上传失败会返回 error。 +- WebSocket 未初始化或未连接会返回 error。 +- 事件上报失败会返回 error。 +- 数据快照上报失败会返回 error。 +- 只有视频上传、事件上报、快照上报都成功后才清理合并文件并返回成功。 +- `reporter.TriggerFireCheckAlarm()` 改为返回 `bool`: + - `true` 表示报警已完整处理成功 + - `false` 表示合并、上传、上报或并发锁获取失败 +- `main.go` 中只有当 `TriggerFireCheckAlarm()` 返回 `true` 时才清空 `FireCheckCandidate`、`FireCheckFirstSeen`、`FireCheckLastSeen`。 + +影响: + +- `fire_check` 报警处理会比原来更严格,调用路径会等待上传和 WebSocket 写入完成。 +- 如果平台或网络异常,本地候选状态不会被误清理,后续仍有机会重新触发。 +- 失败场景下日志可能增加,但这是为了避免报警丢失。 + +### 3.2 `fire_check` 连续检测状态修复 + +原问题: + +旧逻辑在收到推理结果的分支中立即判断持续时间并触发上报。如果上报失败,状态仍可能被重置。同时检测结果和上报处理耦合较重。 + +修改后: + +- `deviceAlarmState` 中使用: + - `FireCheckCandidate` + - `FireCheckFirstSeen` + - `FireCheckLastSeen` +- 收到火焰推理结果时只更新候选状态和最近一次看到火焰的时间。 +- 在 `monitorTicker` 分支中统一判断是否达到 `fire_check` 持续时间。 +- 增加 `fireCheckLastSeenGrace`,避免检测帧之间的短暂间隔导致候选状态过早失效。 + +影响: + +- 火焰检测持续时间判断更稳定。 +- 上报是否成功会影响候选状态是否清理。 +- 短暂丢帧不一定立即打断候选状态。 + +### 3.3 心跳重启范围修复 + +原问题: + +`fire_leave` 或 `fire_check` 某一个推理进程心跳超时时,旧逻辑会调用共享重启流程,可能把两个推理进程都重启。这样会扩大影响面,也可能造成重复重启。 + +修改后: + +- 新增 `connect.RestartInferenceProcess(deviceData, processUUID)`。 +- 该函数只重启指定的 `processUUID`: + - 主模型:`deviceUUID` + - 火焰模型:`deviceUUID_fire_check` +- `connect.StartInferenceProcess()` 保留兼容行为,仍可重启两路推理。 +- `main.go` 心跳监控改为: + - `fire_leave` 超时只重启 `deviceUUID` + - `fire_check` 超时只重启 `deviceUUID_fire_check` +- 重启后只刷新对应进程的心跳时间。 + +影响: + +- 单路推理异常不会连带影响另一路推理。 +- 保留旧的 `StartInferenceProcess()`,避免已有调用点失效。 +- 重启逻辑仍使用 `connect.GlobalPortManager` 获取原端口。 + +### 3.4 视频合并异常处理修复 + +原问题: + +`video_server.parseSimpleTimestamp()` 直接按固定下标切片,遇到异常文件名可能 panic。`MergeVideo()` 虽然有 recover,但旧逻辑存在 panic 后没有明确错误返回的风险。 + +修改后: + +- `MergeVideo()` 改为命名返回值: + - `mergedPath string` + - `err error` +- `recover` 捕获 panic 后会设置: + - `mergedPath = ""` + - `err = fmt.Errorf("merge video panic: %v", recovered)` +- `parseSimpleTimestamp()` 增加文件名长度校验。 +- 年、月、日、时、分、秒解析失败时都会返回明确 error。 +- `getTimestampFromPath()` 改为返回 `(int64, error)`。 +- 排序和时间戳匹配逻辑会处理解析失败,必要时回退到文件名排序。 + +影响: + +- 异常文件名不会导致程序崩溃。 +- 视频合并失败会更明确地返回错误,调用方可感知失败。 +- 正常命名的 mp4 文件处理逻辑不变。 + +### 3.5 WebSocket 重连并发修复 + +原问题: + +读循环、心跳循环、发送失败都可能触发 `reconnect()`。如果多个错误同时发生,可能启动多个重连 goroutine,并在重连成功后重复启动心跳循环。 + +修改后: + +- `WSClient` 增加 `reconnecting bool`。 +- 新增: + - `startReconnect()` + - `startReconnectLocked()` +- 所有触发重连的地方都通过 `startReconnect` 系列方法进入。 +- 同一时间只允许一个重连循环运行。 +- 重连完成后重置 `reconnecting`。 +- 重连成功后只恢复 `readLoop()`,不再重复启动新的 `heartbeatLoop()`。 +- 重新订阅时先复制 `subscribers` map,再遍历订阅,降低并发读写风险。 +- `RegisterService()` 和 `SubscribeTopic()` 使用本地 `client := GetWSClient()`,减少直接访问全局变量的竞态窗口。 + +影响: + +- 断线时重连行为更可控。 +- 减少 goroutine 堆积和重复 heartbeat 的风险。 +- WebSocket 正常收发路径不变。 + +### 3.6 `copyFile` 超时写入修复 + +原问题: + +旧 `copyFile` 使用 goroutine 执行复制,外层通过 context 超时返回。超时后后台 goroutine 仍可能继续写目标文件,导致函数已经失败但文件仍在被写入,甚至留下半成品。 + +修改后: + +- `copyFile` 改为同步分块复制。 +- 每次循环检查 30 秒 deadline。 +- 读取和写入使用 1MB buffer。 +- 超时、读失败、写失败、短写或 sync 失败时都会删除目标文件。 +- 函数返回后不再有后台 goroutine 继续写文件。 + +影响: + +- 超时后不会继续写目标文件。 +- 失败时不保留不完整目标文件。 +- 大文件复制仍可能耗时,但行为更确定。 + +## 4. 构建与验证 + +已执行: + +```text +gofmt -w main.go connect/heartbeat.go connect/ws_channel.go reporter/reporter.go video_server/video_merge.go +go test ./... +go build -o bin/fireleave_tool.new . +``` + +验证结果: + +```text +go test ./... 通过 +所有包均为 [no test files] +``` + +新构建产物: + +```text +bin/fireleave_tool +``` + +构建信息: + +```text +go version: go1.26.4 +GOOS: linux +GOARCH: arm64 +CGO_ENABLED: 0 +binary size: 10011779 bytes +``` + +构建完成后已清理 `bin` 目录旧产物,当前 `bin` 目录只保留: + +```text +bin/fireleave_tool +``` + +## 5. 兼容性与风险说明 + +### 保持兼容的部分 + +- `connect.StartInferenceProcess()` 保留,已有调用仍可使用。 +- `fire_leave` 报警入口仍保持异步上报行为。 +- WebSocket 消息结构和 topic 订阅逻辑没有改协议。 +- 视频文件命名规则未改变。 + +### 行为变化 + +- `fire_check` 上报变为同步确认成功。 +- `fire_check` 上报失败时不会清空候选报警状态。 +- 心跳超时只重启异常的单个推理进程。 +- WebSocket 同一时间只允许一个重连流程。 +- 文件复制超时会删除不完整目标文件。 + +### 仍需关注 + +- 当前部分历史中文注释已经存在编码损坏,后续维护时建议逐步替换成清晰 UTF-8 注释。 +- `reporter` 中仍有旧的 event cycle 相关逻辑,当前未完全重构,只做了本次问题相关的收敛。 +- 如果现场网络长期异常,`fire_check` 候选状态可能持续重试,需要结合日志确认是否需要增加队列、退避或最大重试策略。 + +## 6. 本次结论 + +本次修复重点是避免误判成功、缩小重启影响、减少并发重连和后台写文件风险。新的 `bin/fireleave_tool` 已基于修复后的代码构建完成,可用于后续部署验证。 diff --git a/httpreader/httpreader.go b/httpreader/httpreader.go index 1d95b00..e35a789 100644 --- a/httpreader/httpreader.go +++ b/httpreader/httpreader.go @@ -2,11 +2,9 @@ package httpreader import ( "FireLeave_tool/connect" - "bytes" "context" "encoding/json" "fmt" - "io" "net/http" "os" "sync" @@ -17,14 +15,31 @@ import ( // InferenceResult 推理结果 type InferenceResult struct { - Serial string `json:"serial"` - At int64 `json:"at"` - Type int `json:"type"` - Params []struct { - ClassIdx int `json:"class_idx"` - Name string `json:"name"` - Number int `json:"number"` - } `json:"params"` + MessageType string `json:"message_type"` + Serial string `json:"serial"` + At int64 `json:"at"` + Type int `json:"type"` + Params []InferenceParam `json:"params"` +} + +type InferenceParam struct { + ClassIdx int `json:"class_idx"` + Name string `json:"name"` + Number int `json:"number"` +} + +type HeartbeatMessage struct { + MessageType string `json:"message_type"` + SchemaVersion int `json:"schema_version"` + Serial string `json:"serial"` + ProcessUUID string `json:"process_uuid"` + Timestamp int64 `json:"timestamp"` + Status string `json:"status"` +} + +type inferenceStats struct { + windowStart time.Time + count int } // InferenceServer 推理数据接收服务器 @@ -35,6 +50,8 @@ type InferenceServer struct { dataChan chan InferenceResult mu sync.RWMutex running bool + statsMu sync.Mutex + stats inferenceStats } // NewInferenceServer 创建推理数据接收服务器 @@ -58,6 +75,7 @@ func (is *InferenceServer) Start() error { mux := http.NewServeMux() mux.HandleFunc("/video/post", is.handleVideoPost) + mux.HandleFunc("/heartbeat", is.handleHeartbeat) mux.HandleFunc("/health", is.handleHealthCheck) is.server = &http.Server{ @@ -91,41 +109,23 @@ func (is *InferenceServer) handleVideoPost(w http.ResponseWriter, r *http.Reques return } - // 读取原始 body - bodyBytes, err := io.ReadAll(r.Body) - if err != nil { - logger.Logger.Printf("Read request body failed: %v", err) - http.Error(w, "Bad request", http.StatusBadRequest) - return - } - - // 重新设置 body 用于解析 - r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) - var result InferenceResult if err := json.NewDecoder(r.Body).Decode(&result); err != nil { - logger.Logger.Printf("Parse JSON failed: %v", err) + logger.Logger.Printf("Parse inference_result JSON failed: %v (DeviceUUID=%s)", err, is.deviceUUID) http.Error(w, `{"error":"Invalid JSON"}`, http.StatusBadRequest) return } - // 验证必要字段 - if result.Serial == "" { - logger.Logger.Printf("Data validation failed: Serial is empty") - http.Error(w, `{"error":"Missing serial field"}`, http.StatusBadRequest) + if result.MessageType != "inference_result" { + logger.Logger.Printf("Inference result validation failed: message_type=%q (DeviceUUID=%s)", result.MessageType, is.deviceUUID) + http.Error(w, `{"error":"Invalid message_type"}`, http.StatusBadRequest) return } - now := time.Now().Format("2006/01/02 15:04:05.000") - logger.Logger.Printf("推理心跳收到 | 时间: %s | UUID: %s | Serial: %s | 人数量: %d", - now, is.deviceUUID, result.Serial, getPersonCount(result)) - // 这一行就是你缺失的救命稻草! - connect.UpdateInferenceHeartbeat(is.deviceUUID) - - // 额外写一份永不丢失的独立心跳日志 - if f, err := os.OpenFile("/data/heartbeat.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil { - defer f.Close() - fmt.Fprintf(f, "[%s] HEARTBEAT OK | %s | person=%d\n", now, is.deviceUUID, getPersonCount(result)) + if result.Serial == "" { + logger.Logger.Printf("Inference result validation failed: serial is empty (DeviceUUID=%s)", is.deviceUUID) + http.Error(w, `{"error":"Missing serial field"}`, http.StatusBadRequest) + return } // 发送到处理通道 @@ -135,13 +135,89 @@ func (is *InferenceServer) handleVideoPost(w http.ResponseWriter, r *http.Reques w.WriteHeader(http.StatusOK) w.Write([]byte(`{"status":"ok", "message":"data received"}`)) // 使用 startTime 记录处理时间 - logger.Logger.Printf("Inference data processed in %v", time.Since(startTime)) + is.recordInferenceProcessed(time.Since(startTime)) default: logger.Logger.Printf("Data channel full, discard data") http.Error(w, `{"error":"Channel full"}`, http.StatusServiceUnavailable) } } +func (is *InferenceServer) recordInferenceProcessed(processCost time.Duration) { + is.statsMu.Lock() + defer is.statsMu.Unlock() + + now := time.Now() + if is.stats.windowStart.IsZero() { + is.stats.windowStart = now + } + is.stats.count++ + + if now.Sub(is.stats.windowStart) >= 10*time.Second { + logger.Logger.Printf("Inference data processed summary | UUID=%s | count=%d | window=%v | last_cost=%v", + is.deviceUUID, is.stats.count, now.Sub(is.stats.windowStart).Round(time.Millisecond), processCost) + is.stats.windowStart = now + is.stats.count = 0 + } +} + +func (is *InferenceServer) handleHeartbeat(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + var heartbeat HeartbeatMessage + if err := json.NewDecoder(r.Body).Decode(&heartbeat); err != nil { + logger.Logger.Printf("Parse heartbeat JSON failed: %v (DeviceUUID=%s)", err, is.deviceUUID) + http.Error(w, `{"error":"Invalid JSON"}`, http.StatusBadRequest) + return + } + + if heartbeat.MessageType != "heartbeat" { + logger.Logger.Printf("Heartbeat validation failed: message_type=%q (DeviceUUID=%s)", heartbeat.MessageType, is.deviceUUID) + http.Error(w, `{"error":"Invalid message_type"}`, http.StatusBadRequest) + return + } + + if heartbeat.SchemaVersion != 1 { + logger.Logger.Printf("Heartbeat validation failed: schema_version=%d (DeviceUUID=%s)", heartbeat.SchemaVersion, is.deviceUUID) + http.Error(w, `{"error":"Unsupported schema_version"}`, http.StatusBadRequest) + return + } + + if heartbeat.Serial == "" { + logger.Logger.Printf("Heartbeat validation failed: serial is empty (DeviceUUID=%s)", is.deviceUUID) + http.Error(w, `{"error":"Missing serial field"}`, http.StatusBadRequest) + return + } + + if heartbeat.ProcessUUID == "" { + logger.Logger.Printf("Heartbeat validation failed: process_uuid is empty (DeviceUUID=%s)", is.deviceUUID) + http.Error(w, `{"error":"Missing process_uuid field"}`, http.StatusBadRequest) + return + } + + if heartbeat.ProcessUUID != is.deviceUUID { + logger.Logger.Printf("Heartbeat process_uuid mismatch: got=%s expected=%s", heartbeat.ProcessUUID, is.deviceUUID) + http.Error(w, `{"error":"process_uuid mismatch"}`, http.StatusBadRequest) + return + } + + connect.UpdateInferenceHeartbeat(is.deviceUUID) + now := time.Now().Format("2006/01/02 15:04:05.000") + logger.Logger.Printf("Inference heartbeat received | Time: %s | UUID: %s | Serial: %s | Status: %s", + now, is.deviceUUID, heartbeat.Serial, heartbeat.Status) + + if f, err := os.OpenFile("/data/heartbeat.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644); err == nil { + defer f.Close() + fmt.Fprintf(f, "[%s] HEARTBEAT OK | %s | serial=%s | status=%s | ts=%d\n", now, is.deviceUUID, heartbeat.Serial, heartbeat.Status, heartbeat.Timestamp) + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"status":"ok","message":"heartbeat received"}`)) +} + func (is *InferenceServer) handleHealthCheck(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") @@ -178,11 +254,3 @@ func (s *InferenceServer) StopWithContext(ctx context.Context) { logger.Logger.Printf("The HTTP server has been shut down (DeviceUUID=%s)", s.deviceUUID) } } -func getPersonCount(result InferenceResult) int { - for _, p := range result.Params { - if p.ClassIdx == 1 { - return p.Number - } - } - return 0 -} diff --git a/main.go b/main.go index 000aa08..6aeb970 100644 --- a/main.go +++ b/main.go @@ -27,32 +27,33 @@ import ( ) type deviceAlarmState struct { - InCondition bool - StartTime int64 - AlarmPauseUntil int64 - FireLeaveForbidUntil int64 - // fire_check火焰检测状态 - FireCheckInCondition bool - FireCheckStartTime int64 + InCondition bool + StartTime int64 + AlarmPauseUntil int64 + FireLeaveForbidUntil int64 + FireLeaveDisabled bool + FireCheckCandidate bool + FireCheckFirstSeen time.Time + FireCheckLastSeen time.Time + FireCheckHasResult bool + FireCheckLastNumber int + FireCheckWaitingClear bool } -// 全局 map:每个设备独立状态 var deviceStates sync.Map // map[string]*deviceAlarmState var mergingAlarms sync.Map -// 全局上报管理器 var GlobalReporter = reporter.NewReporter() const defaultFireLeaveForbidDuration = 15 * time.Minute +const fireCheckLastSeenGrace = time.Second 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), @@ -132,7 +133,6 @@ func restoreFireLeaveForbidWindow(deviceUUID string) { 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() @@ -140,7 +140,6 @@ func (dm *DeviceManager) AddDevice(deviceData connect.DeviceData) { logger.Logger.Printf("add equipments: DeviceUUID=%s", deviceData.DeviceUUID) } -// StopDevice 停止设备 func (dm *DeviceManager) StopDevice(deviceUUID string) { dm.mu.Lock() defer dm.mu.Unlock() @@ -150,7 +149,6 @@ func (dm *DeviceManager) StopDevice(deviceUUID string) { } } -// GetDevice 获取设备 func (dm *DeviceManager) GetDevice(deviceUUID string) (*connect.DeviceData, bool) { dm.mu.Lock() defer dm.mu.Unlock() @@ -158,10 +156,8 @@ func (dm *DeviceManager) GetDevice(deviceUUID string) (*connect.DeviceData, bool 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) @@ -172,7 +168,6 @@ func waitForHTTPService(port int, timeout time.Duration) bool { 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 { @@ -183,7 +178,6 @@ func waitForHTTPService(port int, timeout time.Duration) bool { } conn.Close() - // 2. 再检查HTTP服务是否真正就绪 client := &http.Client{Timeout: 3 * time.Second} resp, err := client.Get(fmt.Sprintf("http://%s/health", address)) if err == nil { @@ -204,7 +198,6 @@ func waitForHTTPService(port int, timeout time.Duration) bool { 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) @@ -228,11 +221,10 @@ func waitForTCPService(port int, timeout time.Duration) bool { 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) // 增加等待间隔 + 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) @@ -257,22 +249,28 @@ func getIntFromNumber(num json.Number, def int) int { return def } -// processDevice 处理单个设备 +func getFireCheckDetectionDuration(deviceData connect.DeviceData) time.Duration { + detectionSec := 2 + if deviceData.FireCheckParam.DetectionTime != "" { + if sec, err := strconv.Atoi(deviceData.FireCheckParam.DetectionTime); err == nil && sec >= 0 { + detectionSec = sec + } + } + return time.Duration(detectionSec) * time.Second +} + 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) @@ -280,7 +278,6 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { } 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) @@ -290,25 +287,21 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { 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 @@ -321,16 +314,12 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { 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, @@ -350,12 +339,12 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { return } - // 保存fire_leave进程对象到全局 map + // 婵犵數濮烽弫鎼佸磿閹寸姴绶ら柦妯侯棦濞差亝鏅滈柣鎰靛墮鎼村﹪姊虹粙璺ㄧ伇闁稿鍋ゅ畷鎴﹀Χ婢跺鍘撴繛鎾村嚬閸ㄥ崬鈻嶅▔涔猒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推理程序 + fireCheckConfidence := connect.GetFireCheckConfidence(deviceData) inferenceCmdFireCheck := exec.Command( "./yolov5", "-s", deviceData.CameraRTSP, @@ -363,8 +352,7 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { "-c", fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), "-p", fmt.Sprintf("%d", portFireCheck), "-w", fireCheckDetectAreaStr, - "-r", fmt.Sprintf("%d", deviceData.Confidence), - "-R", + "-r", fmt.Sprintf("%d", fireCheckConfidence), ) inferenceCmdFireCheck.Stdout = os.Stdout inferenceCmdFireCheck.Stderr = os.Stderr @@ -376,19 +364,16 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { return } - // 保存fire_check进程对象到全局 map + // 婵犵數濮烽弫鎼佸磿閹寸姴绶ら柦妯侯棦濞差亝鏅滈柣鎰靛墮鎼村﹪姊虹粙璺ㄧ伇闁稿鍋ゅ畷鎴﹀Χ婢跺鍘撴繛鎾村嚬閸ㄥ崬鈻嶅▔涔猒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 @@ -399,7 +384,6 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { return } - // 检查fire_check推理程序 if inferenceCmdFireCheck.Process == nil { logger.Logger.Printf("Fire_check inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID) return @@ -420,11 +404,36 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { logger.Logger.Printf("Failed to record fire_check process information: %v", err) } + fireLeaveActive := true + fireCheckActive := true + temperatureActive := false + + stopFireLeaveOnly := func(reason string) { + if !fireLeaveActive { + return + } + logger.Logger.Printf("Stop fire_leave only: %s (DeviceUUID=%s)", reason, deviceData.DeviceUUID) + 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) + } + _ = inferenceCmdFireLeave.Wait() + } + connect.InferenceProcesses.Delete(deviceData.DeviceUUID) + inferenceServerFireLeave.StopWithContext(context.Background()) + connect.GlobalPortManager.ReleasePort(deviceData.DeviceUUID) + state := getDeviceState(deviceData.DeviceUUID) + state.FireLeaveDisabled = true + state.InCondition = false + fireLeaveActive = false + } + defer func() { logger.Logger.Printf("Start to stop inference programs (DeviceUUID=%s)", deviceData.DeviceUUID) - // 停止fire_leave推理程序 - if inferenceCmdFireLeave.Process != nil { + if fireLeaveActive && 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 { @@ -432,8 +441,7 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { } } - // 停止fire_check推理程序 - if inferenceCmdFireCheck.Process != nil { + if fireCheckActive && 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 { @@ -442,9 +450,6 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { } }() - // 启动温度程序 - // 在 processDevice 函数中修改温度程序启动部分 - // 启动温度程序 tempPort := portFireLeave + 1000 tempCmdArgs := []string{ "-s", deviceData.CameraRTSP, @@ -460,65 +465,64 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { tempCmd.Stderr = os.Stderr logger.Logger.Printf("Start the temperature program command: %v", tempCmd.Args) + var tempClient *tcpreader.TCPClient if err := tempCmd.Start(); err != nil { logger.Logger.Printf("The temperature program failed to start (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) - return - } + stopFireLeaveOnly("temperature program failed to start") + } else { + time.Sleep(3 * time.Second) - // 增强进程检查 - 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) + stopFireLeaveOnly("temperature program pid is empty") + } else 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) + stopFireLeaveOnly("temperature program process check failed") + } else { + logger.Logger.Printf("The temperature program has been successfully started, PID=%d (DeviceUUID=%s)", tempCmd.Process.Pid, deviceData.DeviceUUID) - 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 - } + logger.Logger.Printf("Wait for the temperature program to be initialized... (DeviceUUID=%s)", deviceData.DeviceUUID) + time.Sleep(2 * time.Second) - // 检查进程是否真的在运行 - 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("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) - logger.Logger.Printf("The temperature program has been successfully started,PID: %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) + 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) + } + _ = tempCmd.Process.Kill() + _ = tempCmd.Wait() + } + stopFireLeaveOnly("temperature TCP server startup timeout") } 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) + 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) + if tempCmd.Process != nil { + _ = tempCmd.Process.Kill() + _ = tempCmd.Wait() + } + stopFireLeaveOnly("temperature TCP client creation failed") + } else { + defer tempClient.Close() + temperatureActive = true + getDeviceState(deviceData.DeviceUUID).FireLeaveDisabled = false + logger.Logger.Printf("The TCP client has been successfully created (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, tempPort) + } } } - 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 @@ -526,24 +530,31 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { monitorTicker := time.NewTicker(2 * time.Second) defer monitorTicker.Stop() + fireLeaveDataChan := inferenceServerFireLeave.GetDataChan() + if !fireLeaveActive { + fireLeaveDataChan = nil + } + fireCheckDataChan := inferenceServerFireCheck.GetDataChan() + for { select { case <-deviceCtx.Done(): logger.Logger.Printf("Equipment processing has been cancelled: DeviceUUID=%s", deviceData.DeviceUUID) - // 强制停止所有子进程 - if inferenceCmdFireLeave.Process != nil { + if fireLeaveActive && inferenceCmdFireLeave.Process != nil { inferenceCmdFireLeave.Process.Kill() } - if inferenceCmdFireCheck.Process != nil { + if fireCheckActive && inferenceCmdFireCheck.Process != nil { inferenceCmdFireCheck.Process.Kill() } if tempCmd.Process != nil { tempCmd.Process.Kill() } - tempClient.Close() + if tempClient != nil { + tempClient.Close() + } return - case inferenceResult, ok := <-inferenceServerFireLeave.GetDataChan(): + case inferenceResult, ok := <-fireLeaveDataChan: if !ok { logger.Logger.Printf("The fire_leave inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID) return @@ -562,69 +573,96 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { logger.Logger.Printf("Personnel quantity update: %d (DeviceUUID=%s)", currentPersonCount, deviceData.DeviceUUID) } - case fireCheckResult, ok := <-inferenceServerFireCheck.GetDataChan(): + case fireCheckResult, ok := <-fireCheckDataChan: 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 + fireNumber := 0 for _, param := range fireCheckResult.Params { - if param.ClassIdx == 0 && param.Number > 0 { // 假设ClassIdx=0表示火焰 + if param.Number > 0 && (param.Name == "fire" || param.ClassIdx == 0) { fireDetected = true - logger.Logger.Printf("Fire detected! DeviceUUID=%s, Number=%d", deviceData.DeviceUUID, param.Number) + fireNumber = 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 - } - } - + state.FireCheckHasResult = true + state.FireCheckLastNumber = fireNumber 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 + seenAt := time.Now() + state.FireCheckLastSeen = seenAt + if state.FireCheckWaitingClear { + continue } - } else { - // 没有检测到火焰,重置状态 - if state.FireCheckInCondition { - logger.Logger.Printf("Fire detection interrupted: lasted %ds < %ds (DeviceUUID=%s)", now-state.FireCheckStartTime, detectionSec, deviceData.DeviceUUID) - state.FireCheckInCondition = false + if !state.FireCheckCandidate { + state.FireCheckCandidate = true + state.FireCheckFirstSeen = seenAt + logger.Logger.Printf("Fire target detected, start timing: required=%v, number=%d, DeviceUUID=%s", + getFireCheckDetectionDuration(deviceData), fireNumber, deviceData.DeviceUUID) } + elapsed := seenAt.Sub(state.FireCheckFirstSeen) + required := getFireCheckDetectionDuration(deviceData) + if elapsed >= required { + alarmTime := seenAt.Unix() + logger.Logger.Printf("Fire detection candidate confirmed by inference data, triggering alarm: duration=%v, DeviceUUID=%s", + elapsed, deviceData.DeviceUUID) + state.FireCheckCandidate = false + state.FireCheckWaitingClear = true + state.FireCheckFirstSeen = time.Time{} + go GlobalReporter.TriggerFireCheckAlarm(deviceData, alarmTime) + } + } else if state.FireCheckWaitingClear { + state.FireCheckWaitingClear = false + state.FireCheckCandidate = false + state.FireCheckFirstSeen = time.Time{} + state.FireCheckLastSeen = time.Time{} + logger.Logger.Printf("Fire target cleared, ready for next fire_check timing: DeviceUUID=%s", deviceData.DeviceUUID) } case <-monitorTicker.C: now := time.Now().Unix() - // === 获取当前设备独立状态 === + // === 闂傚倸鍊搁崐椋庣矆娓氣偓瀹曘儳鈧綆鍠栫壕鍧楁煙閹増顥夐幖鏉戯躬閺屻倝鎳濋幍顔肩墯婵炲瓨绮岀紞濠囧蓟濞戙垹唯妞ゆ棁宕甸弳妤佺箾鐎涙鐭婄紓宥咃躬瀵鎮㈤悡搴g暰閻熸粌绉瑰铏綇閵婏絼绨婚梺闈涚墕閹冲繘宕甸崶顒佺厵妞ゆ梹顑欏鎰版煙瀹勭増鍤囬柟顔惧厴瀵爼宕归褎鍨舵穱濠囨倷椤忓嫧鍋撹閳ь剟娼ч惌鍌氱暦濠婂牊鍋勯柣鎾虫捣閻e搫顪冮妶鍛闁绘绮撳顐﹀幢濞戞瑧鍘遍柣蹇曞仜婢т粙鎯屾繝鍕=鐎广儱鎳庨顓熸叏婵犲懏顏犵紒杈ㄥ笒椤粓宕卞▎蹇撔梻鍌欒兌绾爼寮插鍫熷亱闁绘顕ч弰銉╂煃?=== state := getDeviceState(deviceData.DeviceUUID) + if state.FireCheckWaitingClear && + !state.FireCheckLastSeen.IsZero() && + time.Since(state.FireCheckLastSeen) > fireCheckLastSeenGrace { + lastSeenAge := time.Since(state.FireCheckLastSeen) + state.FireCheckWaitingClear = false + state.FireCheckCandidate = false + state.FireCheckFirstSeen = time.Time{} + state.FireCheckLastSeen = time.Time{} + logger.Logger.Printf("Fire target lost, management rearmed fire_check: lastSeenAge=%v, DeviceUUID=%s", + lastSeenAge.Round(time.Millisecond), deviceData.DeviceUUID) + } + if state.FireCheckCandidate { + checkAt := time.Now() + lastSeenAge := checkAt.Sub(state.FireCheckLastSeen) + elapsed := checkAt.Sub(state.FireCheckFirstSeen) + required := getFireCheckDetectionDuration(deviceData) + if lastSeenAge > fireCheckLastSeenGrace { + logger.Logger.Printf("Fire detection candidate expired: lastSeenAge=%v, DeviceUUID=%s", lastSeenAge, deviceData.DeviceUUID) + state.FireCheckCandidate = false + state.FireCheckFirstSeen = time.Time{} + state.FireCheckLastSeen = time.Time{} + } else { + logger.Logger.Printf("Fire target timing: elapsed=%v, required=%v, DeviceUUID=%s", + elapsed.Round(time.Millisecond), required, deviceData.DeviceUUID) + } + } + + // === 闂傚倸鍊峰ù鍥х暦閸偅鍙忛柡澶嬪殮濞差亜鐓涢柛婊€鐒﹂弲顏堟偡濠婂嫬鐏村┑锛勬暬楠炲洭寮剁捄銊モ偓鐐烘偡濠婂喚鍎旈柛鈹惧亾濡炪倖甯婄粈浣该归鈧弻娑㈠煘閹冣拤闂佺懓鍢查幊妯虹暦濠婂牆绠甸柟鐑樻尰濮e矂姊婚崒姘偓鐑芥倿閿旈敮鍋撶粭娑樻噽閻瑩鏌熸潏楣冩闁稿顑夐弻鐔兼倷椤掑倻鐛梺鎸庣箓濡稓寮ч埀顒€鈹戦鏂や緵闁告挻鐩、娆撳川鐎涙ǚ鎷洪梺鍛婄箓鐎氼喛鍊撮梻浣芥〃缁€渚€骞楀鍛灊?== + if !fireLeaveActive || !temperatureActive || tempClient == nil { + if state.InCondition { + state.InCondition = false + } + continue + } - // === 读取温度(防抖)=== tempResult, err := tempClient.ReadTemperatureResult() if err == nil { if math.Abs(tempResult.Tmp-deviceData.Temperature) >= 0.5 { @@ -632,19 +670,17 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { deviceData.Temperature = tempResult.Tmp connect.UpdateDeviceData(deviceData) GlobalDeviceManager.AddDevice(deviceData) - logger.Logger.Printf("Temperature update: %.1f°C → %.1f°C (DeviceUUID=%s)", + logger.Logger.Printf("Temperature update: %.1fC -> %.1fC (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 @@ -667,7 +703,6 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { continue } - // 1. 冷却期检查 if now < state.AlarmPauseUntil { if state.InCondition { logger.Logger.Printf("Alarm in cooldown: %ds remaining (DeviceUUID=%s)", @@ -677,50 +712,44 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) { 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)", + logger.Logger.Printf("Alarm condition met, start timing (%ds required): PersonCount=%d, Temp=%.1fC > %.1fC (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)", + logger.Logger.Printf("[%d] ALARM TRIGGERED (after %ds): PersonCount=%d, Temp=%.1fC > %.1fC (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)", + logger.Logger.Printf("[%d] Start async alarm handling (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)", + logger.Logger.Printf("[%d] Alarm handling panic: %v (DeviceUUID=%s)", time.Now().Unix(), err, deviceData.DeviceUUID) } }() - // 调用报警处理函数 - GlobalReporter.TriggerAlarmWithDelay(deviceData, state.StartTime, alarmCooldown) - logger.Logger.Printf("[%d] 报警处理完成 (DeviceUUID=%s, 耗时:%.3fs)", + // 闂傚倸鍊峰ù鍥х暦閸偅鍙忛柟缁㈠櫘閺佸嫰鏌涘☉娆愮稇闁汇値鍠栭湁闁稿繐鍚嬬紞鎴︽煛鐎b晝绐旈柡灞炬礋瀹曠厧鈹戦幇顓夛妇绱撴担鍝勵€岄柛锝忕秮瀵寮撮悢椋庣獮濠碘槅鍨抽崢褏鏁妷鈺傗拺闁告稑锕ラ悡銉╂煙閸愭煡鍙勯柍銉閹瑰嫰濡搁敃鈧壕顖炴⒑閸涘﹦绠撻悗姘舵敱鐎靛ジ鍩€椤掑嫭鈷掑ù锝囩摂閸ゅ啴鏌涢悩宕囧⒌闁轰礁鍟撮、鏃堝礋椤撶喐顔曟繝鐢靛仜濡瑩骞愭繝姘惞鐎广儱顦伴悡蹇擃熆閼哥數娲存俊缁㈠枟閵? GlobalReporter.TriggerAlarmWithDelay(deviceData, state.StartTime, alarmCooldown) + logger.Logger.Printf("[%d] Alarm handling completed (DeviceUUID=%s, elapsed=%.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)", + logger.Logger.Printf("[%d] Alarm cooldown set to %d seconds (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) + logger.Logger.Printf("[%d] Update device data (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID) connect.UpdateDeviceData(deviceDataCopy) - logger.Logger.Printf("[%d] 设备数据更新完成 (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID) + logger.Logger.Printf("[%d] Device data update completed (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID) } } else { if state.InCondition { @@ -742,10 +771,8 @@ func waitForShutdown(ctx context.Context, cancel context.CancelFunc, wg *sync.Wa case sig := <-stop: logger.Logger.Printf("Received stop signal: %v, start cleaning...", sig) - // 立即取消上下文,通知所有goroutine停止 - cancel() + // 缂傚倸鍊搁崐鎼佸磹閻戣姤鍊块柨鏇炲€搁拑鐔兼煏婵炵偓娅撻柡浣稿閺屾稑鈽夐崡鐐茬闂佸搫妫庨崐婵嬪蓟濞戙垹鐒洪柛鎰典簴濡插牓姊哄ú璇插箺妞ゃ劌锕濠氬灳閹颁礁鎮戦梺鍛婂姂閸斿矂鈥栫€n喗鐓曢柣鏃堟敱閸熺偟绱掔紒妯兼创妤犵偛顑夐幃娆撳级閹寸媭鏆¢梻鍌欑劍鐎笛兠洪埡鍐ㄧ筏閻犳亽鍔岄崹婵囥亜閹惧崬鐏╃€瑰憡绻堥弻鈩冨緞鐎n亞浠稿銈冨劜缁诲牆顫忓ú顏勭闁绘劖褰冩慨锕€顪冮妶鍐ㄥ姕闁圭澧介崚鎺楀醇閻旈绐為梺褰掑亰閸樻悂骞忛崫鍕垫富闁靛牆妫楁慨褏绱掗弻銉х暫闁诡垰鍟撮獮瀣晜閻e苯骞楅梻浣烘嚀閻忔繄鈧凹鍠氬褔鍩€椤掆偓铻栭柣姗€娼ф禒婊呯磼婢跺﹦绉虹€规洘妞介崺鈧い鎺嶉檷娴滄粓鏌熼悜妯虹仴妞ゅ繆鏅犻幃妤€顫濋澶嬓﹂梺瀹狀潐閸ㄨ泛顕ラ崟顓濇勃闁规儳鎳愬绲﹐utine闂傚倸鍊搁崐鐑芥嚄閸洍鈧箓宕奸妷顔芥櫈闂佺硶鍓濋悷銉╁垂濠靛牃鍋撻獮鍨姎妞わ缚绮欏? cancel() - // 等待所有goroutine退出 done := make(chan struct{}) go func() { wg.Wait() @@ -757,7 +784,6 @@ func waitForShutdown(ctx context.Context, cancel context.CancelFunc, wg *sync.Wa 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) @@ -812,23 +838,22 @@ register: 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 + // 闂傚倸鍊搁崐椋庣矆娓氣偓楠炴牠顢曢敃鈧壕褰掓煟閻旂厧浜伴柣鏂挎閹便劌顪冪拠韫闁荤姳璀﹂崹鍫曞蓟濞戙垹绠涢梻鍫熺⊕閻忓秹姊洪崫鍕闁告挻宀搁獮鍫ュΩ閵夊海鍠撻幏鐘诲箵閹烘繃娴囬梻鍌欑閹诧繝鎳濇ィ鍐ㄧ9闁割煈鍣鏍р攽閻樺疇澹樼紒鈧崘顔界厪濠电姴绻掗悾杈ㄧ箾閸涱偄鈧洟鍩為幋锔藉亹妞ゆ劦婢€婢规洖鈹戦悙瀛樼稇闁告艾顑呴…鍥р枎閹惧磭顔夐梺鍓插亝濞叉﹢鎮″☉姗嗙唵閻犺桨璀﹂崕蹇斻亜閺囩喓鐭掗柡灞剧洴閹垽宕崟顏咁潟闂備浇顕栭崰鏍礊婵犲洤鏄ラ柍?topic if err := connect.SubscribeTopic("/dhlr/alert", connect.HandleBuzzerAlert); err != nil { - logger.Logger.Printf("订阅告警topic失败 (/dhlr/alert): %v", err) + logger.Logger.Printf("Subscribe alert topic failed (/dhlr/alert): %v", err) } else { - logger.Logger.Printf("成功订阅本地告警topic: /dhlr/alert") + logger.Logger.Printf("Subscribed local alert topic: /dhlr/alert") } - // 新增:订阅禁止时间 topic + // 闂傚倸鍊搁崐椋庣矆娓氣偓楠炴牠顢曢敃鈧壕褰掓煟閻旂厧浜伴柣鏂挎閹便劌顪冪拠韫闁荤姳璀﹂崹鍫曞蓟濞戙垹绠涢梻鍫熺⊕閻忓秹姊洪崫鍕闁告挻宀搁獮鍫ュΩ閵夊海鍠撻幏鐘诲箵閹烘繃娴囬梻鍌欑閹诧繝鎳濇ィ鍐ㄧ9闁割煈鍣鏍р攽閻樺疇澹樼紒鈧崘顔界厪濠电姴绻掗悾杈ㄧ箾閸涘洤鍠氬〒濠氭煏閸繂鈧懓鈻嶉弮鍌滅<闁绘ê鍟块悘鎾煥濠靛牆浠卞┑顔瑰亾闂侀潧鐗嗛幏瀣焽婵犲洦鈷戦柡鍌樺劜濞呭懘鏌涢悩瀹犲閸楄鲸銇勯弽顐沪闁?topic if err := connect.SubscribeTopic("/dhlr/forbid_time", handleForbidTimeMessage); err != nil { - logger.Logger.Printf("订阅禁止时间topic失败 (/dhlr/forbid_time): %v", err) + logger.Logger.Printf("Subscribe forbid time topic failed (/dhlr/forbid_time): %v", err) } else { - logger.Logger.Printf("成功订阅禁止时间topic: /dhlr/forbid_time") + logger.Logger.Printf("Subscribed forbid time topic: /dhlr/forbid_time") } } @@ -868,12 +893,10 @@ func main() { 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) } @@ -920,14 +943,12 @@ func main() { 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 { @@ -946,23 +967,20 @@ func main() { 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) + state := getDeviceState(uuid) + if !state.FireLeaveDisabled && connect.ShouldRestartInference(uuid) { + logger.Logger.Printf("fire_leave inference heartbeat timeout (>90s), restarting: DeviceUUID=%s", uuid) + if err := connect.RestartInferenceProcess(device, uuid); err != nil { + logger.Logger.Printf("fire_leave inference restart failed: DeviceUUID=%s, err=%v", uuid, err) + } } - // 检查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) + logger.Logger.Printf("fire_check inference heartbeat timeout (>90s), restarting: DeviceUUID=%s", uuid) + if err := connect.RestartInferenceProcess(device, fireCheckUUID); err != nil { + logger.Logger.Printf("fire_check inference restart failed: DeviceUUID=%s, err=%v", uuid, err) + } } return true }) @@ -992,19 +1010,43 @@ func printGlobalDeviceStatus() { 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", + personValue := "null" + tempValue := "null" + thresholdValue := "null" + if !state.FireLeaveDisabled { + personValue = fmt.Sprintf("%d", data.PersonCount) + tempValue = fmt.Sprintf("%.1fC", data.Temperature) + thresholdValue = fmt.Sprintf("%.1fC", data.TemperatureThreshold) + } + + fireValue := "null" + if state.FireCheckHasResult { + fireValue = fmt.Sprintf("%d", state.FireCheckLastNumber) + } + + fireCandidate := "false" + fireLastSeenAge := "null" + if state.FireCheckCandidate { + fireCandidate = "true" + fireLastSeenAge = time.Since(state.FireCheckLastSeen).Round(time.Millisecond).String() + } + + logger.Logger.Printf(" [%d] DeviceUUID=%s | fire:%s | fire_candidate:%s | fire_last_seen_age:%s | fire_leave_enabled:%t | person:%s | temp:%s | threshold value:%s | cooling time:%ds", i, uuid, - data.PersonCount, - data.Temperature, - data.TemperatureThreshold, + fireValue, + fireCandidate, + fireLastSeenAge, + !state.FireLeaveDisabled, + personValue, + tempValue, + thresholdValue, cooldownLeft, ) i++ @@ -1049,11 +1091,9 @@ func debugDeviceData(deviceData connect.DeviceData) { 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"` @@ -1067,7 +1107,6 @@ func handleForbidTimeMessage(message []byte) { return } - // 解析content字段 var content struct { Minute string `json:"minute"` FireLeaveTimeout string `json:"fire_leave_timeout,omitempty"` @@ -1079,19 +1118,15 @@ func handleForbidTimeMessage(message []byte) { 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") @@ -1103,7 +1138,6 @@ func handleForbidTimeMessage(message []byte) { 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 { diff --git a/reporter/reporter.go b/reporter/reporter.go index 5866b47..44b78da 100644 --- a/reporter/reporter.go +++ b/reporter/reporter.go @@ -1,4 +1,4 @@ -package reporter +package reporter import ( "encoding/json" @@ -14,48 +14,37 @@ import ( ) var ( - // mergingAlarms 用于合并报警,避免重复处理 - mergingAlarms sync.Map - // 事件周期状态 + mergingAlarms sync.Map eventCycleMutex sync.RWMutex currentEventCycle EventCycle cycleTimeout time.Time ) -// EventCycle 事件周期类型 type EventCycle string const ( - // EventCycleFireLeave 动火离人上报周期 EventCycleFireLeave EventCycle = "fire_leave" - // EventCycleFireCheck 火焰检测上报周期 EventCycleFireCheck EventCycle = "fire_check" - // 周期时限缓存文件 - CycleTimeoutFile = "/data/cache/event_cycle_timeout.json" + CycleTimeoutFile = "/data/cache/event_cycle_timeout.json" ) -// Reporter 上报管理器 type Reporter struct { wsClient *connect.WSClient uploadURL string logger *Logger } -// Logger 上报管理器专用日志 type Logger struct { *log.Logger } -// NewLogger 创建上报管理器专用日志 func NewLogger() *Logger { return &Logger{ Logger: logger.GlobalLoggerManager.GetLogger("reporter"), } } -// NewReporter 创建上报管理器 func NewReporter() *Reporter { - // 初始化默认事件周期 eventCycleMutex.Lock() currentEventCycle = EventCycleFireLeave eventCycleMutex.Unlock() @@ -67,61 +56,50 @@ func NewReporter() *Reporter { } } -// getLogger 获取日志实例,确保不为nil func (r *Reporter) getLogger() *log.Logger { if r.logger != nil && r.logger.Logger != nil { return r.logger.Logger } - // 如果logger未初始化,使用全局Logger return logger.Logger } -// GetCurrentEventCycle 获取当前事件周期 func GetCurrentEventCycle() EventCycle { eventCycleMutex.RLock() defer eventCycleMutex.RUnlock() return currentEventCycle } -// IsCycleTimeout 检查事件周期是否超时 func IsCycleTimeout() bool { eventCycleMutex.RLock() defer eventCycleMutex.RUnlock() return time.Now().After(cycleTimeout) } -// SwitchEventCycle 切换事件周期 func SwitchEventCycle(cycle EventCycle) error { eventCycleMutex.Lock() defer eventCycleMutex.Unlock() - // 读取周期时限 timeout, err := loadCycleTimeout() if err != nil { - // 使用默认值 timeout = 30 * time.Minute } - // 更新事件周期和超时时间 oldCycle := currentEventCycle currentEventCycle = cycle cycleTimeout = time.Now().Add(timeout) - // 记录日志 if logger := logger.GlobalLoggerManager.GetLogger("reporter"); logger != nil { - logger.Printf("事件周期切换: %s → %s, 时限: %v", oldCycle, cycle, timeout) + logger.Printf("Event cycle switched: %s -> %s, timeout=%v", oldCycle, cycle, timeout) } return nil } -// CycleTimeoutConfig 周期时限配置 type CycleTimeoutConfig struct { FireLeaveTimeout time.Duration `json:"fire_leave_timeout"` FireCheckTimeout time.Duration `json:"fire_check_timeout"` } -// loadCycleTimeout 加载周期时限 func loadCycleTimeout() (time.Duration, error) { data, err := os.ReadFile(CycleTimeoutFile) if err != nil { @@ -133,7 +111,7 @@ func loadCycleTimeout() (time.Duration, error) { return 0, err } - // 根据当前周期返回对应时限 + // 閺嶈宓佽ぐ鎾冲閸涖劍婀℃潻鏂挎礀鐎电懓绨查弮鍫曟 switch currentEventCycle { case EventCycleFireLeave: return config.FireLeaveTimeout, nil @@ -144,14 +122,12 @@ func loadCycleTimeout() (time.Duration, error) { } } -// SaveCycleTimeout 保存周期时限 func SaveCycleTimeout(config CycleTimeoutConfig) error { data, err := json.Marshal(config) if err != nil { return err } - // 确保目录存在 if err := os.MkdirAll("/data/cache", 0755); err != nil { return err } @@ -159,9 +135,8 @@ func SaveCycleTimeout(config CycleTimeoutConfig) error { return os.WriteFile(CycleTimeoutFile, data, 0644) } -// ForbidTimeConfig 禁止时间配置 var ( - // ForbidTimeFile 禁止时间缓存文件 + // ForbidTimeFile 缁備焦顒涢弮鍫曟?缂傛挸鐡ㄩ弬鍥︽ ForbidTimeFile = "/data/cache/forbid_time_config.json" ) @@ -172,7 +147,7 @@ type ForbidTimeConfig struct { EndTime int64 `json:"end_time"` } -// SaveForbidTimeConfig 保存禁止时间配置 +// SaveForbidTimeConfig 娣囨繂鐡ㄧ粋浣诡剾閺冨爼妫块柊宥囩枂 func SaveForbidTimeConfig(minute string) error { return SaveForbidTimeWindow(minute, 0, 0) } @@ -189,7 +164,6 @@ func SaveForbidTimeWindow(minute string, startTime, endTime int64) error { return err } - // 确保目录存在 if err := os.MkdirAll("/data/cache", 0755); err != nil { return err } @@ -213,7 +187,6 @@ func LoadForbidTimeConfig() (ForbidTimeConfig, error) { return config, nil } -// SaveEventCycleTimeoutConfig 保存事件周期时限配置 func SaveEventCycleTimeoutConfig(fireLeaveTimeout, fireCheckTimeout time.Duration) error { config := CycleTimeoutConfig{ FireLeaveTimeout: fireLeaveTimeout, @@ -225,7 +198,6 @@ func SaveEventCycleTimeoutConfig(fireLeaveTimeout, fireCheckTimeout time.Duratio return err } - // 确保目录存在 if err := os.MkdirAll("/data/cache", 0755); err != nil { return err } @@ -233,11 +205,11 @@ func SaveEventCycleTimeoutConfig(fireLeaveTimeout, fireCheckTimeout time.Duratio return os.WriteFile(CycleTimeoutFile, data, 0644) } -// GetForbidTimeConfig 获取禁止时间配置 +// GetForbidTimeConfig 閼惧嘲褰囩粋浣诡剾閺冨爼妫块柊宥囩枂 func GetForbidTimeConfig() string { config, err := LoadForbidTimeConfig() if err != nil { - // 缓存中没有值时,默认返回0 + // 缂傛挸鐡ㄦ稉顓熺梾閺堝鈧吋妞傞敍宀勭帛鐠併倛绻戦崶? return "0" } @@ -265,19 +237,16 @@ func ParseForbidDuration(value string, defaultDuration time.Duration) time.Durat return defaultDuration } -// ShouldReportBothEvents 判断是否应该同时上报两种事件 func ShouldReportBothEvents() bool { - // 注意:此逻辑已被修改,现在只在特定情况下使用 - // 正常情况下,应根据当前事件周期上报对应类型的事件 return false } -// jsonString 将任意类型转换为JSON字符串 +// jsonString func jsonString(v interface{}) string { b, err := json.Marshal(v) if err != nil { if logger := logger.GlobalLoggerManager.GetLogger("reporter"); logger != nil { - logger.Printf("JSON编码失败: %v", err) + logger.Printf("JSON缂傛牜鐖滄径杈Е: %v", err) } return "" } @@ -290,7 +259,7 @@ func logJSON(logger *log.Logger, label string, v interface{}) { } b, err := json.Marshal(v) if err != nil { - logger.Printf("%s JSON编码失败: %v", label, err) + logger.Printf("%s JSON缂傛牜鐖滄径杈Е: %v", label, err) return } logger.Printf("%s %s", label, string(b)) @@ -298,71 +267,31 @@ func logJSON(logger *log.Logger, label string, v interface{}) { func cleanupMergedFileAfterUpload(logger *log.Logger, mergedFile, deviceUUID string, uploadFailed bool) { if uploadFailed { - logger.Printf("Nginx上传失败,保留临时文件用于排查: %s (DeviceUUID=%s)", mergedFile, deviceUUID) + logger.Printf("Upload failed, keep temp merged file for troubleshooting: %s (DeviceUUID=%s)", mergedFile, deviceUUID) return } - logger.Printf("清理临时文件: %s (DeviceUUID=%s)", mergedFile, deviceUUID) + logger.Printf("Remove temp merged file: %s (DeviceUUID=%s)", mergedFile, deviceUUID) if err := os.Remove(mergedFile); err != nil { - logger.Printf("清理临时文件失败: %v (DeviceUUID=%s, 文件=%s)", err, deviceUUID, mergedFile) + logger.Printf("Remove temp merged file failed: %v (DeviceUUID=%s, file=%s)", err, deviceUUID, mergedFile) } } - -// UploadVideoAndReport 上传视频并上报事件 func (r *Reporter) UploadVideoAndReport(deviceData connect.DeviceData, alarmTime int64, mergedFile string) error { - // 异步执行视频上传和上报,避免阻塞主线程 go func() { logger := r.getLogger() - logger.Printf("开始异步处理视频上传和事件上报 (DeviceUUID=%s, 文件=%s)", deviceData.DeviceUUID, mergedFile) defer func() { - if err := recover(); err != nil { - logger.Printf("上传视频和上报事件发生 panic: %v", err) - // 确保临时文件被清理 - os.Remove(mergedFile) + if recovered := recover(); recovered != nil { + logger.Printf("upload/report panic: %v (DeviceUUID=%s)", recovered, deviceData.DeviceUUID) + _ = os.Remove(mergedFile) } }() - // 检查事件周期是否超时 - logger.Printf("检查事件周期状态 (DeviceUUID=%s)", deviceData.DeviceUUID) if IsCycleTimeout() { - // 超时后切换回默认周期 - logger.Printf("事件周期超时,切换回默认周期 (DeviceUUID=%s)", deviceData.DeviceUUID) if err := SwitchEventCycle(EventCycleFireLeave); err != nil { - logger.Printf("事件周期超时切换失败: %v", err) + logger.Printf("switch event cycle failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) } } - // 获取当前事件周期 currentCycle := GetCurrentEventCycle() - logger.Printf("当前事件周期: %s (DeviceUUID=%s)", currentCycle, deviceData.DeviceUUID) - - // 检查是否应该同时上报两种事件 - if ShouldReportBothEvents() { - logger.Printf("Forbid time config is 0, will report both event types (DeviceUUID=%s)", deviceData.DeviceUUID) - - // 复制文件路径,避免在goroutine中使用同一个变量 - fileToClean := mergedFile - - // 上报fire_leave事件 - logger.Printf("开始上报fire_leave事件 (DeviceUUID=%s)", deviceData.DeviceUUID) - r.reportEventWithLevel(deviceData, alarmTime, mergedFile, ReportLevelFireLeave) - - // 上报fire_check事件 - logger.Printf("开始上报fire_check事件 (DeviceUUID=%s)", deviceData.DeviceUUID) - r.reportEventWithLevel(deviceData, alarmTime, mergedFile, ReportLevelFireCheck) - - // 延迟清理临时文件,确保上传完成 - go func() { - // 等待一段时间,确保上传操作有足够时间完成 - time.Sleep(5 * time.Second) - logger.Printf("清理临时文件: %s (DeviceUUID=%s)", fileToClean, deviceData.DeviceUUID) - os.Remove(fileToClean) - }() - - logger.Printf("双事件上报完成 (DeviceUUID=%s)", deviceData.DeviceUUID) - return - } - - // 根据当前事件周期确定上报级别 var reportLevel ReportLevel switch currentCycle { case EventCycleFireLeave: @@ -372,347 +301,250 @@ func (r *Reporter) UploadVideoAndReport(deviceData connect.DeviceData, alarmTime default: reportLevel = ReportLevelFireLeave } - logger.Printf("确定上报级别: %s (DeviceUUID=%s)", reportLevel, deviceData.DeviceUUID) - // 生成Nginx上传参数 - logger.Printf("生成Nginx上传参数 (DeviceUUID=%s)", deviceData.DeviceUUID) nginxParams := r.GenerateNginxParams(deviceData, alarmTime, reportLevel) - - // 上传视频到Nginx - logger.Printf("开始上传视频到Nginx (DeviceUUID=%s, 文件=%s, URL=%s)", deviceData.DeviceUUID, mergedFile, deviceData.UploadURL) videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL) - videoURLStr := "" - uploadFailed := false if err != nil { - uploadFailed = true - logger.Printf("Nginx上传失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - videoURLStr = videoURL - logger.Printf("Nginx上传成功: %s (DeviceUUID=%s)", videoURL, deviceData.DeviceUUID) - } - - // 检查WebSocket客户端 - logger.Printf("检查WebSocket客户端连接状态 (DeviceUUID=%s)", deviceData.DeviceUUID) - if r.wsClient == nil { - logger.Printf("WebSocket客户端未初始化,尝试重新获取 (DeviceUUID=%s)", deviceData.DeviceUUID) - // 尝试重新获取WebSocket客户端 - r.wsClient = connect.GetWSClient() - if r.wsClient == nil { - logger.Printf("WebSocket客户端仍然为nil,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID) - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) - return - } - } - - if !r.wsClient.IsConnected() { - logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID) - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) + logger.Printf("upload video failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, true) return } - logger.Printf("WebSocket客户端连接状态正常 (DeviceUUID=%s)", deviceData.DeviceUUID) - // 上报事件 - logger.Printf("开始上报事件 (DeviceUUID=%s, Level=%s, VideoURL=%s)", deviceData.DeviceUUID, reportLevel, videoURLStr) - if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(reportLevel)); err != nil { - logger.Printf("事件上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - logger.Printf("事件上报成功 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, reportLevel) - } - - // 上报数据快照 - logger.Printf("开始上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID) - if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil { - logger.Printf("数据快照上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - logger.Printf("数据快照上报成功 (DeviceUUID=%s)", deviceData.DeviceUUID) - } - - // 清理临时文件 - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) - logger.Printf("视频上传和事件上报完成 (DeviceUUID=%s)", deviceData.DeviceUUID) - }() - - return nil -} - -// UploadVideoAndReportFireLeave 上传视频并上报温度报警事件(只上报fire_leave类型) -func (r *Reporter) UploadVideoAndReportFireLeave(deviceData connect.DeviceData, alarmTime int64, mergedFile string) error { - // 异步执行视频上传和上报,避免阻塞主线程 - go func() { - logger := r.getLogger() - logger.Printf("开始异步处理温度报警视频上传和事件上报 (DeviceUUID=%s, 文件=%s)", deviceData.DeviceUUID, mergedFile) - defer func() { - if err := recover(); err != nil { - logger.Printf("上传温度报警视频和上报事件发生 panic: %v", err) - // 确保临时文件被清理 - os.Remove(mergedFile) - } - }() - - // 检查事件周期是否超时 - logger.Printf("检查事件周期状态 (DeviceUUID=%s)", deviceData.DeviceUUID) - if IsCycleTimeout() { - // 超时后切换回默认周期 - logger.Printf("事件周期超时,切换回默认周期 (DeviceUUID=%s)", deviceData.DeviceUUID) - if err := SwitchEventCycle(EventCycleFireLeave); err != nil { - logger.Printf("事件周期超时切换失败: %v", err) - } - } - - // 生成Nginx上传参数 - logger.Printf("生成Nginx上传参数 (DeviceUUID=%s)", deviceData.DeviceUUID) - nginxParams := r.GenerateNginxParams(deviceData, alarmTime, ReportLevelFireLeave) - - // 上传视频到Nginx - logger.Printf("开始上传视频到Nginx (DeviceUUID=%s, 文件=%s, URL=%s)", deviceData.DeviceUUID, mergedFile, deviceData.UploadURL) - videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL) - videoURLStr := "" - uploadFailed := false - if err != nil { - uploadFailed = true - logger.Printf("Nginx上传失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - videoURLStr = videoURL - logger.Printf("Nginx上传成功: %s (DeviceUUID=%s)", videoURL, deviceData.DeviceUUID) - } - - // 检查WebSocket客户端 - logger.Printf("检查WebSocket客户端连接状态 (DeviceUUID=%s)", deviceData.DeviceUUID) if r.wsClient == nil { - logger.Printf("WebSocket客户端未初始化,尝试重新获取 (DeviceUUID=%s)", deviceData.DeviceUUID) - // 尝试重新获取WebSocket客户端 r.wsClient = connect.GetWSClient() - if r.wsClient == nil { - logger.Printf("WebSocket客户端仍然为nil,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID) - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) - return - } } - - if !r.wsClient.IsConnected() { - logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID) - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) - return - } - logger.Printf("WebSocket客户端连接状态正常 (DeviceUUID=%s)", deviceData.DeviceUUID) - - // 上报事件(只上报fire_leave类型) - logger.Printf("开始上报温度报警事件 (DeviceUUID=%s, Level=%s, VideoURL=%s)", deviceData.DeviceUUID, ReportLevelFireLeave, videoURLStr) - if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(ReportLevelFireLeave)); err != nil { - logger.Printf("温度报警事件上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - logger.Printf("温度报警事件上报成功 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, ReportLevelFireLeave) - } - - // 上报数据快照 - logger.Printf("开始上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID) - if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil { - logger.Printf("数据快照上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - logger.Printf("数据快照上报成功 (DeviceUUID=%s)", deviceData.DeviceUUID) - } - - // 清理临时文件 - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) - logger.Printf("温度报警视频上传和事件上报完成 (DeviceUUID=%s)", deviceData.DeviceUUID) - }() - - return nil -} - -// UploadVideoAndReportFireCheck 上传视频并上报火焰检测报警事件(只上报fire_check类型) -func (r *Reporter) UploadVideoAndReportFireCheck(deviceData connect.DeviceData, alarmTime int64, mergedFile string) error { - // 异步执行视频上传和上报,避免阻塞主线程 - go func() { - logger := r.getLogger() - logger.Printf("开始异步处理火焰检测报警视频上传和事件上报 (DeviceUUID=%s, 文件=%s)", deviceData.DeviceUUID, mergedFile) - defer func() { - if err := recover(); err != nil { - logger.Printf("上传火焰检测报警视频和上报事件发生 panic: %v", err) - // 确保临时文件被清理 - os.Remove(mergedFile) - } - }() - - // 检查事件周期是否超时 - logger.Printf("检查事件周期状态 (DeviceUUID=%s)", deviceData.DeviceUUID) - if IsCycleTimeout() { - // 超时后切换到fire_check周期 - logger.Printf("事件周期超时,切换到fire_check周期 (DeviceUUID=%s)", deviceData.DeviceUUID) - if err := SwitchEventCycle(EventCycleFireCheck); err != nil { - logger.Printf("事件周期超时切换失败: %v", err) - } - } - - // 生成Nginx上传参数 - logger.Printf("生成Nginx上传参数 (DeviceUUID=%s)", deviceData.DeviceUUID) - nginxParams := r.GenerateNginxParams(deviceData, alarmTime, ReportLevelFireCheck) - - // 上传视频到Nginx - logger.Printf("开始上传视频到Nginx (DeviceUUID=%s, 文件=%s, URL=%s)", deviceData.DeviceUUID, mergedFile, deviceData.UploadURL) - videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL) - videoURLStr := "" - uploadFailed := false - if err != nil { - uploadFailed = true - logger.Printf("Nginx上传失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - videoURLStr = videoURL - logger.Printf("Nginx上传成功: %s (DeviceUUID=%s)", videoURL, deviceData.DeviceUUID) - } - - // 检查WebSocket客户端 - logger.Printf("检查WebSocket客户端连接状态 (DeviceUUID=%s)", deviceData.DeviceUUID) - if r.wsClient == nil { - logger.Printf("WebSocket客户端未初始化,尝试重新获取 (DeviceUUID=%s)", deviceData.DeviceUUID) - // 尝试重新获取WebSocket客户端 - r.wsClient = connect.GetWSClient() - if r.wsClient == nil { - logger.Printf("WebSocket客户端仍然为nil,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID) - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) - return - } - } - - if !r.wsClient.IsConnected() { - logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID) - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) - return - } - logger.Printf("WebSocket客户端连接状态正常 (DeviceUUID=%s)", deviceData.DeviceUUID) - - // 上报事件(只上报fire_check类型) - logger.Printf("开始上报火焰检测报警事件 (DeviceUUID=%s, Level=%s, VideoURL=%s)", deviceData.DeviceUUID, ReportLevelFireCheck, videoURLStr) - if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(ReportLevelFireCheck)); err != nil { - logger.Printf("火焰检测报警事件上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - logger.Printf("火焰检测报警事件上报成功 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, ReportLevelFireCheck) - } - - // 上报数据快照 - logger.Printf("开始上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID) - if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil { - logger.Printf("数据快照上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) - } else { - logger.Printf("数据快照上报成功 (DeviceUUID=%s)", deviceData.DeviceUUID) - } - - // 清理临时文件 - cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, uploadFailed) - logger.Printf("火焰检测报警视频上传和事件上报完成 (DeviceUUID=%s)", deviceData.DeviceUUID) - }() - - return nil -} - -// reportEventWithLevel 按指定级别上报事件 -func (r *Reporter) reportEventWithLevel(deviceData connect.DeviceData, alarmTime int64, mergedFile string, reportLevel ReportLevel) error { - // 异步执行视频上传和上报,避免阻塞主线程 - go func() { - logger := r.getLogger() - defer func() { - if err := recover(); err != nil { - logger.Printf("按指定级别上报事件发生 panic: %v", err) - } - }() - - // 生成Nginx上传参数 - nginxParams := r.GenerateNginxParams(deviceData, alarmTime, reportLevel) - - // 上传视频到Nginx - videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL) - videoURLStr := "" - if err != nil { - logger.Printf("Nginx上传失败: %v", err) - } else { - videoURLStr = videoURL - logger.Printf("Nginx上传成功: %s", videoURL) - } - - // 检查WebSocket客户端 if r.wsClient == nil || !r.wsClient.IsConnected() { - logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID) + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) return } - // 上报事件 - if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(reportLevel)); err != nil { - logger.Printf("事件上报失败: %v", err) + if err := r.ReportEventSync(deviceData, alarmTime, videoURL, string(reportLevel)); err != nil { + logger.Printf("report event failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) + return } - - // 上报数据快照 - if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil { - logger.Printf("数据快照上报失败: %v", err) + if err := r.ReportDataSnapshotSync(deviceData, alarmTime); err != nil { + logger.Printf("report snapshot failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) + return } - - logger.Printf("按指定级别上报事件完成 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, string(reportLevel)) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) }() return nil } -// ReportEvent 上报事件 +// UploadVideoAndReportFireLeave uploads and reports a fire_leave event asynchronously. +func (r *Reporter) UploadVideoAndReportFireLeave(deviceData connect.DeviceData, alarmTime int64, mergedFile string) error { + go func() { + logger := r.getLogger() + defer func() { + if recovered := recover(); recovered != nil { + logger.Printf("fire_leave upload/report panic: %v (DeviceUUID=%s)", recovered, deviceData.DeviceUUID) + _ = os.Remove(mergedFile) + } + }() + + if IsCycleTimeout() { + if err := SwitchEventCycle(EventCycleFireLeave); err != nil { + logger.Printf("switch event cycle failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + } + } + + nginxParams := r.GenerateNginxParams(deviceData, alarmTime, ReportLevelFireLeave) + videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL) + if err != nil { + logger.Printf("upload fire_leave video failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, true) + return + } + + if r.wsClient == nil { + r.wsClient = connect.GetWSClient() + } + if r.wsClient == nil || !r.wsClient.IsConnected() { + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) + return + } + + if err := r.ReportEventSync(deviceData, alarmTime, videoURL, string(ReportLevelFireLeave)); err != nil { + logger.Printf("report fire_leave event failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) + return + } + if err := r.ReportDataSnapshotSync(deviceData, alarmTime); err != nil { + logger.Printf("report fire_leave snapshot failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) + return + } + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) + }() + + return nil +} + +// UploadVideoAndReportFireCheck uploads and reports a fire_check event synchronously. +// UploadVideoAndReportFireCheck uploads and reports a fire_check event synchronously. +func (r *Reporter) UploadVideoAndReportFireCheck(deviceData connect.DeviceData, alarmTime int64, mergedFile string) (err error) { + logger := r.getLogger() + logger.Printf("Start fire_check upload/report (DeviceUUID=%s, file=%s)", deviceData.DeviceUUID, mergedFile) + + defer func() { + if recovered := recover(); recovered != nil { + logger.Printf("fire_check upload/report panic: %v (DeviceUUID=%s)", recovered, deviceData.DeviceUUID) + err = fmt.Errorf("fire_check upload/report panic: %v", recovered) + } + }() + + if IsCycleTimeout() { + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + if err := SwitchEventCycle(EventCycleFireCheck); err != nil { + logger.Printf("Switch event cycle to fire_check failed: %v", err) + } + } + + nginxParams := r.GenerateNginxParams(deviceData, alarmTime, ReportLevelFireCheck) + ensureNginxReportLevel(nginxParams, ReportLevelFireCheck) + logger.Printf("Upload fire_check video to Nginx (DeviceUUID=%s, file=%s, URL=%s)", deviceData.DeviceUUID, mergedFile, deviceData.UploadURL) + videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL) + if err != nil { + logger.Printf("Upload fire_check video to Nginx failed: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + return fmt.Errorf("upload fire_check video failed: %w", err) + } + logger.Printf("Upload fire_check video to Nginx succeeded: %s (DeviceUUID=%s)", videoURL, deviceData.DeviceUUID) + + if r.wsClient == nil { + r.wsClient = connect.GetWSClient() + } + if r.wsClient == nil { + return fmt.Errorf("websocket client not initialized") + } + if !r.wsClient.IsConnected() { + return fmt.Errorf("websocket client not connected") + } + + if err := r.ReportEventSync(deviceData, alarmTime, videoURL, string(ReportLevelFireCheck)); err != nil { + return err + } + if err := r.ReportDataSnapshotSync(deviceData, alarmTime); err != nil { + return err + } + + cleanupMergedFileAfterUpload(logger, mergedFile, deviceData.DeviceUUID, false) + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + return nil +} +func (r *Reporter) reportEventWithLevel(deviceData connect.DeviceData, alarmTime int64, mergedFile string, reportLevel ReportLevel) error { + go func() { + logger := r.getLogger() + defer func() { + if err := recover(); err != nil { + logger.Printf("鎸夋寚瀹氱骇鍒笂鎶ヤ簨浠跺彂鐢?panic: %v", err) + } + }() + + nginxParams := r.GenerateNginxParams(deviceData, alarmTime, reportLevel) + videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL) + videoURLStr := "" + if err != nil { + logger.Printf("Nginx涓婁紶澶辫触: %v", err) + } else { + videoURLStr = videoURL + logger.Printf("Nginx upload succeeded: %s", videoURL) + } + + if r.wsClient == nil || !r.wsClient.IsConnected() { + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + return + } + + if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(reportLevel)); err != nil { + logger.Printf("浜嬩欢涓婃姤澶辫触: %v", err) + } + if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil { + logger.Printf("鏁版嵁蹇収涓婃姤澶辫触: %v", err) + } + logger.Printf("Report event completed (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, string(reportLevel)) + }() + return nil +} + func (r *Reporter) ReportEvent(deviceData connect.DeviceData, alarmTime int64, videoURL string, level string) error { logger := r.getLogger() - - // 检查WebSocket客户端 if r.wsClient == nil { - logger.Printf("WebSocket客户端未初始化,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID) + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) return fmt.Errorf("websocket client not initialized") } - if !r.wsClient.IsConnected() { - logger.Printf("WebSocket客户端未连接,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID) + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) return fmt.Errorf("websocket client not connected") } - - // 生成事件消息 eventMsg := r.GenerateEventMessage(deviceData, alarmTime, videoURL, level) - - // 发送事件消息 r.wsClient.SendAsync(eventMsg) - logger.Printf("报警事件已上报 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, level) - + logger.Printf("Report event sync succeeded (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, level) + return nil +} + +func (r *Reporter) ReportEventSync(deviceData connect.DeviceData, alarmTime int64, videoURL string, level string) error { + logger := r.getLogger() + if r.wsClient == nil { + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + return fmt.Errorf("websocket client not initialized") + } + if !r.wsClient.IsConnected() { + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + return fmt.Errorf("websocket client not connected") + } + if err := r.wsClient.SendRawMessage(r.GenerateEventMessage(deviceData, alarmTime, videoURL, level)); err != nil { + logger.Printf("浜嬩欢鍚屾涓婃姤澶辫触: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + return err + } + logger.Printf("Report event sync succeeded (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, level) return nil } -// ReportDataSnapshot 上报数据快照 func (r *Reporter) ReportDataSnapshot(deviceData connect.DeviceData, alarmTime int64) error { logger := r.getLogger() - - // 检查WebSocket客户端 if r.wsClient == nil { - logger.Printf("WebSocket客户端未初始化,无法上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID) + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) return fmt.Errorf("websocket client not initialized") } - if !r.wsClient.IsConnected() { - logger.Printf("WebSocket客户端未连接,无法上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID) + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) return fmt.Errorf("websocket client not connected") } - - // 生成数据快照消息 dataMsg := r.GenerateDataMessage(deviceData, alarmTime) - - // 发送数据快照消息 r.wsClient.SendAsync(dataMsg) - logger.Printf("数据快照已上报 (DeviceUUID=%s)", deviceData.DeviceUUID) - + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) return nil } -// GenerateEventMessage 生成事件消息 -func (r *Reporter) GenerateEventMessage(deviceData connect.DeviceData, alarmTime int64, videoURL string, level string) map[string]interface{} { - // 获取HostUUID - hostUUID := r.getHostUUID(deviceData.DeviceUUID) - - // 生成描述 - description := "无人工作区且温度超阈值报警" - if level == string(ReportLevelFireCheck) { - description = "检测到异常火苗" +func (r *Reporter) ReportDataSnapshotSync(deviceData connect.DeviceData, alarmTime int64) error { + logger := r.getLogger() + if r.wsClient == nil { + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + return fmt.Errorf("websocket client not initialized") } + if !r.wsClient.IsConnected() { + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + return fmt.Errorf("websocket client not connected") + } + if err := r.wsClient.SendRawMessage(r.GenerateDataMessage(deviceData, alarmTime)); err != nil { + logger.Printf("鏁版嵁蹇収鍚屾涓婃姤澶辫触: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID) + return err + } + logger.Printf("Report data snapshot sync succeeded (DeviceUUID=%s)", deviceData.DeviceUUID) + return nil +} - // 构建事件负载 +func (r *Reporter) GenerateEventMessage(deviceData connect.DeviceData, alarmTime int64, videoURL string, level string) map[string]interface{} { + hostUUID := r.getHostUUID(deviceData.DeviceUUID) + description := "No person in work area and temperature over threshold" + if level == string(ReportLevelFireCheck) { + description = "Abnormal fire detected" + } eventPayload := EventPayload{ Type: string(ReportTypeEvent), Args: map[string]interface{}{ @@ -725,22 +557,16 @@ func (r *Reporter) GenerateEventMessage(deviceData connect.DeviceData, alarmTime "address": deviceData.Address, }, } - - // 构建上报消息 - eventMsg := map[string]interface{}{ + return map[string]interface{}{ "method": "metric_data", "params": map[string]interface{}{ "route_key": fmt.Sprintf("/dhlr/device/%s/event", deviceData.DeviceUUID), "metric": jsonString(eventPayload), }, } - - return eventMsg } -// GenerateDataMessage 生成数据快照消息 func (r *Reporter) GenerateDataMessage(deviceData connect.DeviceData, alarmTime int64) map[string]interface{} { - // 构建数据快照负载 dataPayload := DataPayload{ Type: string(ReportTypeData), Args: map[string]interface{}{ @@ -754,31 +580,21 @@ func (r *Reporter) GenerateDataMessage(deviceData connect.DeviceData, alarmTime "address": deviceData.Address, }, } - - // 构建上报消息 - dataMsg := map[string]interface{}{ + return map[string]interface{}{ "method": "metric_data", "params": map[string]interface{}{ "route_key": fmt.Sprintf("/dhlr/device/%s/data", deviceData.DeviceUUID), "metric": jsonString(dataPayload), }, } - - return dataMsg } -// GenerateNginxParams 生成Nginx上传参数 func (r *Reporter) GenerateNginxParams(deviceData connect.DeviceData, alarmTime int64, level ReportLevel) map[string]interface{} { - // 获取HostUUID hostUUID := r.getHostUUID(deviceData.DeviceUUID) - - // 生成描述 - description := "无人工作区且温度超阈值报警" + description := "No person in work area and temperature over threshold" if level == ReportLevelFireCheck { - description = "检测到异常火苗" + description = "Abnormal fire detected" } - - // 构建参数 params := map[string]interface{}{ "alarm_time": alarmTime, "description": description, @@ -787,127 +603,152 @@ func (r *Reporter) GenerateNginxParams(deviceData connect.DeviceData, alarmTime "person_count": deviceData.PersonCount, "level": string(level), } - - // 火焰检测不需要温度数据 if level != ReportLevelFireCheck { params["temperature"] = deviceData.Temperature } - logJSON(r.getLogger(), fmt.Sprintf("[Nginx Upload Params JSON] DeviceUUID=%s Level=%s", deviceData.DeviceUUID, level), params) - return params } -// TriggerAlarmWithDelay 延迟触发报警并上报(温度报警) +func ensureNginxReportLevel(params map[string]interface{}, level ReportLevel) { + params["level"] = string(level) + if _, ok := params["description"]; !ok { + if level == ReportLevelFireCheck { + params["description"] = "Abnormal fire detected" + } else { + params["description"] = "No person in work area and temperature over threshold" + } + } +} + +// TriggerAlarmWithDelay 瀵ゆ儼绻滅憴锕€褰傞幎銉劅楠炴湹绗傞幎銉礄濞撯晛瀹抽幎銉劅閿?// TriggerAlarmWithDelay handles a fire_leave temperature alarm. func (r *Reporter) TriggerAlarmWithDelay(deviceData connect.DeviceData, alarmTime int64, cooldown int) { startTime := time.Now() logger := r.getLogger() - logger.Printf("[%.3f] 开始处理温度报警事件 (DeviceUUID=%s, alarmTime=%d, cooldown=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime, cooldown) + logger.Printf("[%.3f] Start fire_leave alarm handling (DeviceUUID=%s, alarmTime=%d, cooldown=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime, cooldown) - // 捕获所有异常,确保函数不会因为panic而崩溃 defer func() { if err := recover(); err != nil { - logger.Printf("[%.3f] 处理温度报警事件发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID) - // 确保锁被释放 + logger.Printf("[%.3f] fire_leave alarm handling panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID) key := fmt.Sprintf("%s_%d", deviceData.DeviceUUID, alarmTime) mergingAlarms.Delete(key) } }() - // 防止重复处理 key := fmt.Sprintf("%s_%d", deviceData.DeviceUUID, alarmTime) - logger.Printf("[%.3f] 检查温度报警事件是否已在处理中 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) + logger.Printf("[%.3f] Check fire_leave alarm lock (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) if _, loaded := mergingAlarms.LoadOrStore(key, true); loaded { - logger.Printf("[%.3f] 温度报警事件已在处理中,跳过重复处理 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) + logger.Printf("[%.3f] fire_leave alarm already handling, skip duplicate (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) return } defer mergingAlarms.Delete(key) - logger.Printf("[%.3f] 成功获取处理锁 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) + logger.Printf("[%.3f] fire_leave alarm lock acquired (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) - // 合并视频 - logger.Printf("[%.3f] 开始合并视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime) + logger.Printf("[%.3f] Start merging fire_leave video (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime) mergeStartTime := time.Now() mergedFile, err := video_server.MergeVideo(deviceData.DeviceUUID, alarmTime) if err != nil { - logger.Printf("[%.3f] 视频合并失败 (DeviceUUID=%s, 耗时:%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds(), err) + logger.Printf("[%.3f] Merge fire_leave video failed (DeviceUUID=%s, elapsed=%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds(), err) return } - logger.Printf("[%.3f] 视频合并成功,生成文件: %s (DeviceUUID=%s, 耗时:%.3fs)", time.Since(startTime).Seconds(), mergedFile, deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds()) + logger.Printf("[%.3f] Merge fire_leave video succeeded: %s (DeviceUUID=%s, elapsed=%.3fs)", time.Since(startTime).Seconds(), mergedFile, deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds()) - // 上传视频并上报(只上报温度报警,即fire_leave类型) - logger.Printf("[%.3f] 开始上传视频并上报温度报警事件 (DeviceUUID=%s, 文件=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, mergedFile) + logger.Printf("[%.3f] Start upload/report for fire_leave alarm (DeviceUUID=%s, file=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, mergedFile) uploadStartTime := time.Now() if err := r.UploadVideoAndReportFireLeave(deviceData, alarmTime, mergedFile); err != nil { - logger.Printf("[%.3f] 温度报警上报失败 (DeviceUUID=%s, 耗时:%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds(), err) - // 清理临时文件 + logger.Printf("[%.3f] fire_leave alarm upload/report failed (DeviceUUID=%s, elapsed=%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds(), err) if err := os.Remove(mergedFile); err != nil { - logger.Printf("[%.3f] 清理临时文件失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID) + logger.Printf("[%.3f] Remove merged file after fire_leave failure failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID) } return } - logger.Printf("[%.3f] 温度报警上报成功 (DeviceUUID=%s, 耗时:%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds()) + logger.Printf("[%.3f] fire_leave alarm upload/report succeeded (DeviceUUID=%s, elapsed=%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds()) - logger.Printf("[%.3f] 温度报警事件处理完成 (DeviceUUID=%s, 总耗时:%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(startTime).Seconds()) + logger.Printf("[%.3f] fire_leave alarm handling completed (DeviceUUID=%s, total=%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(startTime).Seconds()) } - -// TriggerFireCheckAlarm 触发火焰检测报警并上报 -func (r *Reporter) TriggerFireCheckAlarm(deviceData connect.DeviceData, alarmTime int64) { +func (r *Reporter) TriggerFireCheckAlarm(deviceData connect.DeviceData, alarmTime int64) bool { startTime := time.Now() logger := r.getLogger() - logger.Printf("[%.3f] 开始处理火焰检测报警事件 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime) + logger.Printf("[%.3f] Start fire_check alarm handling (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime) - // 捕获所有异常,确保函数不会因为panic而崩溃 defer func() { if err := recover(); err != nil { - logger.Printf("[%.3f] 处理火焰检测报警事件发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID) - // 确保锁被释放 - key := fmt.Sprintf("%s_fire_%d", deviceData.DeviceUUID, alarmTime) + logger.Printf("[%.3f] fire_check alarm handling panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID) + key := fmt.Sprintf("%s_fire", deviceData.DeviceUUID) mergingAlarms.Delete(key) } }() - // 防止重复处理 - key := fmt.Sprintf("%s_fire_%d", deviceData.DeviceUUID, alarmTime) - logger.Printf("[%.3f] 检查火焰检测报警事件是否已在处理中 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) + key := fmt.Sprintf("%s_fire", deviceData.DeviceUUID) + logger.Printf("[%.3f] Check fire_check alarm lock (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) if _, loaded := mergingAlarms.LoadOrStore(key, true); loaded { - logger.Printf("[%.3f] 火焰检测报警事件已在处理中,跳过重复处理 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) - return + logger.Printf("[%.3f] fire_check alarm already handling, skip duplicate (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) + return false } defer mergingAlarms.Delete(key) - logger.Printf("[%.3f] 成功获取处理锁 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) + logger.Printf("[%.3f] fire_check alarm lock acquired (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key) - // 合并视频(使用fire_check目录的视频) - logger.Printf("[%.3f] 开始合并火焰检测视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime) + const fireCheckVideoFlushDelay = 21 * time.Second + logger.Printf("[%.3f] Wait %v for fire_check video cache flush before merge (DeviceUUID=%s)", + time.Since(startTime).Seconds(), fireCheckVideoFlushDelay, deviceData.DeviceUUID) + time.Sleep(fireCheckVideoFlushDelay) + + logger.Printf("[%.3f] Start merging fire_check video (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime) mergeStartTime := time.Now() - // 这里可以修改MergeVideo函数,使其支持指定目录,或者创建一个新的函数用于合并fire_check视频 - mergedFile, err := video_server.MergeVideo(deviceData.DeviceUUID+"_fire_check", alarmTime) + mergedFile, err := video_server.MergeVideoFromDirectory( + deviceData.DeviceUUID+"_fire_check", + deviceData.DeviceUUID, + alarmTime, + ) if err != nil { - logger.Printf("[%.3f] 火焰检测视频合并失败 (DeviceUUID=%s, 耗时:%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds(), err) - return + logger.Printf("[%.3f] Merge fire_check video failed (DeviceUUID=%s, elapsed=%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds(), err) + dumpFireCheckVideoDirectory(logger, deviceData.DeviceUUID) + return false } - logger.Printf("[%.3f] 火焰检测视频合并成功,生成文件: %s (DeviceUUID=%s, 耗时:%.3fs)", time.Since(startTime).Seconds(), mergedFile, deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds()) + logger.Printf("[%.3f] Merge fire_check video succeeded: %s (DeviceUUID=%s, elapsed=%.3fs)", time.Since(startTime).Seconds(), mergedFile, deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds()) - // 上传视频并上报(只上报火焰检测,即fire_check类型) - logger.Printf("[%.3f] 开始上传视频并上报火焰检测报警事件 (DeviceUUID=%s, 文件=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, mergedFile) + logger.Printf("[%.3f] Start upload/report for fire_check alarm (DeviceUUID=%s, file=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, mergedFile) uploadStartTime := time.Now() if err := r.UploadVideoAndReportFireCheck(deviceData, alarmTime, mergedFile); err != nil { - logger.Printf("[%.3f] 火焰检测报警上报失败 (DeviceUUID=%s, 耗时:%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds(), err) - // 清理临时文件 + logger.Printf("[%.3f] fire_check alarm upload/report failed (DeviceUUID=%s, elapsed=%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds(), err) if err := os.Remove(mergedFile); err != nil { - logger.Printf("[%.3f] 清理临时文件失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID) + logger.Printf("[%.3f] Remove merged file after fire_check failure failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID) } - return + return false } - logger.Printf("[%.3f] 火焰检测报警上报成功 (DeviceUUID=%s, 耗时:%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds()) + logger.Printf("[%.3f] fire_check alarm upload/report succeeded (DeviceUUID=%s, elapsed=%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds()) - logger.Printf("[%.3f] 火焰检测报警事件处理完成 (DeviceUUID=%s, 总耗时:%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(startTime).Seconds()) + logger.Printf("[%.3f] fire_check alarm handling completed (DeviceUUID=%s, total=%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(startTime).Seconds()) + return true +} + +func dumpFireCheckVideoDirectory(logger *log.Logger, deviceUUID string) { + videoDir := fmt.Sprintf("/usr/data/camera/%s_fire_check", deviceUUID) + files, err := os.ReadDir(videoDir) + if err != nil { + logger.Printf("fire_check video directory snapshot failed: dir=%s, err=%v", videoDir, err) + return + } + + logger.Printf("fire_check video directory snapshot: dir=%s, files=%d", videoDir, len(files)) + for _, file := range files { + info, err := file.Info() + if err != nil { + logger.Printf(" file=%s, stat_error=%v", file.Name(), err) + continue + } + if file.IsDir() { + logger.Printf(" dir=%s, mod=%s", file.Name(), info.ModTime().Format(time.RFC3339)) + continue + } + logger.Printf(" file=%s, size=%d, mod=%s", file.Name(), info.Size(), info.ModTime().Format(time.RFC3339)) + } } -// getHostUUID 获取设备的HostUUID func (r *Reporter) getHostUUID(deviceUUID string) string { configs, err := connect.LoadServiceConfig() if err != nil { - r.logger.Printf("加载服务配置失败: %v", err) + r.logger.Printf("Load service config failed: %v", err) return "" } @@ -920,17 +761,17 @@ func (r *Reporter) getHostUUID(deviceUUID string) string { return "" } -// SetWSClient 设置WebSocket客户端 +// SetWSClient func (r *Reporter) SetWSClient(client *connect.WSClient) { r.wsClient = client } -// SetUploadURL 设置上传URL +// SetUploadURL 鐠佸墽鐤嗘稉濠佺炊URL func (r *Reporter) SetUploadURL(url string) { r.uploadURL = url } -// IsWSReady 检查WebSocket是否就绪 +// IsWSReady 濡偓閺岊櫇ebSocket閺勵垰鎯佺亸杈╁崕 func (r *Reporter) IsWSReady() bool { return r.wsClient != nil && r.wsClient.IsConnected() } diff --git a/video_server/video_merge.go b/video_server/video_merge.go index 97cc158..ecb0415 100644 --- a/video_server/video_merge.go +++ b/video_server/video_merge.go @@ -1,7 +1,6 @@ -package video_server +package video_server import ( - "context" "fmt" "io" "os" @@ -15,35 +14,45 @@ import ( "FireLeave_tool/logger" ) -// MergeVideo 合并视频文件 - 完全适配 mp4_merge --out 模式 -func MergeVideo(deviceUUID string, alarmTime int64) (string, error) { +// MergeVideo merges nearby raw video clips with ffmpeg. +func MergeVideo(deviceUUID string, alarmTime int64) (mergedPath string, err error) { + return mergeVideo(deviceUUID, deviceUUID, alarmTime) +} + +// MergeVideoFromDirectory reads clips from one device directory while naming +// the uploaded file with the real device UUID. +func MergeVideoFromDirectory(videoDirectoryUUID, uploadDeviceUUID string, alarmTime int64) (mergedPath string, err error) { + return mergeVideo(videoDirectoryUUID, uploadDeviceUUID, alarmTime) +} + +func mergeVideo(deviceUUID, uploadDeviceUUID string, alarmTime int64) (mergedPath string, err error) { startTime := time.Now() - logger.Logger.Printf("[%.3f] 开始合并视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime) - - // 捕获所有异常,确保函数不会因为panic而崩溃 + logger.Logger.Printf("[%.3f] Start video merge (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime) + + // Guard the merge path so one bad video cannot crash the process. defer func() { - if err := recover(); err != nil { - logger.Logger.Printf("[%.3f] 合并视频发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) + if recovered := recover(); recovered != nil { + logger.Logger.Printf("[%.3f] Merge video panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), recovered, deviceUUID) + mergedPath = "" + err = fmt.Errorf("merge video panic: %v", recovered) } }() - // 检查视频目录 videoDir := fmt.Sprintf("/usr/data/camera/%s", deviceUUID) - logger.Logger.Printf("[%.3f] 检查视频目录: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID) + logger.Logger.Printf("[%.3f] Check video directory: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID) if _, err := os.Stat(videoDir); os.IsNotExist(err) { - logger.Logger.Printf("[%.3f] 视频目录不存在: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID) + logger.Logger.Printf("[%.3f] Video directory does not exist: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID) return "", fmt.Errorf("video directory not exist: %s", videoDir) } else if err != nil { - logger.Logger.Printf("[%.3f] 检查视频目录失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) + logger.Logger.Printf("[%.3f] Check video directory failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) return "", fmt.Errorf("check video directory failed: %v", err) } - logger.Logger.Printf("[%.3f] 视频目录检查成功: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID) + logger.Logger.Printf("[%.3f] Video directory is ready: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), videoDir, deviceUUID) - // === 1. 时间对齐到 10 秒 === - logger.Logger.Printf("[%.3f] 开始时间对齐处理 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime) + logger.Logger.Printf("[%.3f] Align alarm time to 10-second boundary (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime) alarmTimeAligned := alignTo10Seconds(alarmTime) alarmTimeStr := time.Unix(alarmTimeAligned, 0).Format("20060102150405") - logger.Logger.Printf("[%.3f] 时间对齐结果: alarmTime=%d → aligned=%d (DeviceUUID=%s)", time.Since(startTime).Seconds(), alarmTime, alarmTimeAligned, deviceUUID) + logger.Logger.Printf("[%.3f] Time aligned: alarmTime=%d aligned=%d (DeviceUUID=%s)", time.Since(startTime).Seconds(), alarmTime, alarmTimeAligned, deviceUUID) beforeTime := alarmTimeAligned - 10 afterTime := alarmTimeAligned + 10 @@ -51,40 +60,37 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) { beforeStr := time.Unix(beforeTime, 0).Format("20060102150405") afterStr := time.Unix(afterTime, 0).Format("20060102150405") - paths := map[string]string{ - beforeStr: filepath.Join(videoDir, beforeStr+".mp4"), - alarmTimeStr: filepath.Join(videoDir, alarmTimeStr+".mp4"), - afterStr: filepath.Join(videoDir, afterStr+".mp4"), - } + targetNames := []string{beforeStr, alarmTimeStr, afterStr} - // 查找视频文件 videoFiles := []string{} - logger.Logger.Printf("[%.3f] 开始查找视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) - for name, path := range paths { - logger.Logger.Printf("[%.3f] 检查文件: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), path, deviceUUID) - if _, err := os.Stat(path); err == nil { + logger.Logger.Printf("[%.3f] Search expected video files (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) + missingVideos := []string{} + for _, name := range targetNames { + if path, ok := findVideoFileForTimestamp(videoDir, name); ok { videoFiles = append(videoFiles, path) - logger.Logger.Printf("[%.3f] Found video: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), name+".mp4", deviceUUID) + logger.Logger.Printf("[%.3f] Found video: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), filepath.Base(path), deviceUUID) } else { - logger.Logger.Printf("[%.3f] Missing video: %s (DeviceUUID=%s, error: %v)", time.Since(startTime).Seconds(), name+".mp4", deviceUUID, err) + missingVideos = append(missingVideos, name+".mp4/.h264") } } + if len(missingVideos) > 0 { + logger.Logger.Printf("[%.3f] Missing expected videos: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), strings.Join(missingVideos, ","), deviceUUID) + } - // === 2. 补充替代文件 === - logger.Logger.Printf("[%.3f] 当前找到 %d 个视频文件,开始补充替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID) + logger.Logger.Printf("[%.3f] Found %d expected video files, searching fallbacks if needed (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID) if len(videoFiles) < 3 { - logger.Logger.Printf("[%.3f] 视频文件不足3个,开始查找替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) + logger.Logger.Printf("[%.3f] Less than 3 expected videos found, scan directory for fallback files (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) allFiles, err := os.ReadDir(videoDir) if err != nil { - logger.Logger.Printf("[%.3f] 读取目录失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) + logger.Logger.Printf("[%.3f] Read video directory failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) } else { - logger.Logger.Printf("[%.3f] 读取目录所有文件,共 %d 个文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(allFiles), deviceUUID) + logger.Logger.Printf("[%.3f] Directory contains %d files (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(allFiles), deviceUUID) var candidates []struct { path string ts int64 } for _, f := range allFiles { - if !f.IsDir() && strings.HasSuffix(f.Name(), ".mp4") && !strings.Contains(f.Name(), "_joined") { + if !f.IsDir() && isSupportedRawVideoFile(f.Name()) { if ts, err := parseSimpleTimestamp(f.Name()); err == nil { candidates = append(candidates, struct { path string @@ -93,7 +99,7 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) { } } } - logger.Logger.Printf("[%.3f] 找到 %d 个候选视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(candidates), deviceUUID) + logger.Logger.Printf("[%.3f] Found %d fallback candidate files (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(candidates), deviceUUID) targets := []int64{beforeTime, alarmTimeAligned, afterTime} for _, target := range targets { if containsTimestamp(videoFiles, target) { @@ -108,16 +114,20 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) { } } - logger.Logger.Printf("[%.3f] 最终收集到 %d 个视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID) + logger.Logger.Printf("[%.3f] Collected %d video files for merge (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID) if len(videoFiles) < 2 { - logger.Logger.Printf("[%.3f] 视频文件不足,至少需要 2 个文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) + logger.Logger.Printf("[%.3f] Not enough video files, need at least 2 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) return "", fmt.Errorf("insufficient video files: %d", len(videoFiles)) } - // === 3. 排序 === - logger.Logger.Printf("[%.3f] 开始排序视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) + logger.Logger.Printf("[%.3f] Sort video files by timestamp (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) sort.Slice(videoFiles, func(i, j int) bool { - return getTimestampFromPath(videoFiles[i]) < getTimestampFromPath(videoFiles[j]) + left, leftErr := getTimestampFromPath(videoFiles[i]) + right, rightErr := getTimestampFromPath(videoFiles[j]) + if leftErr != nil || rightErr != nil { + return videoFiles[i] < videoFiles[j] + } + return left < right }) logger.Logger.Printf("[%.3f] Merging %d videos (DeviceUUID=%s):", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID) @@ -125,93 +135,203 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) { logger.Logger.Printf("[%.3f] [%d] %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), i, filepath.Base(f), deviceUUID) } - // === 4. 显式指定输出文件(100% 可控)=== outputFile := fmt.Sprintf("%s.mp4_joined.mp4", alarmTimeStr) joinedPath := filepath.Join(videoDir, outputFile) - logger.Logger.Printf("[%.3f] 输出文件路径: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID) + logger.Logger.Printf("[%.3f] Output temp file: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID) - // 清理旧文件(防止冲突) - logger.Logger.Printf("[%.3f] 清理旧的输出文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) + logger.Logger.Printf("[%.3f] Remove old output file if present (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) if err := os.Remove(joinedPath); err != nil && !os.IsNotExist(err) { logger.Logger.Printf("[%.3f] Failed to remove old output file %s: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID) } else if err == nil { - logger.Logger.Printf("[%.3f] 旧的输出文件清理成功: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID) + logger.Logger.Printf("[%.3f] Old output file removed: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID) } - // === 5. 调用 mp4_merge,显式 --out === - args := append(videoFiles, "--out", joinedPath) - cmd := exec.Command("mp4_merge", args...) + args := []string{"-hide_banner", "-loglevel", "error", "-y"} + if allH264Files(videoFiles) { + rawJoinedPath := filepath.Join(videoDir, fmt.Sprintf("%s_joined.h264", alarmTimeStr)) + logger.Logger.Printf("[%.3f] Concatenate H264 files in timestamp order: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), rawJoinedPath, deviceUUID) + if err := concatenateFiles(rawJoinedPath, videoFiles); err != nil { + logger.Logger.Printf("[%.3f] Concatenate H264 files failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) + return "", fmt.Errorf("concatenate H264 files failed: %v", err) + } + defer os.Remove(rawJoinedPath) + + args = append(args, + "-fflags", "+genpts", + "-f", "h264", + "-i", rawJoinedPath, + "-c:v", "copy", + "-movflags", "+faststart", + joinedPath, + ) + } else { + concatListPath := filepath.Join(videoDir, fmt.Sprintf("%s_ffmpeg_concat.txt", alarmTimeStr)) + if err := writeFFmpegConcatList(concatListPath, videoFiles); err != nil { + logger.Logger.Printf("[%.3f] Create ffmpeg concat list failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) + return "", fmt.Errorf("create ffmpeg concat list failed: %v", err) + } + defer os.Remove(concatListPath) + + args = append(args, + "-f", "concat", + "-safe", "0", + "-i", concatListPath, + "-c", "copy", + "-movflags", "+faststart", + joinedPath, + ) + } + cmd := exec.Command("ffmpeg", args...) + cmd.Env = ffmpegCleanEnv() cmd.Stdout = logger.Logger.Writer() cmd.Stderr = logger.Logger.Writer() - logger.Logger.Printf("[%.3f] 开始执行视频合并命令: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), cmd.Args, deviceUUID) + logger.Logger.Printf("[%.3f] Run merge command: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), cmd.Args, deviceUUID) mergeStartTime := time.Now() if err := cmd.Run(); err != nil { - logger.Logger.Printf("[%.3f] 视频合并命令执行失败 (耗时:%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), err, deviceUUID) - return "", fmt.Errorf("mp4_merge failed: %v", err) + logger.Logger.Printf("[%.3f] Merge command failed (elapsed=%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), err, deviceUUID) + return "", fmt.Errorf("ffmpeg merge failed: %v", err) } - logger.Logger.Printf("[%.3f] 视频合并命令执行成功 (耗时:%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), deviceUUID) + logger.Logger.Printf("[%.3f] Merge command succeeded (elapsed=%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), deviceUUID) - // === 6. 验证输出文件存在 === - logger.Logger.Printf("[%.3f] 验证输出文件是否存在: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID) + logger.Logger.Printf("[%.3f] Verify merged file: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID) if _, err := os.Stat(joinedPath); err != nil { - logger.Logger.Printf("[%.3f] 输出文件未创建: %s, error: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID) + logger.Logger.Printf("[%.3f] Output file not created: %s, error: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID) return "", fmt.Errorf("output file not created: %s, error: %v", joinedPath, err) } info, err := os.Stat(joinedPath) if err != nil { - logger.Logger.Printf("[%.3f] 获取输出文件信息失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) + logger.Logger.Printf("[%.3f] Stat merged file failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) return "", fmt.Errorf("stat output file failed: %v", err) } logger.Logger.Printf("[%.3f] Merged file created: %s (size: %d bytes) (DeviceUUID=%s)", time.Since(startTime).Seconds(), outputFile, info.Size(), deviceUUID) - // === 7. 移动到 /data/upload/ === - logger.Logger.Printf("[%.3f] 开始移动文件到 /data/upload/ 目录 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) + logger.Logger.Printf("[%.3f] Move merged file to /data/upload (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) t := time.Unix(alarmTimeAligned, 0) year, month, day := t.Date() timestampPart := fmt.Sprintf("%d-%d-%d-%d", year, int(month), day, alarmTimeAligned) - finalName := fmt.Sprintf("%s-%s.MP4", deviceUUID, timestampPart) + finalName := fmt.Sprintf("%s-%s.MP4", uploadDeviceUUID, timestampPart) finalPath := filepath.Join("/data/upload", finalName) - logger.Logger.Printf("[%.3f] 最终文件路径: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), finalPath, deviceUUID) + logger.Logger.Printf("[%.3f] Final upload path: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), finalPath, deviceUUID) - logger.Logger.Printf("[%.3f] 确保 /data/upload 目录存在 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) + logger.Logger.Printf("[%.3f] Ensure /data/upload exists (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) if err := os.MkdirAll("/data/upload", 0755); err != nil { - logger.Logger.Printf("[%.3f] 创建 /data/upload 目录失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) + logger.Logger.Printf("[%.3f] Create /data/upload failed: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) return "", fmt.Errorf("create /data/upload failed: %v", err) } - // 跨分区复制 + 删除 - logger.Logger.Printf("[%.3f] 开始复制文件: %s → %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, finalPath, deviceUUID) + logger.Logger.Printf("[%.3f] Copy merged file: %s -> %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, finalPath, deviceUUID) copyStartTime := time.Now() if err := copyFile(joinedPath, finalPath); err != nil { - logger.Logger.Printf("[%.3f] 文件复制失败 (耗时:%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(copyStartTime).Seconds(), err, deviceUUID) + logger.Logger.Printf("[%.3f] Copy merged file failed (elapsed=%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(copyStartTime).Seconds(), err, deviceUUID) return "", fmt.Errorf("copy failed: %v", err) } - logger.Logger.Printf("[%.3f] 文件复制成功 (耗时:%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(copyStartTime).Seconds(), deviceUUID) - - logger.Logger.Printf("[%.3f] 删除临时文件: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID) + logger.Logger.Printf("[%.3f] Copy merged file succeeded (elapsed=%.3fs) (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(copyStartTime).Seconds(), deviceUUID) + + logger.Logger.Printf("[%.3f] Delete temp merged file: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID) if err := os.Remove(joinedPath); err != nil { logger.Logger.Printf("[%.3f] Failed to delete temp file %s: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID) } else { - logger.Logger.Printf("[%.3f] 临时文件删除成功 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) + logger.Logger.Printf("[%.3f] Temp merged file deleted (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) } - logger.Logger.Printf("[%.3f] Video merged and moved successfully: %s (DeviceUUID=%s, 总耗时:%.3fs)", time.Since(startTime).Seconds(), finalPath, deviceUUID, time.Since(startTime).Seconds()) + logger.Logger.Printf("[%.3f] Video merged and moved successfully: %s (DeviceUUID=%s, total=%.3fs)", time.Since(startTime).Seconds(), finalPath, deviceUUID, time.Since(startTime).Seconds()) return finalPath, nil } -// containsTimestamp 检查是否已有该时间戳 +// containsTimestamp reports whether files include the given timestamp.mp(files []string, ts int64) bool { func containsTimestamp(files []string, ts int64) bool { for _, f := range files { - if getTimestampFromPath(f) == ts { + timestamp, err := getTimestampFromPath(f) + if err == nil && timestamp == ts { return true } } return false } -// findClosest 查找最接近的时间戳文件 +func findVideoFileForTimestamp(videoDir, timestamp string) (string, bool) { + for _, ext := range []string{".mp4", ".h264"} { + path := filepath.Join(videoDir, timestamp+ext) + if _, err := os.Stat(path); err == nil { + return path, true + } + } + return "", false +} + +func isSupportedRawVideoFile(name string) bool { + if strings.Contains(name, "_joined") { + return false + } + ext := strings.ToLower(filepath.Ext(name)) + return ext == ".mp4" || ext == ".h264" +} + +func writeFFmpegConcatList(path string, videoFiles []string) error { + var builder strings.Builder + for _, videoFile := range videoFiles { + escaped := strings.ReplaceAll(videoFile, "'", "'\\''") + builder.WriteString("file '") + builder.WriteString(escaped) + builder.WriteString("'\n") + } + return os.WriteFile(path, []byte(builder.String()), 0644) +} + +func allH264Files(videoFiles []string) bool { + for _, videoFile := range videoFiles { + if !strings.EqualFold(filepath.Ext(videoFile), ".h264") { + return false + } + } + return len(videoFiles) > 0 +} + +func concatenateFiles(outputPath string, inputPaths []string) (err error) { + output, err := os.Create(outputPath) + if err != nil { + return err + } + defer func() { + if closeErr := output.Close(); err == nil { + err = closeErr + } + if err != nil { + _ = os.Remove(outputPath) + } + }() + + for _, inputPath := range inputPaths { + input, openErr := os.Open(inputPath) + if openErr != nil { + return fmt.Errorf("open %s: %w", inputPath, openErr) + } + _, copyErr := io.Copy(output, input) + closeErr := input.Close() + if copyErr != nil { + return fmt.Errorf("append %s: %w", inputPath, copyErr) + } + if closeErr != nil { + return fmt.Errorf("close %s: %w", inputPath, closeErr) + } + } + return output.Sync() +} + +func ffmpegCleanEnv() []string { + env := os.Environ() + cleaned := make([]string, 0, len(env)) + for _, item := range env { + if strings.HasPrefix(item, "LD_LIBRARY_PATH=") { + continue + } + cleaned = append(cleaned, item) + } + return cleaned +} + func findClosest(candidates []struct { path string ts int64 @@ -238,134 +358,182 @@ func abs(x int64) int64 { return x } -// copyFile 支持跨分区复制,带超时机制 func copyFile(src, dst string) error { startTime := time.Now() - logger.Logger.Printf("[%.3f] 开始复制文件: %s → %s", time.Since(startTime).Seconds(), src, dst) - - // 创建带超时的上下文 - ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - defer cancel() - - // 创建通道用于接收复制结果 - type copyResult struct { - n int64 - err error + logger.Logger.Printf("[%.3f] Start file copy: %s -> %s", time.Since(startTime).Seconds(), src, dst) + + in, err := os.Open(src) + if err != nil { + return err } - resultChan := make(chan copyResult, 1) - - // 在goroutine中执行复制操作 - go func() { - // 打开源文件 - logger.Logger.Printf("[%.3f] 打开源文件: %s", time.Since(startTime).Seconds(), src) - in, err := os.Open(src) - if err != nil { - resultChan <- copyResult{0, err} - return - } - defer in.Close() + defer in.Close() - // 创建目标文件 - logger.Logger.Printf("[%.3f] 创建目标文件: %s", time.Since(startTime).Seconds(), dst) - out, err := os.Create(dst) - if err != nil { - resultChan <- copyResult{0, err} - return - } - defer out.Close() - - // 获取源文件大小 - fileInfo, err := in.Stat() - if err != nil { - logger.Logger.Printf("[%.3f] 获取源文件信息失败: %v", time.Since(startTime).Seconds(), err) - } else { - logger.Logger.Printf("[%.3f] 源文件大小: %d 字节", time.Since(startTime).Seconds(), fileInfo.Size()) - } - - // 执行复制 - logger.Logger.Printf("[%.3f] 开始执行文件复制操作", time.Since(startTime).Seconds()) - copyStartTime := time.Now() - n, err := io.Copy(out, in) - elapsed := time.Since(copyStartTime) - if err != nil { - logger.Logger.Printf("[%.3f] 文件复制失败: %v, 已复制 %d 字节, 耗时 %v", time.Since(startTime).Seconds(), err, n, elapsed) - resultChan <- copyResult{n, err} - return - } - logger.Logger.Printf("[%.3f] 文件复制成功: 复制 %d 字节, 耗时 %v", time.Since(startTime).Seconds(), n, elapsed) - - // 同步文件到磁盘 - logger.Logger.Printf("[%.3f] 开始同步文件到磁盘", time.Since(startTime).Seconds()) - syncStartTime := time.Now() - if err = out.Sync(); err != nil { - logger.Logger.Printf("[%.3f] 文件同步失败: %v, 耗时 %v", time.Since(startTime).Seconds(), err, time.Since(syncStartTime)) - resultChan <- copyResult{n, err} - return - } - logger.Logger.Printf("[%.3f] 文件同步成功, 耗时 %v", time.Since(startTime).Seconds(), time.Since(syncStartTime)) - - resultChan <- copyResult{n, nil} - }() - - // 等待复制结果或超时 - select { - case result := <-resultChan: - if result.err != nil { - return result.err - } - case <-ctx.Done(): - err := ctx.Err() - logger.Logger.Printf("[%.3f] 文件复制操作超时: %v", time.Since(startTime).Seconds(), err) - return fmt.Errorf("copy file timeout: %v", err) + out, err := os.Create(dst) + if err != nil { + return err } - - logger.Logger.Printf("[%.3f] 文件复制操作完成", time.Since(startTime).Seconds()) + defer out.Close() + + deadline := time.Now().Add(30 * time.Second) + buf := make([]byte, 1024*1024) + var written int64 + for { + if time.Now().After(deadline) { + _ = out.Close() + _ = os.Remove(dst) + logger.Logger.Printf("[%.3f] Copy timed out, removed incomplete file: %s", time.Since(startTime).Seconds(), dst) + return fmt.Errorf("copy file timeout after %d bytes", written) + } + + nr, readErr := in.Read(buf) + if nr > 0 { + nw, writeErr := out.Write(buf[:nr]) + written += int64(nw) + if writeErr != nil { + _ = os.Remove(dst) + return writeErr + } + if nw != nr { + _ = os.Remove(dst) + return io.ErrShortWrite + } + } + if readErr == io.EOF { + break + } + if readErr != nil { + _ = os.Remove(dst) + return readErr + } + } + + if err := out.Sync(); err != nil { + _ = os.Remove(dst) + return err + } + + logger.Logger.Printf("[%.3f] file copy completed: %d bytes", time.Since(startTime).Seconds(), written) return nil } -// alignTo10Seconds 将时间对齐到最近的10秒间隔 +/* + +func copyFile(src, dst string) error { + startTime := time.Now() + logger.Logger.Printf("[%.3f] Start file copy: %s -> %s", time.Since(startTime).Seconds(), src, dst) + + in, err := os.Open(src) + if err != nil { + return err + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return err + } + defer out.Close() + + deadline := time.Now().Add(30 * time.Second) + buf := make([]byte, 1024*1024) + var written int64 + for { + if time.Now().After(deadline) { + _ = out.Close() + _ = os.Remove(dst) + logger.Logger.Printf("[%.3f] Copy timed out, removed incomplete file: %s", time.Since(startTime).Seconds(), dst) + return fmt.Errorf("copy file timeout after %d bytes", written) + } + + nr, readErr := in.Read(buf) + if nr > 0 { + nw, writeErr := out.Write(buf[:nr]) + written += int64(nw) + if writeErr != nil { + _ = os.Remove(dst) + return writeErr + } + if nw != nr { + _ = os.Remove(dst) + return io.ErrShortWrite + } + } + if readErr == io.EOF { + break + } + if readErr != nil { + _ = os.Remove(dst) + return readErr + } + } + + if err := out.Sync(); err != nil { + _ = os.Remove(dst) + return err + } + + logger.Logger.Printf("[%.3f] file copy completed: %d bytes", time.Since(startTime).Seconds(), written) + return nil +} + +// alignTo10Seconds 鐏忓棙妞傞梻鏉戭嚠姒绘劕鍩岄張鈧潻鎴犳畱10缁夋帡妫块梾?*/ func alignTo10Seconds(timestamp int64) int64 { t := time.Unix(timestamp, 0) - // 获取秒数并对齐到10秒 seconds := t.Second() alignedSeconds := (seconds / 10) * 10 - // 创建新的时间 alignedTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), alignedSeconds, 0, t.Location()) return alignedTime.Unix() } -// parseSimpleTimestamp 解析简单的时间戳文件名 func parseSimpleTimestamp(filename string) (int64, error) { nameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename)) if len(nameWithoutExt) != 14 { - + return 0, fmt.Errorf("invalid timestamp filename length: %s", filename) } + year, err := strconv.Atoi(nameWithoutExt[0:4]) - // 解析时间戳 - year, _ := strconv.Atoi(nameWithoutExt[0:4]) - month, _ := strconv.Atoi(nameWithoutExt[4:6]) - day, _ := strconv.Atoi(nameWithoutExt[6:8]) - hour, _ := strconv.Atoi(nameWithoutExt[8:10]) - minute, _ := strconv.Atoi(nameWithoutExt[10:12]) - second, _ := strconv.Atoi(nameWithoutExt[12:14]) + // 鐟欙絾鐎介弮鍫曟?閹? year, err := strconv.Atoi(nameWithoutExt[0:4]) + if err != nil { + return 0, fmt.Errorf("invalid year in filename %s: %w", filename, err) + } + month, err := strconv.Atoi(nameWithoutExt[4:6]) + if err != nil { + return 0, fmt.Errorf("invalid month in filename %s: %w", filename, err) + } + day, err := strconv.Atoi(nameWithoutExt[6:8]) + if err != nil { + return 0, fmt.Errorf("invalid day in filename %s: %w", filename, err) + } + hour, err := strconv.Atoi(nameWithoutExt[8:10]) + if err != nil { + return 0, fmt.Errorf("invalid hour in filename %s: %w", filename, err) + } + minute, err := strconv.Atoi(nameWithoutExt[10:12]) + if err != nil { + return 0, fmt.Errorf("invalid minute in filename %s: %w", filename, err) + } + second, err := strconv.Atoi(nameWithoutExt[12:14]) + if err != nil { + return 0, fmt.Errorf("invalid second in filename %s: %w", filename, err) + } t := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local) return t.Unix(), nil } -// getTimestampFromPath 从文件路径中提取时间戳 -func getTimestampFromPath(filePath string) int64 { +// getTimestampFromPath +func getTimestampFromPath(filePath string) (int64, error) { filename := filepath.Base(filePath) timestamp, err := parseSimpleTimestamp(filename) if err != nil { - return 0 + return 0, err } - return timestamp + return timestamp, nil } -// contains 检查切片是否包含某个元素 +// contains func contains(slice []string, item string) bool { for _, s := range slice { if s == item { @@ -375,7 +543,7 @@ func contains(slice []string, item string) bool { return false } -// CleanupOldVideos 清理旧的合并视频文件,保留最近的 10 个 +// CleanupOldVideos func CleanupOldVideos(deviceUUID string) { const maxFiles = 10 videoDir := "/data/upload" @@ -414,16 +582,15 @@ func CleanupOldVideos(deviceUUID string) { } } -// CleanupOldRawVideos 清理推理程序生成的原始10s视频,保留最近60条 +// CleanupOldRawVideos func CleanupOldRawVideos(deviceUUID string) { - // 清理主设备目录 + // 濞撳懐鎮婃稉鏄忣啎婢跺洨娲拌ぐ? cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID)) cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID)) - - // 清理火焰检测目录 + + // 濞撳懐鎮婇悘顐ゅ姍濡偓濞村娲拌ぐ? cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check")) cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check")) } -// cleanupRawVideosDirectory 清理单个目录中的原始视频文件 func cleanupRawVideosDirectory(rawDir string) { if _, err := os.Stat(rawDir); os.IsNotExist(err) { return @@ -443,7 +610,7 @@ func cleanupRawVideosDirectory(rawDir string) { } if len(videoFiles) <= 60 { - return // 不超过60条,不清理 + return // 娑撳秷绉存潻?0閺夆槄绱濇稉宥嗙閻? } } sort.Slice(videoFiles, func(i, j int) bool {