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