feat: update inference reporting and ffmpeg video merge

This commit is contained in:
gqc
2026-07-06 16:26:08 +08:00
parent 79490bd27b
commit 0842f555fd
10 changed files with 1430 additions and 990 deletions
Binary file not shown.
+9
View File
@@ -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 // ProcessDetectAreaForInference processes detection area data, converts to format required by inference program
func ProcessDetectAreaForInference(areas []Area) string { func ProcessDetectAreaForInference(areas []Area) string {
if len(areas) == 0 { if len(areas) == 0 {
+72 -98
View File
@@ -12,126 +12,100 @@ import (
) )
var ( var (
InferenceProcesses sync.Map // key: deviceUUID string, value: *exec.Cmd InferenceProcesses sync.Map // key: process UUID, value: *exec.Cmd
inferenceLastHeartbeat sync.Map // key: process UUID, value: time.Time
// 记录最后一次收到心跳的时间
inferenceLastHeartbeat sync.Map // key: deviceUUID string, value: time.Time
) )
// UpdateInferenceHeartbeat 每收到一帧 /video/post 就调用这个 // UpdateInferenceHeartbeat records the latest heartbeat time for one inference process.
func UpdateInferenceHeartbeat(deviceUUID string) { func UpdateInferenceHeartbeat(processUUID string) {
inferenceLastHeartbeat.Store(deviceUUID, time.Now()) inferenceLastHeartbeat.Store(processUUID, time.Now())
// 输出心跳日志 if strings.Contains(processUUID, "_fire_check") {
if strings.Contains(deviceUUID, "_fire_check") { logger.Logger.Printf("推理心跳收到 | 时间: %s | UUID: %s | 类型: fire_check", time.Now().Format("2006/01/02 15:04:05.000"), processUUID)
logger.Logger.Printf("推理心跳收到 | 时间: %s | UUID: %s | 类型: fire_check", time.Now().Format("2006/01/02 15:04:05.000"), deviceUUID)
} else { } 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 判断是否需要重启推理程序 // ShouldRestartInference reports whether one inference process should be restarted.
func ShouldRestartInference(deviceUUID string) bool { func ShouldRestartInference(processUUID string) bool {
val, ok := inferenceLastHeartbeat.Load(deviceUUID) val, ok := inferenceLastHeartbeat.Load(processUUID)
if !ok { if !ok {
return true // 从未收到过心跳,肯定要启动 return true
} }
last := val.(time.Time) last := val.(time.Time)
return time.Since(last) > 85*time.Second return time.Since(last) > 85*time.Second
} }
// StopInferenceProcess 停止推理进程(供 main 和 DeviceManager 调用) // StopInferenceProcess stops one inference process.
func StopInferenceProcess(deviceUUID string) { func StopInferenceProcess(processUUID string) {
if raw, ok := InferenceProcesses.Load(deviceUUID); ok { if raw, ok := InferenceProcesses.Load(processUUID); ok {
if cmd, ok := raw.(*exec.Cmd); ok && cmd.Process != nil { if cmd, ok := raw.(*exec.Cmd); ok && cmd.Process != nil {
logger.Logger.Printf("正在停止推理进程 (DeviceUUID=%s, PID=%d)", deviceUUID, cmd.Process.Pid) logger.Logger.Printf("正在停止推理进程 (ProcessUUID=%s, PID=%d)", processUUID, cmd.Process.Pid)
cmd.Process.Kill() _ = cmd.Process.Kill()
cmd.Wait() _ = cmd.Wait()
} }
InferenceProcesses.Delete(deviceUUID) InferenceProcesses.Delete(processUUID)
} }
} }
// StartInferenceProcess 重启推理程序(保持原端口!) // StartInferenceProcess keeps the old "restart both" behavior for compatibility.
func StartInferenceProcess(deviceData *DeviceData) { func StartInferenceProcess(deviceData *DeviceData) {
if err := RestartInferenceProcess(deviceData, deviceData.DeviceUUID); err != nil {
// 重启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 {
logger.Logger.Printf("fire_leave推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) logger.Logger.Printf("fire_leave推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
} }
if err := RestartInferenceProcess(deviceData, fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)); err != nil {
// 保存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 {
logger.Logger.Printf("fire_check推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) 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进程对象 port, err := GlobalPortManager.GetPort(processUUID)
InferenceProcesses.Store(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), inferenceCmdFireCheck) if err != nil {
logger.Logger.Printf("fire_check推理程序已成功重启,PID=%d (DeviceUUID=%s, Port=%d)", return fmt.Errorf("get port failed: %w", err)
inferenceCmdFireCheck.Process.Pid, deviceData.DeviceUUID, portFireCheck) }
// 重启后立即刷新一次心跳,防止 30s 检测又触发 modelName := "model"
UpdateInferenceHeartbeat(deviceData.DeviceUUID) detectAreaStr := ProcessDetectAreaForInference(deviceData.FireLeaveDetectArea)
UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)) 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
} }
+54 -14
View File
@@ -132,6 +132,7 @@ type WSClient struct {
subscribers map[string]func([]byte) subscribers map[string]func([]byte)
closeChan chan struct{} closeChan chan struct{}
reconnectFlag bool reconnectFlag bool
reconnecting bool
} }
// NewWSClient creates WebSocket client // NewWSClient creates WebSocket client
@@ -185,7 +186,6 @@ func InitWSChannel() error {
} }
isConnecting = true isConnecting = true
defer func() { isConnecting = false }()
logger.Logger.Printf("Starting WebSocket connection initialization: %s", wsURL) logger.Logger.Printf("Starting WebSocket connection initialization: %s", wsURL)
@@ -194,15 +194,21 @@ func InitWSChannel() error {
client, err := NewWSClient(wsURL) client, err := NewWSClient(wsURL)
if err != nil { if err != nil {
logger.Logger.Printf("Failed to create WebSocket client: %v", err) logger.Logger.Printf("Failed to create WebSocket client: %v", err)
wsMutex.Lock()
isConnecting = false
wsMutex.Unlock()
return return
} }
wsMutex.Lock()
defer wsMutex.Unlock()
if ConfigUpdateChan == nil { if ConfigUpdateChan == nil {
ConfigUpdateChan = make(chan DeviceData, 100) ConfigUpdateChan = make(chan DeviceData, 100)
} }
// Note: No longer calling LoadDeviceData() here to avoid duplicate loading // Note: No longer calling LoadDeviceData() here to avoid duplicate loading
wsClient = client wsClient = client
isConnecting = false
logger.Logger.Printf("WebSocket connection initialized successfully (asynchronous)") 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 // RegisterService service registration - modified: now requires providing own server_id
func RegisterService(containerName string) (string, error) { func RegisterService(containerName string) (string, error) {
if wsClient == nil { client := GetWSClient()
if client == nil {
return "", fmt.Errorf("WebSocket client not initialized") 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) 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 { if err != nil {
logger.Logger.Printf("Service registration failed: %v", err) logger.Logger.Printf("Service registration failed: %v", err)
return "", err return "", err
@@ -258,21 +265,24 @@ func RegisterService(containerName string) (string, error) {
return "", fmt.Errorf("Service registration failed: Server returned abnormal response") 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) logger.Logger.Printf("Service registration successful: ServiceID=%s", serviceID)
return serviceID, nil return serviceID, nil
} }
// SubscribeTopic topic subscription // SubscribeTopic topic subscription
func SubscribeTopic(topic string, callback func([]byte)) error { func SubscribeTopic(topic string, callback func([]byte)) error {
if wsClient == nil { client := GetWSClient()
if client == nil {
return fmt.Errorf("WebSocket client not initialized") return fmt.Errorf("WebSocket client not initialized")
} }
params := SubscribeParams{ params := SubscribeParams{
Topic: topic, Topic: topic,
} }
logger.Logger.Printf("Starting topic subscription: Topic=%s", 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 { if err != nil {
logger.Logger.Printf("Topic subscription failed: %v", err) logger.Logger.Printf("Topic subscription failed: %v", err)
return 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) 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) return fmt.Errorf("Topic subscription error: %s", response.Error.Message)
} }
wsClient.mutex.Lock() client.mutex.Lock()
wsClient.subscribers[topic] = callback client.subscribers[topic] = callback
wsClient.mutex.Unlock() client.mutex.Unlock()
logger.Logger.Printf("Topic subscription successful: Topic=%s", topic) logger.Logger.Printf("Topic subscription successful: Topic=%s", topic)
return nil return nil
} }
@@ -344,7 +354,7 @@ func (c *WSClient) sendBinaryFrame(frameType byte, payload []byte) error {
if err != nil { if err != nil {
logger.Logger.Printf("WebSocket message sending failed: %v", err) logger.Logger.Printf("WebSocket message sending failed: %v", err)
c.connected = false c.connected = false
go c.reconnect() c.startReconnectLocked()
return fmt.Errorf("Message sending failed: %v", err) return fmt.Errorf("Message sending failed: %v", err)
} }
return nil return nil
@@ -431,7 +441,7 @@ func (c *WSClient) readLoop() {
c.mutex.Lock() c.mutex.Lock()
c.connected = false c.connected = false
c.mutex.Unlock() c.mutex.Unlock()
go c.reconnect() // Trigger reconnection c.startReconnect() // Trigger reconnection
return return
} }
@@ -569,7 +579,7 @@ func (c *WSClient) heartbeatLoop() {
c.mutex.Lock() c.mutex.Lock()
c.connected = false c.connected = false
c.mutex.Unlock() c.mutex.Unlock()
go c.reconnect() c.startReconnect()
} else { } else {
logger.Logger.Printf("Ping sent successfully") logger.Logger.Printf("Ping sent successfully")
} }
@@ -596,7 +606,13 @@ func (c *WSClient) reRegisterAndResubscribe() {
logger.Logger.Printf("Re-registration successful: ServiceID=%s", serviceID) logger.Logger.Printf("Re-registration successful: ServiceID=%s", serviceID)
// 3. 重新订阅 // 3. 重新订阅
c.mutex.Lock()
subscribers := make(map[string]func([]byte), len(c.subscribers))
for topic, callback := range 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 { if err := SubscribeTopic(topic, callback); err != nil {
logger.Logger.Printf("Failed to re-subscribe topic %s: %v", topic, err) logger.Logger.Printf("Failed to re-subscribe topic %s: %v", topic, err)
} else { } 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 // connect/wschannel.go → reconnect complete fix
func (c *WSClient) reconnect() { 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 return
} }
@@ -636,12 +675,13 @@ func (c *WSClient) reconnect() {
// 重启读写循环 // 重启读写循环
go c.readLoop() go c.readLoop()
go c.heartbeatLoop()
return return
} }
logger.Logger.Printf("Reconnection failed after 12 attempts") logger.Logger.Printf("Reconnection failed after 12 attempts")
c.mutex.Lock()
c.reconnectFlag = false c.reconnectFlag = false
c.mutex.Unlock()
} }
func (d *DeviceData) ToInferenceParams(port int) InferenceParams { func (d *DeviceData) ToInferenceParams(port int) InferenceParams {
return InferenceParams{ return InferenceParams{
@@ -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:<value>` 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
```
+241
View File
@@ -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` 已基于修复后的代码构建完成,可用于后续部署验证。
+113 -45
View File
@@ -2,11 +2,9 @@ package httpreader
import ( import (
"FireLeave_tool/connect" "FireLeave_tool/connect"
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"net/http" "net/http"
"os" "os"
"sync" "sync"
@@ -17,14 +15,31 @@ import (
// InferenceResult 推理结果 // InferenceResult 推理结果
type InferenceResult struct { type InferenceResult struct {
Serial string `json:"serial"` MessageType string `json:"message_type"`
At int64 `json:"at"` Serial string `json:"serial"`
Type int `json:"type"` At int64 `json:"at"`
Params []struct { Type int `json:"type"`
ClassIdx int `json:"class_idx"` Params []InferenceParam `json:"params"`
Name string `json:"name"` }
Number int `json:"number"`
} `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 推理数据接收服务器 // InferenceServer 推理数据接收服务器
@@ -35,6 +50,8 @@ type InferenceServer struct {
dataChan chan InferenceResult dataChan chan InferenceResult
mu sync.RWMutex mu sync.RWMutex
running bool running bool
statsMu sync.Mutex
stats inferenceStats
} }
// NewInferenceServer 创建推理数据接收服务器 // NewInferenceServer 创建推理数据接收服务器
@@ -58,6 +75,7 @@ func (is *InferenceServer) Start() error {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/video/post", is.handleVideoPost) mux.HandleFunc("/video/post", is.handleVideoPost)
mux.HandleFunc("/heartbeat", is.handleHeartbeat)
mux.HandleFunc("/health", is.handleHealthCheck) mux.HandleFunc("/health", is.handleHealthCheck)
is.server = &http.Server{ is.server = &http.Server{
@@ -91,41 +109,23 @@ func (is *InferenceServer) handleVideoPost(w http.ResponseWriter, r *http.Reques
return 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 var result InferenceResult
if err := json.NewDecoder(r.Body).Decode(&result); err != nil { 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) http.Error(w, `{"error":"Invalid JSON"}`, http.StatusBadRequest)
return return
} }
// 验证必要字段 if result.MessageType != "inference_result" {
if result.Serial == "" { logger.Logger.Printf("Inference result validation failed: message_type=%q (DeviceUUID=%s)", result.MessageType, is.deviceUUID)
logger.Logger.Printf("Data validation failed: Serial is empty") http.Error(w, `{"error":"Invalid message_type"}`, http.StatusBadRequest)
http.Error(w, `{"error":"Missing serial field"}`, http.StatusBadRequest)
return 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))
// 这一行就是你缺失的救命稻草! if result.Serial == "" {
connect.UpdateInferenceHeartbeat(is.deviceUUID) logger.Logger.Printf("Inference result validation failed: serial is empty (DeviceUUID=%s)", is.deviceUUID)
http.Error(w, `{"error":"Missing serial field"}`, http.StatusBadRequest)
// 额外写一份永不丢失的独立心跳日志 return
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))
} }
// 发送到处理通道 // 发送到处理通道
@@ -135,13 +135,89 @@ func (is *InferenceServer) handleVideoPost(w http.ResponseWriter, r *http.Reques
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok", "message":"data received"}`)) w.Write([]byte(`{"status":"ok", "message":"data received"}`))
// 使用 startTime 记录处理时间 // 使用 startTime 记录处理时间
logger.Logger.Printf("Inference data processed in %v", time.Since(startTime)) is.recordInferenceProcessed(time.Since(startTime))
default: default:
logger.Logger.Printf("Data channel full, discard data") logger.Logger.Printf("Data channel full, discard data")
http.Error(w, `{"error":"Channel full"}`, http.StatusServiceUnavailable) 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) { func (is *InferenceServer) handleHealthCheck(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") 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) 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
}
+232 -198
View File
@@ -27,32 +27,33 @@ import (
) )
type deviceAlarmState struct { type deviceAlarmState struct {
InCondition bool InCondition bool
StartTime int64 StartTime int64
AlarmPauseUntil int64 AlarmPauseUntil int64
FireLeaveForbidUntil int64 FireLeaveForbidUntil int64
// fire_check火焰检测状态 FireLeaveDisabled bool
FireCheckInCondition bool FireCheckCandidate bool
FireCheckStartTime int64 FireCheckFirstSeen time.Time
FireCheckLastSeen time.Time
FireCheckHasResult bool
FireCheckLastNumber int
FireCheckWaitingClear bool
} }
// 全局 map:每个设备独立状态
var deviceStates sync.Map // map[string]*deviceAlarmState var deviceStates sync.Map // map[string]*deviceAlarmState
var mergingAlarms sync.Map var mergingAlarms sync.Map
// 全局上报管理器
var GlobalReporter = reporter.NewReporter() var GlobalReporter = reporter.NewReporter()
const defaultFireLeaveForbidDuration = 15 * time.Minute const defaultFireLeaveForbidDuration = 15 * time.Minute
const fireCheckLastSeenGrace = time.Second
const buildVersion = "2026-05-13-logger-init-fix" const buildVersion = "2026-05-13-logger-init-fix"
// DeviceManager 设备管理器
type DeviceManager struct { type DeviceManager struct {
mu sync.Mutex mu sync.Mutex
devices map[string]*connect.DeviceData devices map[string]*connect.DeviceData
} }
// NewDeviceManager 初始化设备管理器
func NewDeviceManager() *DeviceManager { func NewDeviceManager() *DeviceManager {
return &DeviceManager{ return &DeviceManager{
devices: make(map[string]*connect.DeviceData), 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) 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) { func (dm *DeviceManager) AddDevice(deviceData connect.DeviceData) {
dm.mu.Lock() dm.mu.Lock()
defer dm.mu.Unlock() defer dm.mu.Unlock()
@@ -140,7 +140,6 @@ func (dm *DeviceManager) AddDevice(deviceData connect.DeviceData) {
logger.Logger.Printf("add equipments: DeviceUUID=%s", deviceData.DeviceUUID) logger.Logger.Printf("add equipments: DeviceUUID=%s", deviceData.DeviceUUID)
} }
// StopDevice 停止设备
func (dm *DeviceManager) StopDevice(deviceUUID string) { func (dm *DeviceManager) StopDevice(deviceUUID string) {
dm.mu.Lock() dm.mu.Lock()
defer dm.mu.Unlock() defer dm.mu.Unlock()
@@ -150,7 +149,6 @@ func (dm *DeviceManager) StopDevice(deviceUUID string) {
} }
} }
// GetDevice 获取设备
func (dm *DeviceManager) GetDevice(deviceUUID string) (*connect.DeviceData, bool) { func (dm *DeviceManager) GetDevice(deviceUUID string) (*connect.DeviceData, bool) {
dm.mu.Lock() dm.mu.Lock()
defer dm.mu.Unlock() defer dm.mu.Unlock()
@@ -158,10 +156,8 @@ func (dm *DeviceManager) GetDevice(deviceUUID string) (*connect.DeviceData, bool
return device, exists return device, exists
} }
// GlobalDeviceManager 全局设备管理器
var GlobalDeviceManager = NewDeviceManager() var GlobalDeviceManager = NewDeviceManager()
// 工具函数
func waitForHTTPService(port int, timeout time.Duration) bool { func waitForHTTPService(port int, timeout time.Duration) bool {
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
address := fmt.Sprintf("127.0.0.1:%d", port) 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) { for time.Now().Before(deadline) {
attempt++ attempt++
// 1. 先检查TCP连接
conn, err := net.DialTimeout("tcp", address, 2*time.Second) conn, err := net.DialTimeout("tcp", address, 2*time.Second)
if err != nil { if err != nil {
if attempt == 1 || attempt%5 == 0 { if attempt == 1 || attempt%5 == 0 {
@@ -183,7 +178,6 @@ func waitForHTTPService(port int, timeout time.Duration) bool {
} }
conn.Close() conn.Close()
// 2. 再检查HTTP服务是否真正就绪
client := &http.Client{Timeout: 3 * time.Second} client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(fmt.Sprintf("http://%s/health", address)) resp, err := client.Get(fmt.Sprintf("http://%s/health", address))
if err == nil { if err == nil {
@@ -204,7 +198,6 @@ func waitForHTTPService(port int, timeout time.Duration) bool {
return false return false
} }
// waitForTCPService 增强版本 - 支持更长的等待时间和更好的错误处理
func waitForTCPService(port int, timeout time.Duration) bool { func waitForTCPService(port int, timeout time.Duration) bool {
deadline := time.Now().Add(timeout) deadline := time.Now().Add(timeout)
address := fmt.Sprintf("127.0.0.1:%d", port) 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) logger.Logger.Printf("Wait for TCP service... (Port=%d, Number of attempts=%d, Error: %v)", port, attempt, err)
lastError = currentError lastError = currentError
} else if attempt%5 == 0 { } else if attempt%5 == 0 {
// 每5次尝试记录一次相同的错误
logger.Logger.Printf("Wait for TCP service... (Port=%d, Number of attempts=%d, Error: %v)", port, attempt, err) 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) 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 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) { func processDevice(ctx context.Context, deviceData connect.DeviceData) {
logger.Logger.Printf("Start processing the equipment: DeviceUUID=%s", deviceData.DeviceUUID) logger.Logger.Printf("Start processing the equipment: DeviceUUID=%s", deviceData.DeviceUUID)
debugDeviceData(deviceData) debugDeviceData(deviceData)
// 首先将设备添加到设备管理器
GlobalDeviceManager.AddDevice(deviceData) GlobalDeviceManager.AddDevice(deviceData)
restoreFireLeaveForbidWindow(deviceData.DeviceUUID) restoreFireLeaveForbidWindow(deviceData.DeviceUUID)
defer GlobalDeviceManager.StopDevice(deviceData.DeviceUUID) defer GlobalDeviceManager.StopDevice(deviceData.DeviceUUID)
// 创建带取消的子上下文
deviceCtx, cancel := context.WithCancel(ctx) deviceCtx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
// 分配端口 - fire_leave推理程序
portFireLeave, err := connect.GlobalPortManager.AllocatePort(deviceData.DeviceUUID) portFireLeave, err := connect.GlobalPortManager.AllocatePort(deviceData.DeviceUUID)
if err != nil { if err != nil {
logger.Logger.Printf(" DeviceUUID=%s Port allocation failed: %v", deviceData.DeviceUUID, err) 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) defer connect.GlobalPortManager.ReleasePort(deviceData.DeviceUUID)
// 分配端口 - fire_check推理程序
portFireCheck, err := connect.GlobalPortManager.AllocatePort(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)) portFireCheck, err := connect.GlobalPortManager.AllocatePort(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
if err != nil { if err != nil {
logger.Logger.Printf(" DeviceUUID=%s Fire check port allocation failed: %v", deviceData.DeviceUUID, err) 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) logger.Logger.Printf("Port allocation successful: DeviceUUID=%s, FireLeavePort=%d, FireCheckPort=%d", deviceData.DeviceUUID, portFireLeave, portFireCheck)
// 创建fire_leave推理数据接收服务器
inferenceServerFireLeave := httpreader.NewInferenceServer(deviceData.DeviceUUID, portFireLeave) inferenceServerFireLeave := httpreader.NewInferenceServer(deviceData.DeviceUUID, portFireLeave)
if err := inferenceServerFireLeave.Start(); err != nil { if err := inferenceServerFireLeave.Start(); err != nil {
logger.Logger.Printf("Failed to start fire_leave inference server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) logger.Logger.Printf("Failed to start fire_leave inference server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return return
} }
// 创建fire_check推理数据接收服务器
inferenceServerFireCheck := httpreader.NewInferenceServer(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), portFireCheck) inferenceServerFireCheck := httpreader.NewInferenceServer(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), portFireCheck)
if err := inferenceServerFireCheck.Start(); err != nil { if err := inferenceServerFireCheck.Start(); err != nil {
logger.Logger.Printf("Failed to start fire_check inference server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) logger.Logger.Printf("Failed to start fire_check inference server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return return
} }
// 使用带上下文的停止
defer inferenceServerFireLeave.StopWithContext(deviceCtx) defer inferenceServerFireLeave.StopWithContext(deviceCtx)
defer inferenceServerFireCheck.StopWithContext(deviceCtx) defer inferenceServerFireCheck.StopWithContext(deviceCtx)
// 等待HTTP服务器启动
if !waitForHTTPService(portFireLeave, 10*time.Second) { if !waitForHTTPService(portFireLeave, 10*time.Second) {
logger.Logger.Printf("Fire_leave inference server startup timeout (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, portFireLeave) logger.Logger.Printf("Fire_leave inference server startup timeout (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, portFireLeave)
return 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) logger.Logger.Printf("Inference servers started successfully: DeviceUUID=%s, FireLeavePort=%d, FireCheckPort=%d", deviceData.DeviceUUID, portFireLeave, portFireCheck)
// 处理检测区域为推理程序需要的格式
// 为fire_leave推理程序获取检测区域
fireLeaveDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireLeaveDetectArea) fireLeaveDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireLeaveDetectArea)
logger.Logger.Printf("The processed fire_leave detection area: %s", fireLeaveDetectAreaStr) logger.Logger.Printf("The processed fire_leave detection area: %s", fireLeaveDetectAreaStr)
// 为fire_check推理程序获取检测区域
fireCheckDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireCheckDetectArea) fireCheckDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireCheckDetectArea)
logger.Logger.Printf("The processed fire_check detection area: %s", fireCheckDetectAreaStr) logger.Logger.Printf("The processed fire_check detection area: %s", fireCheckDetectAreaStr)
// 启动fire_leave推理程序
inferenceCmdFireLeave := exec.Command( inferenceCmdFireLeave := exec.Command(
"./yolov5", "./yolov5",
"-s", deviceData.CameraRTSP, "-s", deviceData.CameraRTSP,
@@ -350,12 +339,12 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
return return
} }
// 保存fire_leave进程对象到全局 map // 婵犵數濮烽弫鎼佸磿閹寸姴绶ら柦妯侯棦濞差亝鏅滈柣鎰靛墮鎼村﹪姊虹粙璺ㄧ伇闁稿鍋ゅ畷鎴﹀Χ婢跺鍘撴繛鎾村嚬閸ㄥ崬鈻嶅▔涔猒leave闂傚倸鍊风粈渚€骞栭位鍥敃閿曗偓閻ょ偓绻濇繝鍌涘櫧闁活厽鐟╅弻鐔煎箲閹伴潧娈梺缁樻尰缁诲牓寮婚妶鍚ゅ湱鈧綆鍋呭鎺楁⒑缁嬫鍎愰柟鐟版喘楠炲啴鎸婃径鍡樼亖闂佸湱顭堢€涒晠寮抽悙鐑樷拻濞达綀妫勮闂佺粯顨嗗ú婊堝焵椤掍胶顣叉繝銏★耿閹儳鐣¢柇锔藉兊闁荤娀缂氬▍锝嗩殽閸ヮ剚鈷戦柛鎾瑰皺閸樻盯鏌涢悩宕囧閻庨潧鍚嬮幆鏃堝Ω閿旀儳骞楅梻濠庡亜濞诧箑煤閺嶃劎顩茬憸鐗堝笚閻?map
connect.InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmdFireLeave) connect.InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmdFireLeave)
logger.Logger.Printf("Fire_leave inference program started, PID=%d (DeviceUUID=%s)", inferenceCmdFireLeave.Process.Pid, deviceData.DeviceUUID) 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( inferenceCmdFireCheck := exec.Command(
"./yolov5", "./yolov5",
"-s", deviceData.CameraRTSP, "-s", deviceData.CameraRTSP,
@@ -363,8 +352,7 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
"-c", fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), "-c", fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID),
"-p", fmt.Sprintf("%d", portFireCheck), "-p", fmt.Sprintf("%d", portFireCheck),
"-w", fireCheckDetectAreaStr, "-w", fireCheckDetectAreaStr,
"-r", fmt.Sprintf("%d", deviceData.Confidence), "-r", fmt.Sprintf("%d", fireCheckConfidence),
"-R",
) )
inferenceCmdFireCheck.Stdout = os.Stdout inferenceCmdFireCheck.Stdout = os.Stdout
inferenceCmdFireCheck.Stderr = os.Stderr inferenceCmdFireCheck.Stderr = os.Stderr
@@ -376,19 +364,16 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
return return
} }
// 保存fire_check进程对象到全局 map // 婵犵數濮烽弫鎼佸磿閹寸姴绶ら柦妯侯棦濞差亝鏅滈柣鎰靛墮鎼村﹪姊虹粙璺ㄧ伇闁稿鍋ゅ畷鎴﹀Χ婢跺鍘撴繛鎾村嚬閸ㄥ崬鈻嶅▔涔猒check闂傚倸鍊风粈渚€骞栭位鍥敃閿曗偓閻ょ偓绻濇繝鍌涘櫧闁活厽鐟╅弻鐔煎箲閹伴潧娈梺缁樻尰缁诲牓寮婚妶鍚ゅ湱鈧綆鍋呭鎺楁⒑缁嬫鍎愰柟鐟版喘楠炲啴鎸婃径鍡樼亖闂佸湱顭堢€涒晠寮抽悙鐑樷拻濞达綀妫勮闂佺粯顨嗗ú婊堝焵椤掍胶顣叉繝銏★耿閹儳鐣¢柇锔藉兊闁荤娀缂氬▍锝嗩殽閸ヮ剚鈷戦柛鎾瑰皺閸樻盯鏌涢悩宕囧閻庨潧鍚嬮幆鏃堝Ω閿旀儳骞楅梻濠庡亜濞诧箑煤閺嶃劎顩茬憸鐗堝笚閻?map
connect.InferenceProcesses.Store(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), inferenceCmdFireCheck) 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) logger.Logger.Printf("Fire_check inference program started, PID=%d (DeviceUUID=%s)", inferenceCmdFireCheck.Process.Pid, deviceData.DeviceUUID)
// 初始化心跳
connect.UpdateInferenceHeartbeat(deviceData.DeviceUUID) connect.UpdateInferenceHeartbeat(deviceData.DeviceUUID)
connect.UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)) connect.UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
// 检查推理程序是否启动成功
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
// 检查fire_leave推理程序
if inferenceCmdFireLeave.Process == nil { if inferenceCmdFireLeave.Process == nil {
logger.Logger.Printf("Fire_leave inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID) logger.Logger.Printf("Fire_leave inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID)
return return
@@ -399,7 +384,6 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
return return
} }
// 检查fire_check推理程序
if inferenceCmdFireCheck.Process == nil { if inferenceCmdFireCheck.Process == nil {
logger.Logger.Printf("Fire_check inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID) logger.Logger.Printf("Fire_check inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID)
return 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) 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() { defer func() {
logger.Logger.Printf("Start to stop inference programs (DeviceUUID=%s)", deviceData.DeviceUUID) logger.Logger.Printf("Start to stop inference programs (DeviceUUID=%s)", deviceData.DeviceUUID)
// 停止fire_leave推理程序 if fireLeaveActive && inferenceCmdFireLeave.Process != nil {
if inferenceCmdFireLeave.Process != nil {
if err := inferenceCmdFireLeave.Process.Kill(); err != nil { if err := inferenceCmdFireLeave.Process.Kill(); err != nil {
logger.Logger.Printf("Fire_leave inference program termination failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) logger.Logger.Printf("Fire_leave inference program termination failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
} else { } else {
@@ -432,8 +441,7 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
} }
} }
// 停止fire_check推理程序 if fireCheckActive && inferenceCmdFireCheck.Process != nil {
if inferenceCmdFireCheck.Process != nil {
if err := inferenceCmdFireCheck.Process.Kill(); err != nil { if err := inferenceCmdFireCheck.Process.Kill(); err != nil {
logger.Logger.Printf("Fire_check inference program termination failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) logger.Logger.Printf("Fire_check inference program termination failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
} else { } else {
@@ -442,9 +450,6 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
} }
}() }()
// 启动温度程序
// 在 processDevice 函数中修改温度程序启动部分
// 启动温度程序
tempPort := portFireLeave + 1000 tempPort := portFireLeave + 1000
tempCmdArgs := []string{ tempCmdArgs := []string{
"-s", deviceData.CameraRTSP, "-s", deviceData.CameraRTSP,
@@ -460,65 +465,64 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
tempCmd.Stderr = os.Stderr tempCmd.Stderr = os.Stderr
logger.Logger.Printf("Start the temperature program command: %v", tempCmd.Args) logger.Logger.Printf("Start the temperature program command: %v", tempCmd.Args)
var tempClient *tcpreader.TCPClient
if err := tempCmd.Start(); err != nil { if err := tempCmd.Start(); err != nil {
logger.Logger.Printf("The temperature program failed to start (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) 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)
// 增强进程检查 if tempCmd.Process == nil || tempCmd.Process.Pid == 0 {
time.Sleep(3 * time.Second) // 减少等待时间 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("Wait for the temperature program to be initialized... (DeviceUUID=%s)", deviceData.DeviceUUID)
logger.Logger.Printf("The temperature program was not started correctly, and the process PID is 0 (DeviceUUID=%s)", deviceData.DeviceUUID) time.Sleep(2 * time.Second)
return
}
// 检查进程是否真的在运行 logger.Logger.Printf("Wait for the temperature program TCP server to start... (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, tempPort)
if err := tempCmd.Process.Signal(syscall.Signal(0)); err != nil { if !waitForTCPService(tempPort, 20*time.Second) {
logger.Logger.Printf("The temperature program process check failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err) logger.Logger.Printf("The TCP server of the temperature program has timed out during startup (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, tempPort)
return
}
logger.Logger.Printf("The temperature program has been successfully startedPID: %d (DeviceUUID=%s)", tempCmd.Process.Pid, deviceData.DeviceUUID) if tempCmd.Process != nil {
if err := tempCmd.Process.Signal(syscall.Signal(0)); err != nil {
// 增加TCP服务启动前的额外检查 logger.Logger.Printf("The temperature program has abnormally exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
logger.Logger.Printf("Wait for the temperature program to be initialized... (DeviceUUID=%s)", deviceData.DeviceUUID) } else {
time.Sleep(2 * time.Second) 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)
}
// 等待 TCP 服务器就绪 _ = tempCmd.Process.Kill()
logger.Logger.Printf("Wait for the temperature program TCP server to start... (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, tempPort) _ = tempCmd.Wait()
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) stopFireLeaveOnly("temperature TCP server startup timeout")
// 检查温度程序是否还在运行
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 { } 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 { if err := os.MkdirAll("/data/upload", 0755); err != nil {
logger.Logger.Printf("Failed to create the upload directory: %v", err) logger.Logger.Printf("Failed to create the upload directory: %v", err)
return return
} }
logger.Logger.Printf("The directory upload has been successfully created: /data/upload") logger.Logger.Printf("The directory upload has been successfully created: /data/upload")
// 主循环:从通道接收推理数据
logger.Logger.Printf("Start monitoring the equipment (DeviceUUID=%s)", deviceData.DeviceUUID) logger.Logger.Printf("Start monitoring the equipment (DeviceUUID=%s)", deviceData.DeviceUUID)
//var lastPersonCount = -1 //var lastPersonCount = -1
@@ -526,24 +530,31 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
monitorTicker := time.NewTicker(2 * time.Second) monitorTicker := time.NewTicker(2 * time.Second)
defer monitorTicker.Stop() defer monitorTicker.Stop()
fireLeaveDataChan := inferenceServerFireLeave.GetDataChan()
if !fireLeaveActive {
fireLeaveDataChan = nil
}
fireCheckDataChan := inferenceServerFireCheck.GetDataChan()
for { for {
select { select {
case <-deviceCtx.Done(): case <-deviceCtx.Done():
logger.Logger.Printf("Equipment processing has been cancelled: DeviceUUID=%s", deviceData.DeviceUUID) logger.Logger.Printf("Equipment processing has been cancelled: DeviceUUID=%s", deviceData.DeviceUUID)
// 强制停止所有子进程 if fireLeaveActive && inferenceCmdFireLeave.Process != nil {
if inferenceCmdFireLeave.Process != nil {
inferenceCmdFireLeave.Process.Kill() inferenceCmdFireLeave.Process.Kill()
} }
if inferenceCmdFireCheck.Process != nil { if fireCheckActive && inferenceCmdFireCheck.Process != nil {
inferenceCmdFireCheck.Process.Kill() inferenceCmdFireCheck.Process.Kill()
} }
if tempCmd.Process != nil { if tempCmd.Process != nil {
tempCmd.Process.Kill() tempCmd.Process.Kill()
} }
tempClient.Close() if tempClient != nil {
tempClient.Close()
}
return return
case inferenceResult, ok := <-inferenceServerFireLeave.GetDataChan(): case inferenceResult, ok := <-fireLeaveDataChan:
if !ok { if !ok {
logger.Logger.Printf("The fire_leave inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID) logger.Logger.Printf("The fire_leave inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID)
return 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) logger.Logger.Printf("Personnel quantity update: %d (DeviceUUID=%s)", currentPersonCount, deviceData.DeviceUUID)
} }
case fireCheckResult, ok := <-inferenceServerFireCheck.GetDataChan(): case fireCheckResult, ok := <-fireCheckDataChan:
if !ok { if !ok {
logger.Logger.Printf("The fire_check inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID) logger.Logger.Printf("The fire_check inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID)
return return
} }
// 处理fire_check推理结果
logger.Logger.Printf("Received fire_check inference data: DeviceUUID=%s", deviceData.DeviceUUID)
// 检查是否检测到火焰
fireDetected := false fireDetected := false
fireNumber := 0
for _, param := range fireCheckResult.Params { 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 fireDetected = true
logger.Logger.Printf("Fire detected! DeviceUUID=%s, Number=%d", deviceData.DeviceUUID, param.Number) fireNumber = param.Number
break break
} }
} }
// 处理火焰检测的连续时间判断
state := getDeviceState(deviceData.DeviceUUID) state := getDeviceState(deviceData.DeviceUUID)
now := time.Now().Unix() state.FireCheckHasResult = true
state.FireCheckLastNumber = fireNumber
// 解析detection_time配置
detectionSec := 2 // 默认2秒
if deviceData.FireCheckParam.DetectionTime != "" {
if sec, err := strconv.Atoi(deviceData.FireCheckParam.DetectionTime); err == nil && sec > 0 {
detectionSec = sec
}
}
if fireDetected { if fireDetected {
if !state.FireCheckInCondition { seenAt := time.Now()
// 开始计时 state.FireCheckLastSeen = seenAt
state.FireCheckStartTime = now if state.FireCheckWaitingClear {
state.FireCheckInCondition = true continue
logger.Logger.Printf("Fire detection started, timing for %ds: DeviceUUID=%s", detectionSec, deviceData.DeviceUUID)
} else if now-state.FireCheckStartTime >= int64(detectionSec) {
// 连续检测到火焰达到指定时间,触发上报
logger.Logger.Printf("Fire detected continuously for %ds, triggering alarm: DeviceUUID=%s", detectionSec, deviceData.DeviceUUID)
go func() {
alarmTime := time.Now().Unix()
logger.Logger.Printf("[%.3f] FIRE DETECTED ALARM TRIGGERED (DeviceUUID=%s)", time.Since(time.Now()).Seconds(), deviceData.DeviceUUID)
// 调用专门的火焰检测上报函数
GlobalReporter.TriggerFireCheckAlarm(deviceData, alarmTime)
}()
// 重置状态
state.FireCheckInCondition = false
} }
} else { if !state.FireCheckCandidate {
// 没有检测到火焰,重置状态 state.FireCheckCandidate = true
if state.FireCheckInCondition { state.FireCheckFirstSeen = seenAt
logger.Logger.Printf("Fire detection interrupted: lasted %ds < %ds (DeviceUUID=%s)", now-state.FireCheckStartTime, detectionSec, deviceData.DeviceUUID) logger.Logger.Printf("Fire target detected, start timing: required=%v, number=%d, DeviceUUID=%s",
state.FireCheckInCondition = false 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: case <-monitorTicker.C:
now := time.Now().Unix() now := time.Now().Unix()
// === 获取当前设备独立状态 === // === 闂傚倸鍊搁崐椋庣矆娓氣偓瀹曘儳鈧綆鍠栫壕鍧楁煙閹増顥夐幖鏉戯躬閺屻倝鎳濋幍顔肩墯婵炲瓨绮岀紞濠囧蓟濞戙垹唯妞ゆ棁宕甸弳妤佺箾鐎涙鐭婄紓宥咃躬瀵鎮㈤悡搴g暰閻熸粌绉瑰铏綇閵婏絼绨婚梺闈涚墕閹冲繘宕甸崶顒佺厵妞ゆ梹顑欏鎰版煙瀹勭増鍤囬柟顔惧厴瀵爼宕归褎鍨舵穱濠囨倷椤忓嫧鍋撹閳ь剟娼ч惌鍌氱暦濠婂牊鍋勯柣鎾虫捣閻e搫顪冮妶鍛闁绘绮撳顐﹀幢濞戞瑧鍘遍柣蹇曞仜婢т粙鎯屾繝鍕=鐎广儱鎳庨顓熸叏婵犲懏顏犵紒杈ㄥ笒椤粓宕卞▎蹇撔梻鍌欒兌绾爼寮插鍫熷亱闁绘顕ч弰銉╂煃?===
state := getDeviceState(deviceData.DeviceUUID) 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() tempResult, err := tempClient.ReadTemperatureResult()
if err == nil { if err == nil {
if math.Abs(tempResult.Tmp-deviceData.Temperature) >= 0.5 { 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 deviceData.Temperature = tempResult.Tmp
connect.UpdateDeviceData(deviceData) connect.UpdateDeviceData(deviceData)
GlobalDeviceManager.AddDevice(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) oldTemp, tempResult.Tmp, deviceData.DeviceUUID)
} }
} }
// 获取参数
detectionSec := getIntFromNumber(deviceData.DetectionTime, 20) detectionSec := getIntFromNumber(deviceData.DetectionTime, 20)
alarmCooldown := getIntFromNumber(deviceData.AlarmTime, 300) alarmCooldown := getIntFromNumber(deviceData.AlarmTime, 300)
// === 防抖:无人 + 温度稳定超阈值 === // === 闂傚倸鍊搁崐鎼佸磹閹间礁纾归柟闂寸閻ゎ喗銇勯幇鈺佺労闁搞倖娲熼弻銈囩矙鐠恒劋绮甸梺鍛婄懃缁绘﹢寮诲☉銏╂晝闁绘ɑ褰冩慨鏇㈡⒑閸濆嫭濯奸柛鎾村哺楠炲牓濡搁妷顔藉缓闂佺硶鍓濋〃鍛不濞差亝鈷戦悹鎭掑妼閺嬪倿鏌涙惔銊ゆ喚婵犫偓?+ 濠电姷鏁告慨鐑藉极閹间礁纾婚柣鎰惈缁犱即鏌熺紒銏犳灈闁汇倗鍋撶换娑㈠箣閻愯尙鍔伴梺绋款儐閹瑰洭骞冮悜钘夌妞ゆ洖妫涚粈濠勭磽閸屾瑧璐伴柛鐘愁殜閹兘鍩℃担鐑樻濠殿喗銇涢崑鎾绘煙瀹曞洤鈻堟い銏☆殕閹峰懘宕烽鐔滄棃姊婚崒娆掑厡闁稿海鏁诲畷婊冣攽閸喐娈伴梺鍦劋椤ㄥ懐绮婚悩鍏呯箚妞ゆ牗鐟х粔鍧楁煥濠靛棭妲搁柣鎰攻閵囧嫰骞掗崱妞惧缂?===
noPerson := deviceData.PersonCount == 0 noPerson := deviceData.PersonCount == 0
// 温度防抖
var tempStableHigh bool var tempStableHigh bool
threshold := deviceData.TemperatureThreshold threshold := deviceData.TemperatureThreshold
@@ -667,7 +703,6 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
continue continue
} }
// 1. 冷却期检查
if now < state.AlarmPauseUntil { if now < state.AlarmPauseUntil {
if state.InCondition { if state.InCondition {
logger.Logger.Printf("Alarm in cooldown: %ds remaining (DeviceUUID=%s)", logger.Logger.Printf("Alarm in cooldown: %ds remaining (DeviceUUID=%s)",
@@ -677,50 +712,44 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
continue continue
} }
// 2. 条件满足
if conditionMet { if conditionMet {
if !state.InCondition { if !state.InCondition {
state.StartTime = now state.StartTime = now
state.InCondition = true 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) detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
} else if now-state.StartTime >= int64(detectionSec) { } else if now-state.StartTime >= int64(detectionSec) {
currentTime := time.Now() 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) currentTime.Unix(), detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
// 异步处理报警,避免阻塞主线程
go func() { go func() {
alarmStartTime := time.Now() 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) alarmStartTime.Unix(), deviceData.DeviceUUID, state.StartTime, alarmCooldown)
// 捕获所有异常,确保goroutine不会因为panic而崩溃
defer func() { defer func() {
if err := recover(); err != nil { 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) time.Now().Unix(), err, deviceData.DeviceUUID)
} }
}() }()
// 调用报警处理函数 // 闂傚倸鍊峰ù鍥х暦閸偅鍙忛柟缁㈠櫘閺佸嫰鏌涘☉娆愮稇闁汇値鍠栭湁闁稿繐鍚嬬紞鎴︽煛鐎b晝绐旈柡灞炬礋瀹曠厧鈹戦幇顓夛妇绱撴担鍝勵€岄柛锝忕秮瀵寮撮悢椋庣獮濠碘槅鍨抽崢褏鏁妷鈺傗拺闁告稑锕ラ悡銉╂煙閸愭煡鍙勯柍銉閹瑰嫰濡搁敃鈧壕顖炴⒑閸涘﹦绠撻悗姘舵敱鐎靛ジ鍩€椤掑嫭鈷掑ù锝囩摂閸ゅ啴鏌涢悩宕囧⒌闁轰礁鍟撮、鏃堝礋椤撶喐顔曟繝鐢靛仜濡瑩骞愭繝姘惞鐎广儱顦伴悡蹇擃熆閼哥數娲存俊缁㈠枟閵? GlobalReporter.TriggerAlarmWithDelay(deviceData, state.StartTime, alarmCooldown)
GlobalReporter.TriggerAlarmWithDelay(deviceData, state.StartTime, alarmCooldown) logger.Logger.Printf("[%d] Alarm handling completed (DeviceUUID=%s, elapsed=%.3fs)",
logger.Logger.Printf("[%d] 报警处理完成 (DeviceUUID=%s, 耗时:%.3fs)",
time.Now().Unix(), deviceData.DeviceUUID, time.Since(alarmStartTime).Seconds()) time.Now().Unix(), deviceData.DeviceUUID, time.Since(alarmStartTime).Seconds())
}() }()
// 更新状态
state.AlarmPauseUntil = now + int64(alarmCooldown) state.AlarmPauseUntil = now + int64(alarmCooldown)
state.InCondition = false 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) time.Now().Unix(), alarmCooldown, deviceData.DeviceUUID)
// 更新设备数据
deviceDataCopy := deviceData deviceDataCopy := deviceData
deviceDataCopy.AlarmPauseUntil = state.AlarmPauseUntil 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) 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 { } else {
if state.InCondition { if state.InCondition {
@@ -742,10 +771,8 @@ func waitForShutdown(ctx context.Context, cancel context.CancelFunc, wg *sync.Wa
case sig := <-stop: case sig := <-stop:
logger.Logger.Printf("Received stop signal: %v, start cleaning...", sig) logger.Logger.Printf("Received stop signal: %v, start cleaning...", sig)
// 立即取消上下文,通知所有goroutine停止 // 缂傚倸鍊搁崐鎼佸磹閻戣姤鍊块柨鏇炲€搁拑鐔兼煏婵炵偓娅撻柡浣稿閺屾稑鈽夐崡鐐茬闂佸搫妫庨崐婵嬪蓟濞戙垹鐒洪柛鎰典簴濡插牓姊哄ú璇插箺妞ゃ劌锕濠氬灳閹颁礁鎮戦梺鍛婂姂閸斿矂鈥栫€n喗鐓曢柣鏃堟敱閸熺偟绱掔紒妯兼创妤犵偛顑夐幃娆撳级閹寸媭鏆¢梻鍌欑劍鐎笛兠洪埡鍐ㄧ筏閻犳亽鍔岄崹婵囥亜閹惧崬鐏╃€瑰憡绻堥弻鈩冨緞鐎n亞浠稿銈冨劜缁诲牆顫忓ú顏勭闁绘劖褰冩慨锕€顪冮妶鍐ㄥ姕闁圭澧介崚鎺楀醇閻旈绐為梺褰掑亰閸樻悂骞忛崫鍕垫富闁靛牆妫楁慨褏绱掗弻銉х暫闁诡垰鍟撮獮瀣晜閻e苯骞楅梻浣烘嚀閻忔繄鈧凹鍠氬褔鍩€椤掆偓铻栭柣姗€娼ф禒婊呯磼婢跺﹦绉虹€规洘妞介崺鈧い鎺嶉檷娴滄粓鏌熼悜妯虹仴妞ゅ繆鏅犻幃妤€顫濋澶嬓﹂梺瀹狀潐閸ㄨ泛顕ラ崟顓濇勃闁规儳鎳愬绲﹐utine闂傚倸鍊搁崐鐑芥嚄閸洍鈧箓宕奸妷顔芥櫈闂佺硶鍓濋悷銉╁垂濠靛牃鍋撻獮鍨姎妞わ缚绮欏? cancel()
cancel()
// 等待所有goroutine退出
done := make(chan struct{}) done := make(chan struct{})
go func() { go func() {
wg.Wait() wg.Wait()
@@ -757,7 +784,6 @@ func waitForShutdown(ctx context.Context, cancel context.CancelFunc, wg *sync.Wa
logger.Logger.Printf("All goroutines exited normally") logger.Logger.Printf("All goroutines exited normally")
case <-time.After(15 * time.Second): case <-time.After(15 * time.Second):
logger.Logger.Printf("Waiting time exceeded, force exit") logger.Logger.Printf("Waiting time exceeded, force exit")
// 强制终止所有推理进程
connect.InferenceProcesses.Range(func(key, value interface{}) bool { connect.InferenceProcesses.Range(func(key, value interface{}) bool {
if cmd, ok := value.(*exec.Cmd); ok && cmd.Process != nil { if cmd, ok := value.(*exec.Cmd); ok && cmd.Process != nil {
logger.Logger.Printf("Force kill process: %v", key) logger.Logger.Printf("Force kill process: %v", key)
@@ -812,23 +838,22 @@ register:
logger.Logger.Printf("Service registration successful: ServiceID=%s", serviceID) logger.Logger.Printf("Service registration successful: ServiceID=%s", serviceID)
} }
// 更新GlobalReporter的WebSocket客户端
wsClient := connect.GetWSClient() wsClient := connect.GetWSClient()
GlobalReporter.SetWSClient(wsClient) GlobalReporter.SetWSClient(wsClient)
logger.Logger.Printf("Updated GlobalReporter with WebSocket client: %v", wsClient != nil) logger.Logger.Printf("Updated GlobalReporter with WebSocket client: %v", wsClient != nil)
// 新增:订阅本地告警 topic // 闂傚倸鍊搁崐椋庣矆娓氣偓楠炴牠顢曢敃鈧壕褰掓煟閻旂厧浜伴柣鏂挎閹便劌顪冪拠韫闁荤姳璀﹂崹鍫曞蓟濞戙垹绠涢梻鍫熺⊕閻忓秹姊洪崫鍕闁告挻宀搁獮鍫ュΩ閵夊海鍠撻幏鐘诲箵閹烘繃娴囬梻鍌欑閹诧繝鎳濇ィ鍐ㄧ9闁割煈鍣鏍р攽閻樺疇澹樼紒鈧崘顔界厪濠电姴绻掗悾杈ㄧ箾閸涱偄鈧洟鍩為幋锔藉亹妞ゆ劦婢€婢规洖鈹戦悙瀛樼稇闁告艾顑呴…鍥р枎閹惧磭顔夐梺鍓插亝濞叉﹢鎮″☉姗嗙唵閻犺桨璀﹂崕蹇斻亜閺囩喓鐭掗柡灞剧洴閹垽宕崟顏咁潟闂備浇顕栭崰鏍礊婵犲洤鏄ラ柍?topic
if err := connect.SubscribeTopic("/dhlr/alert", connect.HandleBuzzerAlert); err != nil { 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 { } 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 { 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 { } 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() defer logger.CloseLogger()
// 初始化上报管理器专用日志
if err := logger.InitReporterLogger(); err != nil { if err := logger.InitReporterLogger(); err != nil {
logger.Logger.Printf("Reporter logger initialization failed: %v", err) logger.Logger.Printf("Reporter logger initialization failed: %v", err)
} }
// 初始化GPIO监控专用日志
if err := logger.InitGPILogger(); err != nil { if err := logger.InitGPILogger(); err != nil {
logger.Logger.Printf("GPIO logger initialization failed: %v", err) logger.Logger.Printf("GPIO logger initialization failed: %v", err)
} }
@@ -920,14 +943,12 @@ func main() {
logger.Logger.Printf("Step 5: Start GPIO button monitoring") logger.Logger.Printf("Step 5: Start GPIO button monitoring")
go func() { go func() {
// 创建GPIO按钮监视器
monitor := gpio_monitor.NewGPIOButtonMonitor(gpio_monitor.ButtonGPIO) monitor := gpio_monitor.NewGPIOButtonMonitor(gpio_monitor.ButtonGPIO)
monitor.SetOnPress(func() { monitor.SetOnPress(func() {
minute := reporter.GetForbidTimeConfig() minute := reporter.GetForbidTimeConfig()
duration := reporter.ParseForbidDuration(minute, defaultFireLeaveForbidDuration) duration := reporter.ParseForbidDuration(minute, defaultFireLeaveForbidDuration)
persistAndApplyFireLeaveForbidWindow(minute, duration) persistAndApplyFireLeaveForbidWindow(minute, duration)
}) })
// 启动GPIO监控
if err := monitor.Start(ctx); err != nil { if err := monitor.Start(ctx); err != nil {
logger.Logger.Printf("Failed to start GPIO button monitor: %v", err) logger.Logger.Printf("Failed to start GPIO button monitor: %v", err)
} else { } else {
@@ -946,23 +967,20 @@ func main() {
defer ticker.Stop() defer ticker.Stop()
for range ticker.C { for range ticker.C {
GlobalDeviceManager.RangeDevices(func(uuid string, device *connect.DeviceData) bool { GlobalDeviceManager.RangeDevices(func(uuid string, device *connect.DeviceData) bool {
// 检查fire_leave推理程序心跳 state := getDeviceState(uuid)
if connect.ShouldRestartInference(uuid) { if !state.FireLeaveDisabled && connect.ShouldRestartInference(uuid) {
logger.Logger.Printf("fire_leave推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid) logger.Logger.Printf("fire_leave inference heartbeat timeout (>90s), restarting: DeviceUUID=%s", uuid)
connect.StopInferenceProcess(uuid) if err := connect.RestartInferenceProcess(device, uuid); err != nil {
time.Sleep(6 * time.Second) logger.Logger.Printf("fire_leave inference restart failed: DeviceUUID=%s, err=%v", uuid, err)
connect.StartInferenceProcess(device) }
connect.UpdateInferenceHeartbeat(uuid)
} }
// 检查fire_check推理程序心跳
fireCheckUUID := fmt.Sprintf("%s_fire_check", uuid) fireCheckUUID := fmt.Sprintf("%s_fire_check", uuid)
if connect.ShouldRestartInference(fireCheckUUID) { if connect.ShouldRestartInference(fireCheckUUID) {
logger.Logger.Printf("fire_check推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid) logger.Logger.Printf("fire_check inference heartbeat timeout (>90s), restarting: DeviceUUID=%s", uuid)
connect.StopInferenceProcess(fireCheckUUID) if err := connect.RestartInferenceProcess(device, fireCheckUUID); err != nil {
time.Sleep(6 * time.Second) logger.Logger.Printf("fire_check inference restart failed: DeviceUUID=%s, err=%v", uuid, err)
connect.StartInferenceProcess(device) }
connect.UpdateInferenceHeartbeat(fireCheckUUID)
} }
return true return true
}) })
@@ -992,19 +1010,43 @@ func printGlobalDeviceStatus() {
i := 1 i := 1
for uuid, data := range activeDevices { for uuid, data := range activeDevices {
state := getDeviceState(uuid) state := getDeviceState(uuid)
cooldownLeft := int64(0) cooldownLeft := int64(0)
if now < state.AlarmPauseUntil { if now < state.AlarmPauseUntil {
cooldownLeft = state.AlarmPauseUntil - now 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, i,
uuid, uuid,
data.PersonCount, fireValue,
data.Temperature, fireCandidate,
data.TemperatureThreshold, fireLastSeenAge,
!state.FireLeaveDisabled,
personValue,
tempValue,
thresholdValue,
cooldownLeft, cooldownLeft,
) )
i++ i++
@@ -1049,11 +1091,9 @@ func debugDeviceData(deviceData connect.DeviceData) {
logger.Logger.Printf(" Temperature: %f", deviceData.Temperature) logger.Logger.Printf(" Temperature: %f", deviceData.Temperature)
} }
// handleForbidTimeMessage 处理禁止时间配置消息
func handleForbidTimeMessage(message []byte) { func handleForbidTimeMessage(message []byte) {
logger.Logger.Printf("Received forbid time message: %s", string(message)) logger.Logger.Printf("Received forbid time message: %s", string(message))
// 解析消息
var msg struct { var msg struct {
UUID string `json:"uuid"` UUID string `json:"uuid"`
Topic string `json:"topic"` Topic string `json:"topic"`
@@ -1067,7 +1107,6 @@ func handleForbidTimeMessage(message []byte) {
return return
} }
// 解析content字段
var content struct { var content struct {
Minute string `json:"minute"` Minute string `json:"minute"`
FireLeaveTimeout string `json:"fire_leave_timeout,omitempty"` FireLeaveTimeout string `json:"fire_leave_timeout,omitempty"`
@@ -1079,19 +1118,15 @@ func handleForbidTimeMessage(message []byte) {
return return
} }
// 处理禁止时间配置
if content.Minute != "" { if content.Minute != "" {
logger.Logger.Printf("Forbid time config received: Minute=%s", content.Minute) logger.Logger.Printf("Forbid time config received: Minute=%s", content.Minute)
persistAndApplyFireLeaveForbidWindow(content.Minute, reporter.ParseForbidDuration(content.Minute, 0)) persistAndApplyFireLeaveForbidWindow(content.Minute, reporter.ParseForbidDuration(content.Minute, 0))
} }
// 处理事件周期时限配置
if content.FireLeaveTimeout != "" || content.FireCheckTimeout != "" { if content.FireLeaveTimeout != "" || content.FireCheckTimeout != "" {
// 解析时间
fireLeaveTimeout, err1 := time.ParseDuration(content.FireLeaveTimeout) fireLeaveTimeout, err1 := time.ParseDuration(content.FireLeaveTimeout)
fireCheckTimeout, err2 := time.ParseDuration(content.FireCheckTimeout) fireCheckTimeout, err2 := time.ParseDuration(content.FireCheckTimeout)
// 如果解析失败,使用默认值
if err1 != nil { if err1 != nil {
fireLeaveTimeout = 30 * time.Minute fireLeaveTimeout = 30 * time.Minute
logger.Logger.Printf("Invalid fire_leave_timeout, using default: 30m0s") 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) logger.Logger.Printf("Event cycle timeout config received: FireLeaveTimeout=%v, FireCheckTimeout=%v", fireLeaveTimeout, fireCheckTimeout)
// 保存到缓存文件
if err := reporter.SaveEventCycleTimeoutConfig(fireLeaveTimeout, fireCheckTimeout); err != nil { if err := reporter.SaveEventCycleTimeoutConfig(fireLeaveTimeout, fireCheckTimeout); err != nil {
logger.Logger.Printf("Failed to save event cycle timeout config: %v", err) logger.Logger.Printf("Failed to save event cycle timeout config: %v", err)
} else { } else {
+301 -460
View File
File diff suppressed because it is too large Load Diff
+342 -175
View File
@@ -1,7 +1,6 @@
package video_server package video_server
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"os" "os"
@@ -15,35 +14,45 @@ import (
"FireLeave_tool/logger" "FireLeave_tool/logger"
) )
// MergeVideo 合并视频文件 - 完全适配 mp4_merge --out 模式 // MergeVideo merges nearby raw video clips with ffmpeg.
func MergeVideo(deviceUUID string, alarmTime int64) (string, error) { 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() startTime := time.Now()
logger.Logger.Printf("[%.3f] 开始合并视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime) logger.Logger.Printf("[%.3f] Start video merge (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
// 捕获所有异常,确保函数不会因为panic而崩溃 // Guard the merge path so one bad video cannot crash the process.
defer func() { defer func() {
if err := recover(); err != nil { if recovered := recover(); recovered != nil {
logger.Logger.Printf("[%.3f] 合并视频发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID) 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) 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) { 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) return "", fmt.Errorf("video directory not exist: %s", videoDir)
} else if err != nil { } 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) 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] Align alarm time to 10-second boundary (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
logger.Logger.Printf("[%.3f] 开始时间对齐处理 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
alarmTimeAligned := alignTo10Seconds(alarmTime) alarmTimeAligned := alignTo10Seconds(alarmTime)
alarmTimeStr := time.Unix(alarmTimeAligned, 0).Format("20060102150405") 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 beforeTime := alarmTimeAligned - 10
afterTime := alarmTimeAligned + 10 afterTime := alarmTimeAligned + 10
@@ -51,40 +60,37 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
beforeStr := time.Unix(beforeTime, 0).Format("20060102150405") beforeStr := time.Unix(beforeTime, 0).Format("20060102150405")
afterStr := time.Unix(afterTime, 0).Format("20060102150405") afterStr := time.Unix(afterTime, 0).Format("20060102150405")
paths := map[string]string{ targetNames := []string{beforeStr, alarmTimeStr, afterStr}
beforeStr: filepath.Join(videoDir, beforeStr+".mp4"),
alarmTimeStr: filepath.Join(videoDir, alarmTimeStr+".mp4"),
afterStr: filepath.Join(videoDir, afterStr+".mp4"),
}
// 查找视频文件
videoFiles := []string{} videoFiles := []string{}
logger.Logger.Printf("[%.3f] 开始查找视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID) logger.Logger.Printf("[%.3f] Search expected video files (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
for name, path := range paths { missingVideos := []string{}
logger.Logger.Printf("[%.3f] 检查文件: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), path, deviceUUID) for _, name := range targetNames {
if _, err := os.Stat(path); err == nil { if path, ok := findVideoFileForTimestamp(videoDir, name); ok {
videoFiles = append(videoFiles, path) 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 { } 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] Found %d expected video files, searching fallbacks if needed (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
logger.Logger.Printf("[%.3f] 当前找到 %d 个视频文件,开始补充替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
if len(videoFiles) < 3 { 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) allFiles, err := os.ReadDir(videoDir)
if err != nil { 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 { } 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 { var candidates []struct {
path string path string
ts int64 ts int64
} }
for _, f := range allFiles { 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 { if ts, err := parseSimpleTimestamp(f.Name()); err == nil {
candidates = append(candidates, struct { candidates = append(candidates, struct {
path string 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} targets := []int64{beforeTime, alarmTimeAligned, afterTime}
for _, target := range targets { for _, target := range targets {
if containsTimestamp(videoFiles, target) { 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 { 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)) return "", fmt.Errorf("insufficient video files: %d", len(videoFiles))
} }
// === 3. 排序 === logger.Logger.Printf("[%.3f] Sort video files by timestamp (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] 开始排序视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
sort.Slice(videoFiles, func(i, j int) bool { 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) 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) 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) outputFile := fmt.Sprintf("%s.mp4_joined.mp4", alarmTimeStr)
joinedPath := filepath.Join(videoDir, outputFile) 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] Remove old output file if present (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] 清理旧的输出文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
if err := os.Remove(joinedPath); err != nil && !os.IsNotExist(err) { 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) 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 { } 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 := []string{"-hide_banner", "-loglevel", "error", "-y"}
args := append(videoFiles, "--out", joinedPath) if allH264Files(videoFiles) {
cmd := exec.Command("mp4_merge", args...) 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.Stdout = logger.Logger.Writer()
cmd.Stderr = 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() mergeStartTime := time.Now()
if err := cmd.Run(); err != nil { if err := cmd.Run(); err != nil {
logger.Logger.Printf("[%.3f] 视频合并命令执行失败 (耗时:%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), err, deviceUUID) 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("mp4_merge failed: %v", err) 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] Verify merged file: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
logger.Logger.Printf("[%.3f] 验证输出文件是否存在: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
if _, err := os.Stat(joinedPath); err != nil { 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) return "", fmt.Errorf("output file not created: %s, error: %v", joinedPath, err)
} }
info, err := os.Stat(joinedPath) info, err := os.Stat(joinedPath)
if err != nil { 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) 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) 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] Move merged file to /data/upload (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
logger.Logger.Printf("[%.3f] 开始移动文件到 /data/upload/ 目录 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
t := time.Unix(alarmTimeAligned, 0) t := time.Unix(alarmTimeAligned, 0)
year, month, day := t.Date() year, month, day := t.Date()
timestampPart := fmt.Sprintf("%d-%d-%d-%d", year, int(month), day, alarmTimeAligned) 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) 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 { 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) return "", fmt.Errorf("create /data/upload failed: %v", err)
} }
// 跨分区复制 + 删除 logger.Logger.Printf("[%.3f] Copy merged file: %s -> %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, finalPath, deviceUUID)
logger.Logger.Printf("[%.3f] 开始复制文件: %s → %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, finalPath, deviceUUID)
copyStartTime := time.Now() copyStartTime := time.Now()
if err := copyFile(joinedPath, finalPath); err != nil { 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) 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] Copy merged file succeeded (elapsed=%.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] Delete temp merged file: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
if err := os.Remove(joinedPath); err != nil { 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) logger.Logger.Printf("[%.3f] Failed to delete temp file %s: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID)
} else { } 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 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 { func containsTimestamp(files []string, ts int64) bool {
for _, f := range files { for _, f := range files {
if getTimestampFromPath(f) == ts { timestamp, err := getTimestampFromPath(f)
if err == nil && timestamp == ts {
return true return true
} }
} }
return false 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 { func findClosest(candidates []struct {
path string path string
ts int64 ts int64
@@ -238,134 +358,182 @@ func abs(x int64) int64 {
return x return x
} }
// copyFile 支持跨分区复制,带超时机制
func copyFile(src, dst string) error { func copyFile(src, dst string) error {
startTime := time.Now() startTime := time.Now()
logger.Logger.Printf("[%.3f] 开始复制文件: %s %s", time.Since(startTime).Seconds(), src, dst) logger.Logger.Printf("[%.3f] Start file copy: %s -> %s", time.Since(startTime).Seconds(), src, dst)
// 创建带超时的上下文 in, err := os.Open(src)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) if err != nil {
defer cancel() return err
// 创建通道用于接收复制结果
type copyResult struct {
n int64
err error
} }
resultChan := make(chan copyResult, 1) defer in.Close()
// 在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()
// 创建目标文件 out, err := os.Create(dst)
logger.Logger.Printf("[%.3f] 创建目标文件: %s", time.Since(startTime).Seconds(), dst) if err != nil {
out, err := os.Create(dst) return err
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)
} }
defer out.Close()
logger.Logger.Printf("[%.3f] 文件复制操作完成", time.Since(startTime).Seconds())
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 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 { func alignTo10Seconds(timestamp int64) int64 {
t := time.Unix(timestamp, 0) t := time.Unix(timestamp, 0)
// 获取秒数并对齐到10秒
seconds := t.Second() seconds := t.Second()
alignedSeconds := (seconds / 10) * 10 alignedSeconds := (seconds / 10) * 10
// 创建新的时间
alignedTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), alignedSeconds, 0, t.Location()) alignedTime := time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), alignedSeconds, 0, t.Location())
return alignedTime.Unix() return alignedTime.Unix()
} }
// parseSimpleTimestamp 解析简单的时间戳文件名
func parseSimpleTimestamp(filename string) (int64, error) { func parseSimpleTimestamp(filename string) (int64, error) {
nameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename)) nameWithoutExt := strings.TrimSuffix(filename, filepath.Ext(filename))
if len(nameWithoutExt) != 14 { if len(nameWithoutExt) != 14 {
return 0, fmt.Errorf("invalid timestamp filename length: %s", filename)
} }
year, err := strconv.Atoi(nameWithoutExt[0:4])
// 解析时间戳 // 鐟欙絾鐎介弮鍫曟?閹? year, err := strconv.Atoi(nameWithoutExt[0:4])
year, _ := strconv.Atoi(nameWithoutExt[0:4]) if err != nil {
month, _ := strconv.Atoi(nameWithoutExt[4:6]) return 0, fmt.Errorf("invalid year in filename %s: %w", filename, err)
day, _ := strconv.Atoi(nameWithoutExt[6:8]) }
hour, _ := strconv.Atoi(nameWithoutExt[8:10]) month, err := strconv.Atoi(nameWithoutExt[4:6])
minute, _ := strconv.Atoi(nameWithoutExt[10:12]) if err != nil {
second, _ := strconv.Atoi(nameWithoutExt[12:14]) 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) t := time.Date(year, time.Month(month), day, hour, minute, second, 0, time.Local)
return t.Unix(), nil return t.Unix(), nil
} }
// getTimestampFromPath 从文件路径中提取时间戳 // getTimestampFromPath
func getTimestampFromPath(filePath string) int64 { func getTimestampFromPath(filePath string) (int64, error) {
filename := filepath.Base(filePath) filename := filepath.Base(filePath)
timestamp, err := parseSimpleTimestamp(filename) timestamp, err := parseSimpleTimestamp(filename)
if err != nil { if err != nil {
return 0 return 0, err
} }
return timestamp return timestamp, nil
} }
// contains 检查切片是否包含某个元素 // contains
func contains(slice []string, item string) bool { func contains(slice []string, item string) bool {
for _, s := range slice { for _, s := range slice {
if s == item { if s == item {
@@ -375,7 +543,7 @@ func contains(slice []string, item string) bool {
return false return false
} }
// CleanupOldVideos 清理旧的合并视频文件,保留最近的 10 个 // CleanupOldVideos
func CleanupOldVideos(deviceUUID string) { func CleanupOldVideos(deviceUUID string) {
const maxFiles = 10 const maxFiles = 10
videoDir := "/data/upload" videoDir := "/data/upload"
@@ -414,16 +582,15 @@ func CleanupOldVideos(deviceUUID string) {
} }
} }
// CleanupOldRawVideos 清理推理程序生成的原始10s视频,保留最近60条 // CleanupOldRawVideos
func CleanupOldRawVideos(deviceUUID string) { 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))
// 清理火焰检测目录 // 濞撳懐鎮婇悘顐ゅ姍濡偓濞村娲拌ぐ? cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check"))
cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check")) cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check"))
} }
// cleanupRawVideosDirectory 清理单个目录中的原始视频文件
func cleanupRawVideosDirectory(rawDir string) { func cleanupRawVideosDirectory(rawDir string) {
if _, err := os.Stat(rawDir); os.IsNotExist(err) { if _, err := os.Stat(rawDir); os.IsNotExist(err) {
return return
@@ -443,7 +610,7 @@ func cleanupRawVideosDirectory(rawDir string) {
} }
if len(videoFiles) <= 60 { if len(videoFiles) <= 60 {
return // 不超过60条,不清理 return // 娑撳秷绉存潻?0閺夆槄绱濇稉宥嗙閻? }
} }
sort.Slice(videoFiles, func(i, j int) bool { sort.Slice(videoFiles, func(i, j int) bool {