chore: upload current project state

This commit is contained in:
gqc
2026-05-12 15:45:50 +08:00
parent cd9b199e2c
commit 6056b1fdc5
25 changed files with 4051 additions and 373 deletions
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoImports">
<option name="excludedPackages">
<array>
<option value="golang.org/x/net/context" />
</array>
</option>
</component>
</project>
Binary file not shown.
+226 -1
View File
@@ -9,11 +9,191 @@ import (
"strings"
)
// Area 检测区域结构体
type Area struct {
ID int `json:"id"`
Pts []Point `json:"pts"`
}
// Point 坐标点结构体
type Point struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
// 新的配置结构体
// ServiceConfig 服务配置结构体
type ServiceConfig struct {
HostUUID string `json:"host_uuid"`
DeviceUUID string `json:"device_uuid"`
Address string `json:"address"`
TaskID string `json:"task_id"`
CameraRTSP string `json:"camera_rtsp"`
CameraIP string `json:"camera_ip"`
Confidence int `json:"confidence"`
DetectArea DetectAreaConfig `json:"detect_area"`
Param ParamConfig `json:"param"`
}
// DetectAreaConfig 检测区域配置
type DetectAreaConfig struct {
FireLeave []Area `json:"fire_leave"`
FireCheck []Area `json:"fire_check"`
}
// ParamConfig 参数配置
type ParamConfig struct {
FireCheck FireCheckParam `json:"fire_check"`
FireLeave FireLeaveParam `json:"fire_leave"`
}
// FireCheckParam 火检参数
type FireCheckParam struct {
Confidence string `json:"confidence"`
DetectionTime string `json:"detection_time"`
}
// FireLeaveParam 离岗参数
type FireLeaveParam struct {
PersonCount string `json:"person_count"`
Confidence string `json:"confidence"`
DetectionTime string `json:"detection_time"`
TemperatureThreshold string `json:"temperature_threshold"`
AlarmTime string `json:"alarm_time"`
}
var (
devicesBaseDir = "/data/devices"
deviceDataList []DeviceData
serviceConfigFile = "/usr/local/etc/service.conf"
)
// LoadServiceConfig loads multiple device data from service config file
func LoadServiceConfig() ([]DeviceData, error) {
if _, err := os.Stat(serviceConfigFile); os.IsNotExist(err) {
logger.Logger.Printf("Service config file does not exist: %s", serviceConfigFile)
return nil, fmt.Errorf("service config file does not exist")
}
data, err := os.ReadFile(serviceConfigFile)
if err != nil {
logger.Logger.Printf("Failed to read service config file: %v", err)
return nil, err
}
// Check if file content is empty
if len(data) == 0 {
logger.Logger.Printf("Service config file is empty")
return nil, fmt.Errorf("service config file is empty")
}
// Parse as ServiceConfig array
var serviceConfigs []ServiceConfig
if err := json.Unmarshal(data, &serviceConfigs); err != nil {
logger.Logger.Printf("Failed to parse service config file: %v", err)
return nil, err
}
// Convert ServiceConfig to DeviceData
var deviceDataList []DeviceData
const FixedUploadURL = "http://192.168.80.168/api/v1/upload/video"
for _, config := range serviceConfigs {
// Create DeviceData from ServiceConfig
deviceData := DeviceData{
DeviceUUID: config.DeviceUUID,
TaskID: config.TaskID,
CameraRTSP: config.CameraRTSP,
CameraIP: config.CameraIP,
Confidence: config.Confidence,
HostUUID: config.HostUUID,
Address: config.Address,
UploadURL: FixedUploadURL,
// Set new fields for new configuration format
FireLeaveDetectArea: config.DetectArea.FireLeave,
FireCheckDetectArea: config.DetectArea.FireCheck,
FireLeaveParam: config.Param.FireLeave,
FireCheckParam: config.Param.FireCheck,
}
// Parse and set temperature threshold from FireLeaveParam
if tempThresholdStr := config.Param.FireLeave.TemperatureThreshold; tempThresholdStr != "" {
var tempThreshold float64
if _, err := fmt.Sscanf(tempThresholdStr, "%f", &tempThreshold); err == nil && tempThreshold > 0 {
deviceData.TemperatureThreshold = tempThreshold
logger.Logger.Printf("Set temperature threshold from config: %.1f°C (DeviceUUID=%s)", tempThreshold, config.DeviceUUID)
}
}
// Filter out configurations with empty DeviceUUID
if deviceData.DeviceUUID != "" {
deviceDataList = append(deviceDataList, deviceData)
}
}
if len(deviceDataList) == 0 {
logger.Logger.Printf("No valid device configurations found in service config file")
return nil, fmt.Errorf("no valid device configurations")
}
logger.Logger.Printf("Loaded %d device configurations from service config file", len(deviceDataList))
return deviceDataList, nil
}
// ProcessDetectAreaForInference processes detection area data, converts to format required by inference program
func ProcessDetectAreaForInference(areas []Area) string {
if len(areas) == 0 {
return ""
}
// Only take the first detection area
area := areas[0]
var points []string
for _, point := range area.Pts {
// Directly round, remove decimals
x := int(point.X)
y := int(point.Y)
points = append(points, fmt.Sprintf("%d,%d", x, y))
}
return strings.Join(points, ",")
}
// ProcessDeviceDataForInference processes device data, converts to format required by inference program
func ProcessDeviceDataForInference(data DeviceData) DeviceData {
// Create a copy, only process fields that need conversion
processed := data
// For backward compatibility, process original DetectArea if FireLeaveDetectArea is empty
if len(data.FireLeaveDetectArea) > 0 {
processed.DetectAreaStr = ProcessDetectAreaForInference(data.FireLeaveDetectArea)
} else {
processed.DetectAreaStr = ProcessDetectAreaForInference(data.DetectArea)
}
return processed
}
// ProcessFireLeaveDataForInference processes device data for fire_leave inference
func ProcessFireLeaveDataForInference(data DeviceData) DeviceData {
processed := data
if len(data.FireLeaveDetectArea) > 0 {
processed.DetectAreaStr = ProcessDetectAreaForInference(data.FireLeaveDetectArea)
}
return processed
}
// ProcessFireCheckDataForInference processes device data for fire_check inference
func ProcessFireCheckDataForInference(data DeviceData) DeviceData {
processed := data
if len(data.FireCheckDetectArea) > 0 {
processed.DetectAreaStr = ProcessDetectAreaForInference(data.FireCheckDetectArea)
}
return processed
}
func InitializeDevicesFromServiceConfig() ([]DeviceData, error) {
deviceDataMutex.Lock()
defer deviceDataMutex.Unlock()
@@ -94,8 +274,10 @@ func cleanDeviceData(data DeviceData) DeviceData {
if cleaned.Temperature < 0 {
cleaned.Temperature = 0
}
if cleaned.TemperatureThreshold <= 0 {
// 只有当温度阈值真正为0时才设置默认值45.0°C,避免覆盖用户配置的值
if cleaned.TemperatureThreshold == 0 {
cleaned.TemperatureThreshold = 45.0
logger.Logger.Printf("Set default temperature threshold: 45.0°C (DeviceUUID=%s)", cleaned.DeviceUUID)
}
return cleaned
@@ -195,3 +377,46 @@ func UpdateDeviceData(data DeviceData) error {
logger.Logger.Printf("Device data update completed: DeviceUUID=%s", cleanedData.DeviceUUID)
return nil
}
// SaveServiceConfig saves multiple device configurations to service config file
func SaveServiceConfig(deviceDataList []DeviceData) error {
configData, err := json.MarshalIndent(deviceDataList, "", " ")
if err != nil {
logger.Logger.Printf("Failed to serialize service configuration: %v", err)
return err
}
if err := os.WriteFile(serviceConfigFile, configData, 0644); err != nil {
logger.Logger.Printf("Failed to write service config file: %v", err)
return err
}
logger.Logger.Printf("Service configuration saved to: %s, %d devices", serviceConfigFile, len(deviceDataList))
return nil
}
// SaveSingleServiceConfig saves single device configuration (compatible with existing calls)
func SaveSingleServiceConfig(deviceData DeviceData) error {
// First read existing configuration
existingConfigs, err := LoadServiceConfig()
if err != nil {
// If reading fails, create new configuration list
existingConfigs = []DeviceData{deviceData}
} else {
// Find if configuration with same DeviceUUID already exists
found := false
for i, existing := range existingConfigs {
if existing.DeviceUUID == deviceData.DeviceUUID {
existingConfigs[i] = deviceData
found = true
break
}
}
// If not exists, add to list
if !found {
existingConfigs = append(existingConfigs, deviceData)
}
}
return SaveServiceConfig(existingConfigs)
}
+66 -16
View File
@@ -4,10 +4,12 @@ import (
"fmt"
"os"
"os/exec"
"strings"
"sync"
"time"
"FireLeave_tool/logger"
"FireLeave_tool/portmanager"
)
var (
@@ -20,7 +22,12 @@ var (
// 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)
} else {
logger.Logger.Printf("♥ 推理心跳收到 | 时间: %s | UUID: %s | 类型: fire_leave", time.Now().Format("2006/01/02 15:04:05.000"), deviceUUID)
}
}
// ShouldRestartInference 判断是否需要重启推理程序
@@ -48,41 +55,84 @@ func StopInferenceProcess(deviceUUID string) {
// StartInferenceProcess 重启推理程序(保持原端口!)
func StartInferenceProcess(deviceData *DeviceData) {
// 重启fire_leave推理程序
StopInferenceProcess(deviceData.DeviceUUID)
port, err := GlobalPortManager.GetPort(deviceData.DeviceUUID)
portFireLeave, err := portmanager.GlobalPortManager.GetPort(deviceData.DeviceUUID)
if err != nil {
logger.Logger.Printf("获取端口失败,无法重启推理程序 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
logger.Logger.Printf("获取fire_leave端口失败,无法重启推理程序 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
detectAreaStr := ProcessDetectAreaForInference(deviceData.DetectArea)
// 重启fire_check推理程序
StopInferenceProcess(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
inferenceCmd := exec.Command(
portFireCheck, err := portmanager.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", port),
"-w", detectAreaStr,
"-p", fmt.Sprintf("%d", portFireLeave),
"-w", fireLeaveDetectAreaStr,
"-r", fmt.Sprintf("%d", deviceData.Confidence),
)
inferenceCmd.Stdout = os.Stdout
inferenceCmd.Stderr = os.Stderr
inferenceCmdFireLeave.Stdout = os.Stdout
inferenceCmdFireLeave.Stderr = os.Stderr
logger.Logger.Printf("正在重启推理程序 (使用原端口 %d): %v", port, inferenceCmd.Args)
logger.Logger.Printf("正在重启fire_leave推理程序 (使用原端口 %d): %v", portFireLeave, inferenceCmdFireLeave.Args)
if err := inferenceCmd.Start(); err != nil {
logger.Logger.Printf("推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
if err := inferenceCmdFireLeave.Start(); err != nil {
logger.Logger.Printf("fire_leave推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 保存进程对象
InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmd)
logger.Logger.Printf("推理程序已成功重启,PID=%d (DeviceUUID=%s, Port=%d)",
inferenceCmd.Process.Pid, deviceData.DeviceUUID, port)
// 保存fire_leave进程对象
InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmdFireLeave)
logger.Logger.Printf("fire_leave推理程序已成功重启,PID=%d (DeviceUUID=%s, Port=%d)",
inferenceCmdFireLeave.Process.Pid, deviceData.DeviceUUID, portFireLeave)
// 为fire_check推理程序获取检测区域
fireCheckDetectAreaStr := ProcessDetectAreaForInference(deviceData.FireCheckDetectArea)
// 启动fire_check推理程序
inferenceCmdFireCheck := exec.Command(
"./yolov5",
"-s", deviceData.CameraRTSP,
"-m", "fire_check",
"-c", fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID),
"-p", fmt.Sprintf("%d", portFireCheck),
"-w", fireCheckDetectAreaStr,
"-r", fmt.Sprintf("%d", deviceData.Confidence),
"-R",
)
inferenceCmdFireCheck.Stdout = os.Stdout
inferenceCmdFireCheck.Stderr = os.Stderr
logger.Logger.Printf("正在重启fire_check推理程序 (使用原端口 %d): %v", portFireCheck, inferenceCmdFireCheck.Args)
if err := inferenceCmdFireCheck.Start(); err != nil {
logger.Logger.Printf("fire_check推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 保存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)
// 重启后立即刷新一次心跳,防止 30s 检测又触发
UpdateInferenceHeartbeat(deviceData.DeviceUUID)
UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
}
+6 -130
View File
@@ -4,8 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"strings"
"sync"
"time"
@@ -49,9 +47,7 @@ func (c *WSClient) IsConnected() bool {
return c.connected
}
const (
serviceConfigFile = "/usr/local/etc/service.conf"
)
// DeviceData struct
type DeviceData struct {
@@ -74,6 +70,11 @@ type DeviceData struct {
AlarmPauseUntil int64 `json:"-"` // Pause alarm untilw
HostUUID string `json:"host_uuid"`
Address string `json:"address"`
// New fields for new configuration format
FireLeaveDetectArea []Area `json:"fire_leave_detect_area,omitempty"`
FireCheckDetectArea []Area `json:"fire_check_detect_area,omitempty"`
FireLeaveParam FireLeaveParam `json:"fire_leave_param,omitempty"`
FireCheckParam FireCheckParam `json:"fire_check_param,omitempty"`
}
type InferenceParams struct {
@@ -85,16 +86,6 @@ type InferenceParams struct {
Port int `json:"port"` // HTTP server port
}
type Area struct {
ID int `json:"id"`
Pts []Point `json:"pts"`
}
type Point struct {
X float64 `json:"x"`
Y float64 `json:"y"`
}
// WSMessage struct
type WSMessage struct {
ID int64 `json:"id,omitempty"`
@@ -584,126 +575,11 @@ func (d *DeviceData) ToInferenceParams(port int) InferenceParams {
}
}
// SaveServiceConfig saves multiple device configurations to service config file
func SaveServiceConfig(deviceDataList []DeviceData) error {
configData, err := json.MarshalIndent(deviceDataList, "", " ")
if err != nil {
logger.Logger.Printf("Failed to serialize service configuration: %v", err)
return err
}
if err := os.WriteFile(serviceConfigFile, configData, 0644); err != nil {
logger.Logger.Printf("Failed to write service config file: %v", err)
return err
}
logger.Logger.Printf("Service configuration saved to: %s, %d devices", serviceConfigFile, len(deviceDataList))
return nil
}
// SaveSingleServiceConfig saves single device configuration (compatible with existing calls)
func SaveSingleServiceConfig(deviceData DeviceData) error {
// First read existing configuration
existingConfigs, err := LoadServiceConfig()
if err != nil {
// If reading fails, create new configuration list
existingConfigs = []DeviceData{deviceData}
} else {
// Find if configuration with same DeviceUUID already exists
found := false
for i, existing := range existingConfigs {
if existing.DeviceUUID == deviceData.DeviceUUID {
existingConfigs[i] = deviceData
found = true
break
}
}
// If not exists, add to list
if !found {
existingConfigs = append(existingConfigs, deviceData)
}
}
return SaveServiceConfig(existingConfigs)
}
// LoadServiceConfig loads multiple device data from service config file
func LoadServiceConfig() ([]DeviceData, error) {
if _, err := os.Stat(serviceConfigFile); os.IsNotExist(err) {
logger.Logger.Printf("Service config file does not exist: %s", serviceConfigFile)
return nil, fmt.Errorf("service config file does not exist")
}
data, err := os.ReadFile(serviceConfigFile)
if err != nil {
logger.Logger.Printf("Failed to read service config file: %v", err)
return nil, err
}
// Check if file content is empty
if len(data) == 0 {
logger.Logger.Printf("Service config file is empty")
return nil, fmt.Errorf("service config file is empty")
}
// Parse as DeviceData array
var deviceDataList []DeviceData
if err := json.Unmarshal(data, &deviceDataList); err != nil {
logger.Logger.Printf("Failed to parse service config file: %v", err)
return nil, err
}
// Filter out configurations with empty DeviceUUID
var validDeviceDataList []DeviceData
for _, deviceData := range deviceDataList {
if deviceData.DeviceUUID != "" {
validDeviceDataList = append(validDeviceDataList, deviceData)
}
}
const FixedUploadURL = "http://192.168.80.168/api/v1/upload/video"
for i := range validDeviceDataList {
if validDeviceDataList[i].UploadURL == "" {
validDeviceDataList[i].UploadURL = FixedUploadURL
logger.Logger.Printf("Auto-completed UploadURL: %s (DeviceUUID=%s)",
FixedUploadURL, validDeviceDataList[i].DeviceUUID)
}
}
if len(validDeviceDataList) == 0 {
logger.Logger.Printf("No valid device configurations found in service config file")
return nil, fmt.Errorf("no valid device configurations")
}
logger.Logger.Printf("Loaded %d device configurations from service config file", len(validDeviceDataList))
return validDeviceDataList, nil
}
// ProcessDetectAreaForInference processes detection area data, converts to format required by inference program
func ProcessDetectAreaForInference(areas []Area) string {
if len(areas) == 0 {
return ""
}
// Only take the first detection area
area := areas[0]
var points []string
for _, point := range area.Pts {
// Directly round, remove decimals
x := int(point.X)
y := int(point.Y)
points = append(points, fmt.Sprintf("%d,%d", x, y))
}
return strings.Join(points, ",")
}
func ProcessDeviceDataForInference(data DeviceData) DeviceData {
// Create a copy, only process fields that need conversion
processed := data
processed.DetectAreaStr = ProcessDetectAreaForInference(data.DetectArea)
return processed
}
func (c *WSClient) SendAsync(message interface{}) {
go func() {
@@ -0,0 +1,355 @@
# WebSocket 协议改造分析
日期:2026-05-11
参考文档:
- `https://gitea.s5s8.com/anlicheng/ekfa/raw/branch/protobuf_mst/docs/service_websocket_protocol.md`
## 1. 当前项目的通讯方式
当前实现文件:
- `connect/ws_channel.go`
- `reporter/reporter.go`
- `main.go`
当前协议特征:
1. 传输层
- WebSocket 文本帧
- 使用 JSON 编码
2. 请求响应模型
- 统一消息结构:`{id, method, params, result, error}`
- 当前请求方法主要有:
- `register`
- `subscribe`
- 当前推送/上报主要有:
- `metric_data`
- `publish`
3. 注册逻辑
- 客户端连接到 `ws://172.17.0.1:18080/ws`
- 发送 `register`
- 参数包含:
- `container_name`
- `service_id`
- `meta_data`
4. 订阅逻辑
- 发送 `subscribe`
- 参数为:
- `topic`
5. 下行消息
- 服务端下发 `publish`
- `params` 中当前代码按如下字段解析:
- `topic`
- `data`
- `content`
6. 上行数据上报
- 业务侧通过:
- `method = "metric_data"`
- `params.route_key`
- `params.metric`
- `reporter` 中事件与数据快照上报都走这个逻辑
7. 心跳与重连
- 客户端主动发 WebSocket ping frame
- 自己维护重连、重注册、重订阅
## 2. 目标协议的通讯方式
根据新文档,目标协议特征如下:
1. 传输层
- WebSocket binary frame
- 每帧格式:
- `1 byte frame_type`
- `N bytes protobuf payload`
2. 帧类型
- `0x01``REQUEST`
- `0x02``REPLY`
- `0x03``CAST`
3. protobuf 消息
- `ServiceRequest`
- `ServiceReply`
- `ServiceCast`
4. request/reply
- `register`
- 参数只有 `service_id`
- `subscribe`
- 参数只有 `topic`
- `reply`
- 成功:`result = "ok"`
- 失败:`error.code = -1`
5. cast
- service -> efka
- `metric_data`
- 字段:
- `route_key: bytes`
- `metric: bytes`
- efka -> service
- `topic_event`
- 字段:
- `topic: string`
- `content: bytes`
6. 注册前约束
- 未注册前只允许 `register`
- 其它 request 返回 `invalid request`
- cast 会被忽略
7. ping/pong
- Cowboy 直接处理 ping/pong
- service 侧无需再设计额外业务层心跳协议
## 3. 当前与目标协议的核心差异
## 编码层差异
当前:
- JSON
- 文本帧
目标:
- protobuf
- 二进制帧
影响:
- `connect/ws_channel.go` 的读写逻辑要整体重写,不能局部修补
## 消息模型差异
当前:
- 通用 JSON RPC 风格:`id + method + params/result/error`
目标:
- 分离为 `REQUEST / REPLY / CAST`
- 且由帧头字节决定消息类型
影响:
- 现有 `WSMessage` 结构体要废弃或降级为兼容层
## 注册参数差异
当前:
- `register(container_name, service_id, meta_data)`
目标:
- `register(service_id)`
影响:
- `RegisterParams` 需要改
- `container_name``meta_data` 不应继续按当前协议发送
## 下行消息差异
当前:
- 服务端下发 `publish`
- 业务代码把 `publish.params` 再反序列化
目标:
- 服务端下发 `CAST topic_event`
影响:
- `handlePublishMessage()` 这类 JSON `publish` 解析流程要删除
- 新增 `topic_event` 的 protobuf 解码与路由
## 上行上报差异
当前:
- `method = metric_data`
- `params.route_key`
- `params.metric`
目标:
- `CAST metric_data`
- `route_key``metric` 都是 `bytes`
影响:
- `reporter` 发消息时不再构造 JSON RPC
- 要构造 `ServiceCast.metric_data`
## reply 相关差异
当前:
- 请求响应靠 JSON 的 `id`
目标:
- `packet_id`
- 且只存在于 `ServiceRequest / ServiceReply`
影响:
- 当前 `callbacks map[int64]chan *WSMessage` 机制可以保留思路
- 但要替换成 protobuf `packet_id``ServiceReply`
## 4. 当前项目中需要改动的代码点
## A. `connect/ws_channel.go` 需要整体重构
需要改动:
1. 连接保持不变
- 仍然连 `/ws`
2. 写消息逻辑
- 现在 `SendRawMessage()` 直接 `json.Marshal()` 后发 `TextMessage`
- 需要改成:
- protobuf 序列化
- 前面拼接 `frame_type`
-`BinaryMessage`
3. 请求发送逻辑
- 现在 `sendRequest(method, params, waitResponse)` 是 JSON 风格
- 需要改成:
-`ServiceRequest`
-`packet_id`
- body 选择 `register``subscribe`
-`0x01 + protobuf(ServiceRequest)`
4. 读消息逻辑
- 现在 `readLoop()` 直接把整帧当 JSON 解码
- 需要改成:
- 读取二进制帧
- 取第一个字节判断帧类型
- `0x02` 解码为 `ServiceReply`
- `0x03` 解码为 `ServiceCast`
5. 下行消息分发
- 现在按 `message.Method == "publish"` 处理
- 需要改成按 `ServiceCast.body`
- `topic_event`
- 后续如协议扩展再加别的 cast
6. 回调等待机制
- 现在按 `id` 关联回调
- 需要改成按 `packet_id`
## B. 注册逻辑需要调整
当前:
- `RegisterService(containerName string)`
- 发送 `container_name + service_id + meta_data`
目标:
- 仅发送 `service_id`
建议:
-`container_name` 从协议层移除
- `service_id` 建议继续使用当前生成逻辑,但需确认上层是否有固定命名要求
## C. 订阅逻辑需要调整
当前:
- 发送 JSON `subscribe`
目标:
- 发送 protobuf `ServiceRequest.Subscribe`
建议:
- `SubscribeTopic()` 对外接口可以保留
- 内部实现改为 protobuf request
## D. 上报逻辑需要调整
当前:
- `reporter.ReportEvent()`
- `reporter.ReportDataSnapshot()`
- `GenerateEventMessage()`
- `GenerateDataMessage()`
- 最终都构造成 JSON `metric_data`
目标:
- 最终发送 protobuf `ServiceCast.metric_data`
建议:
- `GenerateEventMessage()` / `GenerateDataMessage()` 不一定保留 map 结构
- 更合理的是:
- 先生成业务 payload JSON 字符串
- 再把该字符串作为 `metric_data.metric` 的 bytes 内容
- `route_key` 也按 bytes 写入 protobuf
## E. 下行配置/告警处理需要调整
当前:
- 依赖 `publish.params.topic/data/content`
- 并把 `data``DeviceData` 解析
目标:
- 依赖 `topic_event.topic/content`
- `content` 是 bytes
建议:
- 重新梳理每个 topic 的 payload 格式
- 尤其要确认:
- `/dhlr/alert`
- `/dhlr/forbid_time`
- 设备配置下发 topic
- 不能再默认存在 `publish.data`
## 5. 改造时需要确认的外部问题
这些问题代码里无法自行推断,需要和上层协议一起确认:
1. 设备配置下发现在走哪个 topic
- 当前代码默认任何 `publish.data` 都可能是 `DeviceData`
- 新协议文档只定义了 `topic_event(topic, content)`,没定义具体业务 topic 的 payload schema
2. `content` 的业务内容格式是什么
- 是 JSON bytes
- 还是 protobuf bytes
- 还是普通字符串
3. `service_id` 的命名规则
- 现有代码自动生成 `fireleave-service-<timestamp>`
- 新链路是否要求固定 `service_id`
4. topic 命名是否需要改
- 当前项目使用:
- `/dhlr/alert`
- `/dhlr/forbid_time`
- `/dhlr/device/<uuid>/event`
- `/dhlr/device/<uuid>/data`
- 文档中的示例 topic 风格更像:
- `device/a/event`
- `device/*/event`
- 需要确认上层真实路由规范
5. 是否还需要客户端主动 ping
- 文档说明服务端会直接回复 ping/pong
- 但没有明确要求客户端是否必须主动发 ping
- 当前保留主动 ping 一般没问题,但需要确认是否有重复机制
## 6. 建议的改造顺序
1. 先生成 `service.proto` 的 Go 代码
2. 新建独立的 protobuf WebSocket 客户端层
3. 保留现有上层接口:
- `RegisterService`
- `SubscribeTopic`
- `SendAsync`
4. 先把协议层替换掉,不动业务层调用点
5. 再改下行 `topic_event` 到业务 topic 的解析分发
6. 最后联调配置下发、事件上报、重连重订阅
## 7. 对当前项目的直接结论
这次协议改造不是小修小补,而是通讯层整体替换,最核心的改动文件会是:
- `connect/ws_channel.go`
- `reporter/reporter.go`
- 可能新增:
- `connect/protocol/`
- `connect/pb/`
同时,当前项目里“配置更新不生效”的问题建议和这次协议改造一起处理,否则即使新协议打通,设备配置下发链路也还是不完整。
1.我的业务逻辑是这样的,我是末端的微服务,我负责采集数据然后上报数据,我的业务是安防的,我这边分析发现危险情况然后上报给上层,而forbid_time和alert是我联动的硬件侧的声光报警器,他们分别区分的是报警间隔时间和报警信息。我对你这句话的理解,你认为topic是对多个微服务或者设备的一个共同网络,如果改动了topic_event(topic, content)我订阅这个topic的所有设备都会触发声光报警是吗?
2.对的,主要是外层传输协议的改动。
3.可能是你不清楚现在的运行环境,我先描述一下现在的业务环境,
+70
View File
@@ -0,0 +1,70 @@
# TODO
日期:2026-05-11
## 第一阶段
1. 统一端口管理器
- 明确保留哪一套端口管理器作为唯一实现
- 替换重启链路里的端口获取逻辑
- 校验 `fire_leave``fire_check`、测温端口关系
- 清理废弃实现和重复状态
2. 修复 `service.conf` schema
- 明确 `service.conf` 的唯一 schema
- 增加 `DeviceData -> ServiceConfig` 转换
- 重写 `SaveServiceConfig()` / `SaveSingleServiceConfig()`
- 检查已有启动流程兼容性
3. 修复 `fire_leave` 参数映射
- 明确 `fire_leave` 运行时依赖字段
- 补齐 `param.fire_leave` 到运行字段的映射
- 清理主流程里对默认值的隐式依赖
- 复核 `confidence` 是否要区分双模型
## 第二阶段
1. 接入 `/dhlr/forbid_time``fire_leave` 暂停上报
- 明确 `/dhlr/forbid_time` 最终字段语义
- 增加 `fire_leave` 暂停状态存储
-`fire_leave` 主判断前接入暂停窗口检查
- 清理或降级 `reporter` 中不再适用的事件周期逻辑
2. 调整 GPIO 按钮逻辑
- 确认按钮最终由谁决定暂停时长
- 确认按钮是本地触发还是必须等待上层下发
- 移除与“事件周期切换”强耦合的旧逻辑
## 第三阶段
1. 改造 WebSocket 协议层为 protobuf binary frame
- 引入 `service.proto` 对应的 Go 类型
- 重写 websocket 读写帧逻辑
- 重写 request/reply 关联逻辑
- 重写下行 `topic_event` 分发逻辑
- 重写上行 `metric_data` 发送逻辑
2. 保留主动 ping 和失败重连
- 在新协议层中保留 ping loop
- 保留 ping 失败即重连逻辑
- 复核重连后 register 和 subscribe 恢复是否完整
## 暂不处理
1. 业务上报 ACK
- 暂不增加业务 ACK
- 协议改造时保留现有单向上报模式
- 作为已知风险保留在 `整改计划.md`
2. 运行中配置热更新链路
- 判断当前业务是否真的需要运行中热更新
- 如果需要,后续再设计完整消费链路
- 如果不需要,后续再移除误导性代码
## 收尾
1. 增加最小验证
- 增加协议层编解码基础测试
- 增加配置映射测试
- 增加报警暂停窗口逻辑测试
- 条件允许时补最小集成测试
+528
View File
@@ -0,0 +1,528 @@
# 动火离人微服务业务逻辑梳理
本文档基于当前代码整理,目标是帮助快速回忆这个微服务现在“实际在做什么”,以及哪些逻辑是设计上存在但当前代码里已经弱化或未真正生效的。
## 1. 项目定位
当前仓库是“动火离人”项目中的微服务管理程序,职责不是前端展示,也不是后端总控,而是:
- 接收后端/平台下发的设备配置
- 为每个设备启动并管理本地推理程序
- 接收推理结果和测温结果
- 按规则判断是否触发报警
- 回溯报警前后视频,拼接并上传
- 通过 WebSocket 将报警事件和数据快照发给后端,再由后端转给前端
整体链路可以理解为:
`前端配置 -> 后端下发配置 -> 微服务启动/管理推理 -> 推理结果回流微服务 -> 微服务判定报警 -> 上传视频 + websocket上报 -> 后端转发前端展示`
## 2. 当前代码里的三类核心进程
每个设备启动时,实际上会拉起 3 条本地能力:
1. `fire_leave` 推理程序
- 模型参数:`./yolov5 -m model`
- 主要用途:统计人员数量
- 结果通过本地 HTTP 服务回传
2. `fire_check` 推理程序
- 模型参数:`./yolov5 -m fire_check -R`
- 主要用途:检测火焰
- 结果通过另一套本地 HTTP 服务回传
3. 测温程序
- 启动脚本:`./release/run_thermometry.sh`
- 结果通过本地 TCP 服务回传
也就是说,当前“动火离人”并不是单模型,而是:
- 一套模型做人数量判断
- 一套模型做火焰检测
- 一套测温程序提供温度值
## 3. 配置来源与初始化
### 3.1 启动时配置来源
程序启动后先从 `/usr/local/etc/service.conf` 读取设备配置。
配置结构已经是多设备形式,核心字段包括:
- `device_uuid`
- `task_id`
- `camera_rtsp`
- `camera_ip`
- `confidence`
- `detect_area.fire_leave`
- `detect_area.fire_check`
- `param.fire_leave`
- `param.fire_check`
其中:
- `fire_leave` 检测区给人员检测使用
- `fire_check` 检测区给火焰检测使用
- 两套检测区彼此独立
### 3.2 配置落盘
初始化后,每个设备还会落盘到:
- `/data/devices/device_{deviceUUID}.json`
用于后续读取和状态持久化。
## 4. 每个设备启动时的完整流程
`main.go` 中每个设备会进入 `processDevice()`,当前流程如下:
1. 设备加入全局 `DeviceManager`
2.`fire_leave` 分配一个端口
3.`fire_check` 再分配一个端口
4. 分别启动两个本地 HTTP 接收服务
5. 等待 HTTP `/health` 就绪
6. 把前端配置的检测区域转换成推理程序需要的点串格式
7. 启动 `fire_leave` 推理进程
8. 启动 `fire_check` 推理进程
9. 启动测温程序
10. 等待测温 TCP 服务就绪
11. 创建 TCP 客户端读取温度
12. 进入主循环,持续处理:
- `fire_leave` 推理结果
- `fire_check` 推理结果
- 周期性温度读取和报警判定
## 5. 两条检测链路的当前职责
## 5.1 fire_leave 链路
当前代码里,`fire_leave` 推理结果主要只负责更新人数。
处理逻辑:
- 微服务接收 `/video/post`
- 解析 `params`
-`ClassIdx == 1`
-`Number` 作为当前人数 `PersonCount`
- 写回设备状态
也就是说,当前“离人”判断本身不是由模型直接报“报警/不报警”,而是:
- 模型只提供人数
- 微服务结合温度再做二次判定
## 5.2 fire_check 链路
当前代码里,`fire_check` 推理结果专门负责火焰报警。
处理逻辑:
- 微服务接收 `fire_check``/video/post`
- 遍历结果
-`ClassIdx == 0 && Number > 0` 时,认为检测到火焰
- 连续满足 `detection_time` 秒后触发火焰报警
火焰报警完全独立于“无人 + 高温”逻辑,不依赖温度和人数。
## 6. 动火离人报警的当前判定逻辑
这是你现在代码里的主报警逻辑,运行在定时器分支里,每 2 秒检查一次。
### 6.1 输入条件
判定使用两个实时量:
- `PersonCount == 0`
- `Temperature > TemperatureThreshold`
其中温度还做了一个简化的防抖:
- 高于阈值 `+0.5`:稳定超温
- 低于阈值 `-0.5`:稳定未超温
- 落在中间:按当前是否大于阈值判断
### 6.2 触发条件
只有同时满足以下条件才会计时:
- 无人
- 温度超阈值
满足后开始累计持续时间,达到 `DetectionTime` 后触发报警。
默认值:
- `DetectionTime` 默认 `20`
- `AlarmTime` 默认 `300`
### 6.3 冷却时间
报警触发后会进入冷却:
- `AlarmPauseUntil = now + AlarmTime`
冷却期内,即使条件继续满足,也不会重复触发。
### 6.4 当前本质
当前“动火离人报警”实际规则可以概括成一句话:
`人员检测结果为 0 + 测温超过阈值 + 持续达到设定秒数 -> 触发 fire_leave 报警`
## 7. 火焰检测报警的当前判定逻辑
火焰检测走另一套状态机。
### 7.1 输入条件
- `fire_check` 结果中出现 `ClassIdx == 0 && Number > 0`
### 7.2 持续时间
持续时间取自:
- `deviceData.FireCheckParam.DetectionTime`
默认值:
- 如果没配,默认 `2`
### 7.3 触发结果
持续检测到火焰达到时长后,触发:
- `GlobalReporter.TriggerFireCheckAlarm(deviceData, alarmTime)`
### 7.4 当前本质
当前“火焰检测报警”可以概括成:
`火焰模型连续检测到火焰达到设定秒数 -> 触发 fire_check 报警`
## 8. 报警后做了什么
无论是 `fire_leave` 还是 `fire_check`,触发后总体都会走这几步:
1. 防止同一告警重复处理
2. 去录像目录找报警前后视频
3. 拼接成一个完整 MP4
4. 复制到 `/data/upload`
5. 上传到 Nginx/后端文件接口
6. 通过 WebSocket 上报事件消息
7. 再上报一份数据快照消息
## 9. 视频回溯与拼接逻辑
当前录像目录:
- 普通动火离人视频:`/usr/data/camera/{deviceUUID}`
- 火焰检测视频:`/usr/data/camera/{deviceUUID}_fire_check`
拼接逻辑:
1. 先把报警时间对齐到最近的 10 秒片段
2.`前10秒``当前10秒``后10秒` 三段视频
3. 如果缺段,会从同目录找最接近的 mp4 作为替代
4. 至少要找到 2 段视频才继续
5.`mp4_merge` 进行拼接
6. 输出到 `/data/upload/{deviceUUID}-{日期-时间}.MP4`
所以当前上报视频本质上是“报警时刻前后约 30 秒的回溯片段”。
## 10. WebSocket 在当前系统里的作用
WebSocket 地址写死为:
- `ws://172.17.0.1:18080/ws`
主要承担 4 件事:
1. 服务注册
2. 订阅平台 topic
3. 接收配置更新
4. 上报事件与数据
### 10.1 启动后注册
启动后会注册一个服务:
- `containerName = fireleave-container`
- `serviceID = fireleave-service-{timestamp}`
### 10.2 订阅的 topic
当前代码里会订阅:
- `/dhlr/alert`
- `/dhlr/forbid_time`
### 10.3 当前业务上真正需要接收的不是设备配置
这里要和“设备配置来源”明确区分。
设备基础配置当前应以本地文件为准:
- `/usr/local/etc/service.conf`
也就是说:
- 摄像头
- 检测区域
- 置信度
- 各类基础参数
这些设备配置不是依赖 WebSocket 实时下发来生效,而是直接读取本地 `service.conf`
WebSocket 在这块更应该承担的是“控制类消息”的接收,例如:
- 报警暂停时长
- 延迟报警/允许离岗时长
- 声光报警控制
补充说明:
- 当前代码里 `ws_channel.go` 仍保留了把 `publish.data` 解析成 `DeviceData` 并写回本地的逻辑
- 但按你确认后的业务,这部分不应视为当前核心业务链路
- 也就是说,代码现状和业务认知这里存在偏差,文档以你刚确认的业务为准
## 11. 按钮状态控制的当前真实逻辑
这是你提到“有点忘了”的重点之一。
当前 GPIO 按钮监控在 `gpio_monitor` 包里,监听:
- 按钮 GPIO`111`
- 蜂鸣器 GPIO`109`
### 11.1 按钮不是启动/停止按钮
当前业务里,按钮按下后不会:
- 启动推理
- 停止推理
- 直接触发报警
按钮的真实业务含义是:
- 临时关闭“人员离开”这一类报警的上报
- 但火焰检测报警仍然保持正常上报
### 11.2 为什么要有这个按钮
这是为了解决现场的特殊用工场景。
例如后厨煲汤:
- 厨师需要很早开始生火
- 但不可能在整个过程中始终守在灶台旁
- 现场管理允许短时间离开
- 如果仍严格套用“动火离人”规则,就会不断产生不符合现场管理实际的报警
所以按钮的业务意义是:
- 在允许短暂离岗的场景下,临时屏蔽人员离开报警
- 同时保留火焰检测能力,避免真正的火焰风险漏报
### 11.3 按钮按下后的目标行为
按你的描述,正确业务应该是:
- `fire_leave` 报警在暂停窗口内不再上报
- `fire_check` 报警仍然是每次检测到火焰后都正常上报
### 11.4 当前代码和目标业务的偏差
当前代码里,按钮实际上是在切换 `reporter` 里的事件周期状态:
- `fire_leave`
- `fire_check`
并带有:
- 200ms 去抖
- 5 秒冷却
但这和你描述的真实业务目标并不一致。
也就是说:
- 现在代码实现的是“事件周期切换”
- 你要的业务其实是“人员离开报警进入暂停期”
这部分应视为待修正逻辑。
## 12. 蜂鸣器逻辑
蜂鸣器不由本地报警自动触发,而是来自平台 topic:
- 订阅 `/dhlr/alert`
收到消息后,如果内容里的:
- `type == 1`
则触发本地蜂鸣器响指定秒数,默认 3 秒。
所以当前蜂鸣器逻辑是:
- 平台推送声光报警命令
- 微服务本地点响 GPIO109
不是“本地一检测到报警就必然自己响”。
## 13. `/dhlr/forbid_time` 的真实业务目标与当前问题
按你刚补充的业务,这个 topic 的核心作用不是“事件周期切换”,而是:
- 当前端在按钮按下后,下发一个“允许人员短暂离开”的暂停时长
- 微服务收到后,进入一个人员离开报警暂停窗口
- 在这个窗口内,即使检测到人员离开,也不要触发 `fire_leave` 上报
-`fire_check` 火焰报警仍然要照常上报
### 13.1 真实业务目标
`/dhlr/forbid_time` 应该承载的是:
- 暂停人员离开报警的时长
- 或与之等价的延迟/禁止上报控制
### 13.2 当前代码现状
当前代码收到 `/dhlr/forbid_time` 后,主要做的是:
1. 保存 `minute``/data/cache/forbid_time_config.json`
2. 保存 `fire_leave_timeout` / `fire_check_timeout``/data/cache/event_cycle_timeout.json`
3. 继续服务于 `reporter` 里的事件周期逻辑
### 13.3 当前存在的问题
这和你描述的真实业务不一致,问题点在于:
- 订阅逻辑没有真正控制 `fire_leave` 报警暂停窗口
- 没有把“按钮触发的允许离岗时长”落到主报警判定里
- 暂停期内是否跳过 `fire_leave` 上报,当前主流程没有真正接进去
- `fire_check``fire_leave` 的差异化处理没有按这个业务目标落地
### 13.4 应有的目标行为
这部分后续应改成:
1. 前端按钮按下
2. 后端通过 `/dhlr/forbid_time` 下发暂停时长
3. 微服务记录 `fire_leave` 报警暂停截止时间
4. 在暂停窗口内:
- 人员离开检测继续运行
- 但不触发 `fire_leave` 报警上报
5. 火焰检测逻辑不受影响,继续正常上报
## 14. 推理程序保活与异常恢复
代码里做了基础保活。
### 14.1 心跳来源
每次推理程序向本地 `/video/post` 发结果时,都会更新一次心跳时间。
也就是说:
- 心跳不是单独接口
- 心跳就是“推理结果持续回传”
### 14.2 超时判断
如果超过 `85` 秒没有收到某个推理程序的数据,就认为需要重启。
主循环每 `30` 秒检查一次:
- `fire_leave`
- `fire_check`
如果超时,则:
1. 杀掉旧进程
2. 复用原端口重启 `yolov5`
3. 立即刷新心跳
## 15. 当前代码里值得特别记住的几个事实
### 15.1 动火离人报警不是“火焰+离人”
当前代码里 `fire_leave` 报警依赖的是:
- 无人
- 温度超阈值
不是火焰检测结果。
### 15.2 火焰报警是完全独立的第二条链路
`fire_check` 只看火焰检测结果,单独触发 `fire_check` 级别事件。
### 15.3 两套检测区域是独立配置
- `FireLeaveDetectArea`
- `FireCheckDetectArea`
前端可以分别配置。
### 15.4 前端参数里真正参与当前判定的关键值
动火离人链路:
- `param.fire_leave.detection_time`
- `param.fire_leave.temperature_threshold`
- `param.fire_leave.alarm_time`
- 人数结果来自 `fire_leave` 模型
火焰链路:
- `param.fire_check.detection_time`
- 火焰结果来自 `fire_check` 模型
### 15.5 置信度当前是共用的
当前两个 `yolov5` 进程启动时都使用:
- `deviceData.Confidence`
没有看到两套模型分别使用不同置信度字段的实现。
## 16. 结合当前代码后的业务总结
如果用最贴近当前实现的话来概括:
- 前端负责配置设备、检测区和阈值
- 后端负责配置转发、消息汇总和给前端展示
- 微服务负责拉起两套推理和一套测温程序
- `fire_leave` 模型负责人数统计
- 测温程序负责温度
- 微服务把“无人 + 超温 + 持续时长”判定为动火离人报警
- `fire_check` 模型负责火焰检测
- 微服务把“持续检测到火焰”判定为火焰报警
- 两类报警都会回溯视频、上传视频、再通过 WebSocket 上报后端
## 17. 当前代码里可能需要你后续再确认的点
这几项不是我猜测,而是从代码现状看出来“可能存在设计与实现不完全一致”的地方:
1. `ConfigUpdateChan` 已有生产者,但未看到消费者
- 说明配置热更新链路可能没做完,或在当前仓库外
2. GPIO 按钮当前实现的是事件周期切换,但真实业务需要的是“暂停 fire_leave 上报”
- 说明这部分现在是实现偏差,不只是保留能力
3. `/dhlr/forbid_time` 当前没有真正接入 `fire_leave` 报警暂停控制
- 说明“按钮按下后允许短时离岗”的核心业务还没真正落地
4. `fire_leave``person_count` 参数没有直接用于报警判断
- 当前代码写死判断 `PersonCount == 0`
- 如果业务上原本支持“少于 N 人也报警”,那这部分目前没有实现
5. `param.fire_leave.confidence``param.fire_check.confidence`
- 配置结构里有
- 但实际启动命令仍统一使用 `deviceData.Confidence`
---
如果后面你继续描述前端和后端的预期业务,我可以再基于这份文档继续补一版“设计业务逻辑 vs 当前代码实现差异”文档,专门帮你找现在代码和你脑中业务之间的偏差。
+332
View File
@@ -0,0 +1,332 @@
# 整改计划
日期:2026-05-11
本文档只保留以下内容:
- 当前确认存在的问题和风险
- 对应的修改方案、影响范围和代码落点
执行项已单独整理到 `docs/todo.md`
## 1. 推理进程重启链路失效
### 问题
当前启动和重启使用了两套不同的端口管理器:
- 启动阶段:`connect.GlobalPortManager`
- 心跳重启阶段:`portmanager.GlobalPortManager`
结果是:
- 初次启动能分到端口
- 心跳超时后重启拿不到原端口
- `yolov5` 重启流程会失败
### 风险
- 推理进程挂掉后无法自恢复
- 设备会长期失去 `fire_leave` / `fire_check` 检测能力
### 准备怎么改
统一端口管理实现,只保留一套端口管理器。
建议方案:
1. 以当前 `main.go` 正在使用的 `connect.GlobalPortManager` 为准,统一所有调用
2. 重写或合并 `connect/heartbeat.go` 中的端口获取逻辑
3. 清理重复的 `portmanager` 包引用,避免两套状态并存
### 需要修改的代码
- `main.go`
- `connect/heartbeat.go`
- `connect/port_manager.go`
- `portmanager/portmanager.go`
- `portmanager/types.go`
## 2. `service.conf` 读写 schema 不一致
### 问题
当前配置读取和写回不是同一套结构:
- `LoadServiceConfig()``[]ServiceConfig` 读取
- `SaveServiceConfig()` / `SaveSingleServiceConfig()``[]DeviceData` 写回
### 风险
- WebSocket 或其它更新路径一旦写回配置
- 下次启动可能无法按原 schema 正确读取
- 配置字段可能丢失或错位
### 准备怎么改
统一配置文件模型,明确:
- `service.conf` 是“部署输入配置”
- `device_*.json` 是“运行态持久化”
建议方案:
1. `service.conf` 始终只用 `ServiceConfig` 读写
2. 运行态状态继续写入 `/data/devices/device_{uuid}.json`
3. 如果确实需要从 WebSocket 更新配置,先把 `DeviceData` 映射回 `ServiceConfig` 再写入
### 需要修改的代码
- `connect/device_storage.go`
- `connect/ws_channel.go`
## 3. `fire_leave` 参数映射不完整
### 问题
当前 `fire_leave` 主判定读的是:
- `deviceData.DetectionTime`
- `deviceData.AlarmTime`
但新配置的真实值在:
- `deviceData.FireLeaveParam.DetectionTime`
- `deviceData.FireLeaveParam.AlarmTime`
另外 `person_count`、双模型 `confidence` 也存在未完整映射的问题。
### 风险
- 离岗报警持续时间可能一直用默认值
- 冷却时间可能一直用默认值
- 前端/平台配置和实际行为不一致
### 准备怎么改
在配置初始化阶段统一完成映射,不在运行主循环里到处兜底。
建议方案:
1. `LoadServiceConfig()` 时完成 `param.fire_leave` 到运行字段的转换
2. `ProcessDeviceDataForInference()` 或专门的 normalize 逻辑里统一补齐
3. 后续主循环尽量只读规范化后的字段
### 需要修改的代码
- `connect/device_storage.go`
- `main.go`
## 4. `/dhlr/forbid_time` 还没真正接入 `fire_leave` 暂停上报
### 问题
按当前确认后的业务:
- `/dhlr/forbid_time` 是控制 `fire_leave` 暂停上报窗口
- `fire_check` 仍要继续正常上报
但当前代码主要做的是:
- 缓存 `minute`
- 缓存 `fire_leave_timeout` / `fire_check_timeout`
- 服务于 `reporter` 里的事件周期逻辑
没有真正控制 `fire_leave` 主报警判定。
### 风险
- 按钮按下后,现场期待的“暂时允许离岗”不会真正生效
- 现场仍可能连续收到不符合预期的 `fire_leave` 报警
### 准备怎么改
`/dhlr/forbid_time` 从“事件周期配置”改为“主报警暂停窗口控制”。
建议方案:
1. 在设备级状态里增加 `fire_leave` 暂停截止时间
2. 收到 `/dhlr/forbid_time` 后更新这个截止时间
3.`fire_leave` 报警判定前先检查暂停窗口
4. 暂停期内继续更新人数、温度,但跳过 `fire_leave` 上报
5. `fire_check` 保持独立
### 需要修改的代码
- `main.go`
- `reporter/reporter.go`
- `gpio_monitor/gpio_monitor.go`
- 可能涉及 `connect/ws_channel.go` 的控制消息分发
## 5. GPIO 按钮逻辑和真实业务存在偏差
### 问题
当前按钮逻辑更接近“切换事件周期”,而不是“暂停 `fire_leave` 上报”。
### 风险
- 代码行为和现场业务理解不一致
- 后续维护时容易误判
### 准备怎么改
将 GPIO 按钮逻辑收敛为:
- 触发本地允许离岗窗口
- 或配合 `/dhlr/forbid_time` 完成暂停控制
### 需要修改的代码
- `gpio_monitor/gpio_monitor.go`
- `reporter/reporter.go`
- `main.go`
## 6. WebSocket 协议需要整体切换到 protobuf binary frame
### 问题
当前协议层是:
- JSON
- 文本帧
- `method + params + result + error`
目标协议是:
- protobuf
- binary frame
- `REQUEST / REPLY / CAST`
### 风险
- 现有 `ws_channel.go` 不能局部修补
- 如果只改部分字段,会造成协议不兼容
### 准备怎么改
按“先替换协议层,尽量少动业务层接口”的原则改。
建议方案:
1. 保留上层调用接口:
- `RegisterService`
- `SubscribeTopic`
- `SendAsync`
2. 内部把 JSON 封装替换为 protobuf 编解码
3. 下行从 `publish` 改为 `topic_event`
4. 上行从 JSON `metric_data` 改为 `ServiceCast.metric_data`
5. 内层业务 payload 继续保留当前 JSON 结构
### 需要修改的代码
- `connect/ws_channel.go`
- `reporter/reporter.go`
- 可能新增 `connect/pb/``connect/protocol/`
## 7. 主动 ping 建议保留,失败立即重连
### 问题
如果不主动 ping
- 死连接发现会变慢
- 空闲连接可能被网络设备静默回收
- 只有下次读写时才发现断连
### 风险
- `/dhlr/alert`
- `/dhlr/forbid_time`
- 事件上报链路
以上三类能力恢复变慢。
### 准备怎么改
继续保留主动 ping。
建议方案:
1. 定时发送 websocket ping frame
2. ping 失败时立即标记断连
3. 触发重连
4. 重连后重新 register / subscribe
### 需要修改的代码
- `connect/ws_channel.go`
## 8. 业务上报仍然是“只发不确认”
### 问题
当前:
- `register`
- `subscribe`
这类请求有协议层确认。
但:
- 报警事件上报
- 数据快照上报
仍然是单向发送。
### 风险
- 只能确认本地是否成功写出 websocket 消息
- 不能确认 efka 是否真正处理成功
- 不能确认上层平台是否真正收到、入库、展示
### 当前处理决定
这一项先不解决,只记录风险。
### 相关代码
- `connect/ws_channel.go`
- `reporter/reporter.go`
## 9. 运行中配置更新链路未闭环
### 问题
当前有:
- `ConfigUpdateChan` 生产者
但没有看到清晰的消费者和热更新执行链路。
### 风险
- 代码让人以为支持实时热更新
- 实际运行中可能并不会生效
### 准备怎么改
先做一次取舍:
- 要么补齐热更新
- 要么明确去掉这条未完成链路
### 需要修改的代码
- `connect/ws_channel.go`
- `main.go`
## 10. 自动化验证不足
### 问题
当前环境没有 `go`,也没有看到覆盖关键链路的自动化测试。
### 风险
- 协议层改造、重启链路改造、报警暂停逻辑改造容易引入回归
### 准备怎么改
至少补最小化验证路径。
+149
View File
@@ -0,0 +1,149 @@
2026/02/11 17:01:05 [1770800465] ALARM TRIGGERED (after 20s): PersonCount=0, Temp=27.8°C > 20.0°C (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [1770800465] 更新报警状态: 冷却时间设置为 300 秒 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [1770800465] 更新设备数据 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 Device data saved successfully: /data/devices/device_3eeba8usb86c0gs448oktxay41a0rm1v.json
2026/02/11 17:01:05 Device data update completed: DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v
2026/02/11 17:01:05 [1770800465] 设备数据更新完成 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [1770800465] 开始异步处理报警 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, alarmTime=1770800445, cooldown=300)
2026/02/11 17:01:05 [0.000] 开始处理报警事件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, alarmTime=1770800445, cooldown=300)
2026/02/11 17:01:05 [0.000] 检查报警事件是否已在处理中 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, key=3eeba8usb86c0gs448oktxay41a0rm1v_1770800445)
2026/02/11 17:01:05 [0.000] 成功获取处理锁 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, key=3eeba8usb86c0gs448oktxay41a0rm1v_1770800445)
2026/02/11 17:01:05 [0.001] 开始合并视频 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, alarmTime=1770800445)
2026/02/11 17:01:05 [0.000] 开始合并视频 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, alarmTime=1770800445)
2026/02/11 17:01:05 [0.000] 检查视频目录: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.000] 视频目录检查成功: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.000] 开始时间对齐处理 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, alarmTime=1770800445)
2026/02/11 17:01:05 [0.000] 时间对齐结果: alarmTime=1770800445 → aligned=1770800440 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.000] 开始查找视频文件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.000] 检查文件: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170030.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] Found video: 20260211170030.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] 检查文件: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] Found video: 20260211170040.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] 检查文件: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170050.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] Found video: 20260211170050.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] 当前找到 3 个视频文件,开始补充替代文件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] 最终收集到 3 个视频文件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] 开始排序视频文件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] Merging 3 videos (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v):
2026/02/11 17:01:05 [0.001] [0] 20260211170030.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] [1] 20260211170040.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] [2] 20260211170050.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] 输出文件路径: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4_joined.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] 清理旧的输出文件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.001] 开始执行视频合并命令: [mp4_merge /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170030.mp4 /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4 /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170050.mp4 --out /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4_joined.mp4] (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
Merging file "/usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170030.mp4"
Merging file "/usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4"
Merging file "/usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170050.mp4"
Output file "/usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4_joined.mp4"
Done in 0.029s
2026/02/11 17:01:05 [0.041] 视频合并命令执行成功 (耗时:0.040s) (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.042] 验证输出文件是否存在: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4_joined.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.042] Merged file created: 20260211170040.mp4_joined.mp4 (size: 1715493 bytes) (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.042] 开始移动文件到 /data/upload/ 目录 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.042] 最终文件路径: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.042] 确保 /data/upload 目录存在 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.043] 开始复制文件: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4_joined.mp4 → /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.000] 开始复制文件: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4_joined.mp4 → /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:05 [0.000] 打开源文件: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4_joined.mp4
2026/02/11 17:01:05 [0.001] 创建目标文件: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:05 [0.002] 源文件大小: 1715493 字节
2026/02/11 17:01:05 [0.003] 开始执行文件复制操作
2026/02/11 17:01:05 [0.037] 文件复制成功: 复制 1715493 字节, 耗时 33.610332ms
2026/02/11 17:01:05 [0.042] 开始同步文件到磁盘
2026/02/11 17:01:05 [0.081] 文件同步成功, 耗时 38.810767ms
2026/02/11 17:01:05 [0.082] 文件复制操作完成
2026/02/11 17:01:05 [0.125] 文件复制成功 (耗时:0.083s) (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.125] 删除临时文件: /usr/data/camera/3eeba8usb86c0gs448oktxay41a0rm1v/20260211170040.mp4_joined.mp4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.126] 临时文件删除成功 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 [0.126] Video merged and moved successfully: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, 总耗时:0.126s)
2026/02/11 17:01:05 [0.126] 视频合并成功,生成文件: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, 耗时:0.126s)
2026/02/11 17:01:05 [0.126] 开始上传视频并上报事件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, 文件=/data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4)
2026/02/11 17:01:05 [0.127] 报警上报成功 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, 耗时:0.000s)
2026/02/11 17:01:05 [0.127] 报警事件处理完成 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, 总耗时:0.127s)
2026/02/11 17:01:05 [1770800465] 报警处理完成 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, 耗时:0.127s)
2026/02/11 17:01:05 开始异步处理视频上传和事件上报 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, 文件=/data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4)
2026/02/11 17:01:05 检查事件周期状态 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 事件周期超时,切换回默认周期 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 当前事件周期: fire_leave (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 Forbid time config is 0, will report both event types (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 开始上报fire_leave事件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 开始上报fire_check事件 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 双事件上报完成 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 Set temperature threshold from config: 20.0°C (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 Loaded 1 device configurations from service config file
2026/02/11 17:01:05 开始上传视频到 Nginx: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4 → http://192.168.80.168/api/v1/upload/video
2026/02/11 17:01:05 打开文件: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:05 文件打开成功, 耗时: 55.417µs
2026/02/11 17:01:05 创建多部分表单
2026/02/11 17:01:05 创建表单文件字段: 3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:05 开始复制文件内容到表单
2026/02/11 17:01:05 Set temperature threshold from config: 20.0°C (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:05 Loaded 1 device configurations from service config file
2026/02/11 17:01:05 开始上传视频到 Nginx: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4 → http://192.168.80.168/api/v1/upload/video
2026/02/11 17:01:05 打开文件: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:05 文件打开成功, 耗时: 72.917µs
2026/02/11 17:01:05 创建多部分表单
2026/02/11 17:01:05 创建表单文件字段: 3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:05 开始复制文件内容到表单
2026/02/11 17:01:05 文件内容复制成功, 耗时: 50.381519ms
2026/02/11 17:01:05 序列化事件参数
2026/02/11 17:01:05 [Upload Event parameters]eventParams: map[alarm_time:1770800445 description:检测到异常火苗 device_UUID:3eeba8usb86c0gs448oktxay41a0rm1v host_uuid:qbxmjyzrkpntfgswaevodhluicqzxplkm level:fire_check person_count:0]
2026/02/11 17:01:05 [Upload Event parameters]eventJSON: {"alarm_time":1770800445,"description":"检测到异常火苗","device_UUID":"3eeba8usb86c0gs448oktxay41a0rm1v","host_uuid":"qbxmjyzrkpntfgswaevodhluicqzxplkm","level":"fire_check","person_count":0}
2026/02/11 17:01:05 写入事件字段到表单
2026/02/11 17:01:05 关闭表单写入器
2026/02/11 17:01:05 [HTTP Request]URL: http://192.168.80.168/api/v1/upload/video
2026/02/11 17:01:05 [HTTP Request]Content-Type: multipart/form-data; boundary=ea8f2517f1310dab1e3f4a8cdb6eecd24b54e96a950b34d7163021c79279
2026/02/11 17:01:05 [HTTP Request]Body 长度: 1716094 bytes
2026/02/11 17:01:05 创建HTTP请求
2026/02/11 17:01:05 发送HTTP请求到 Nginx
2026/02/11 17:01:05 文件内容复制成功, 耗时: 50.004684ms
2026/02/11 17:01:05 序列化事件参数
2026/02/11 17:01:05 [Upload Event parameters]eventParams: map[alarm_time:1770800445 description:无人工作区且温度超阈值报警 device_UUID:3eeba8usb86c0gs448oktxay41a0rm1v host_uuid:qbxmjyzrkpntfgswaevodhluicqzxplkm level:fire_leave person_count:0 temperature:27.800003]
2026/02/11 17:01:05 [Upload Event parameters]eventJSON: {"alarm_time":1770800445,"description":"无人工作区且温度超阈值报警","device_UUID":"3eeba8usb86c0gs448oktxay41a0rm1v","host_uuid":"qbxmjyzrkpntfgswaevodhluicqzxplkm","level":"fire_leave","person_count":0,"temperature":27.800003}
2026/02/11 17:01:05 写入事件字段到表单
2026/02/11 17:01:05 关闭表单写入器
2026/02/11 17:01:05 [HTTP Request]URL: http://192.168.80.168/api/v1/upload/video
2026/02/11 17:01:05 [HTTP Request]Content-Type: multipart/form-data; boundary=29d6eedc2272d36e33f6fe82d928288f7e1b9db50a4b2f8c6cb67af9b185
2026/02/11 17:01:05 [HTTP Request]Body 长度: 1716136 bytes
2026/02/11 17:01:05 创建HTTP请求
2026/02/11 17:01:05 发送HTTP请求到 Nginx
2026/02/11 17:01:05 === Global Device Status monitoring === Currently running 1
2026/02/11 17:01:05 [1] DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v | person:0 | temp:27.8°C | threshold value:20.0°C | cooling time:300s
2026/02/11 17:01:05 Device file status: A total of 1 device files
2026/02/11 17:01:05 device file[0]: device_3eeba8usb86c0gs448oktxay41a0rm1v.json
2026/02/11 17:01:05 Operating equipment 1
2026/02/11 17:01:06 HTTP请求发送成功, 耗时: 445.145637ms
2026/02/11 17:01:06 读取HTTP响应
2026/02/11 17:01:06 响应读取完成, 耗时: 357.877µs
2026/02/11 17:01:06 [nginx response]Status: 200
2026/02/11 17:01:06 [nginx response]Body: {"code":0,"error":"","result":"\/upload\/2026\/2\/11\/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4"}
2026/02/11 17:01:06 解析Nginx响应
2026/02/11 17:01:06 提取视频URL
2026/02/11 17:01:06 [ Upload Successful ] Local file: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4 → Remote URL: /upload/2026/2/11/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:06 视频上传完成, 总耗时: 505.346151ms
2026/02/11 17:01:06 Nginx上传成功: /upload/2026/2/11/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:06 HTTP请求发送成功, 耗时: 452.366747ms
2026/02/11 17:01:06 读取HTTP响应
2026/02/11 17:01:06 Set temperature threshold from config: 20.0°C (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:06 响应读取完成, 耗时: 319.085µs
2026/02/11 17:01:06 [nginx response]Status: 200
2026/02/11 17:01:06 [nginx response]Body: {"code":0,"error":"","result":"\/upload\/2026\/2\/11\/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4"}
2026/02/11 17:01:06 解析Nginx响应
2026/02/11 17:01:06 提取视频URL
2026/02/11 17:01:06 [ Upload Successful ] Local file: /data/upload/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4 → Remote URL: /upload/2026/2/11/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:06 视频上传完成, 总耗时: 522.634464ms
2026/02/11 17:01:06 Nginx上传成功: /upload/2026/2/11/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4
2026/02/11 17:01:06 Loaded 1 device configurations from service config file
2026/02/11 17:01:06 报警事件已上报 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, Level=fire_leave)
2026/02/11 17:01:06 Set temperature threshold from config: 20.0°C (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:06 Loaded 1 device configurations from service config file
2026/02/11 17:01:06 Sending WebSocket message: {"method":"metric_data","params":{"metric":"{\"type\":\"event\",\"args\":{\"address\":\"地址2\",\"alarm_time\":1770800445,\"description\":\"无人工作区且温度超阈值报警\",\"device_uuid\":\"3eeba8usb86c0gs448oktxay41a0rm1v\",\"host_uuid\":\"qbxmjyzrkpntfgswaevodhluicqzxplkm\",\"level\":\"fire_leave\",\"video_url\":\"/upload/2026/2/11/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4\"}}","route_key":"/dhlr/device/3eeba8usb86c0gs448oktxay41a0rm1v/event"}}
2026/02/11 17:01:06 报警事件已上报 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, Level=fire_check)
2026/02/11 17:01:06 Asynchronous reporting successful
2026/02/11 17:01:06 数据快照已上报 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:06 按指定级别上报事件完成 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, Level=fire_leave)
2026/02/11 17:01:06 Sending WebSocket message: {"method":"metric_data","params":{"metric":"{\"type\":\"data\",\"args\":{\"address\":\"地址2\",\"alarm_time\":1770800445,\"camera_ip\":\"192.168.80.64\",\"camera_rtsp\":\"rtsp://admin:JH11223344@192.168.80.64:554\",\"confidence\":70,\"person_count\":0,\"task_id\":\"3319\",\"temperature\":27.800003}}","route_key":"/dhlr/device/3eeba8usb86c0gs448oktxay41a0rm1v/data"}}
2026/02/11 17:01:06 Sending WebSocket message: {"method":"metric_data","params":{"metric":"{\"type\":\"event\",\"args\":{\"address\":\"地址2\",\"alarm_time\":1770800445,\"description\":\"检测到异常火苗\",\"device_uuid\":\"3eeba8usb86c0gs448oktxay41a0rm1v\",\"host_uuid\":\"qbxmjyzrkpntfgswaevodhluicqzxplkm\",\"level\":\"fire_check\",\"video_url\":\"/upload/2026/2/11/3eeba8usb86c0gs448oktxay41a0rm1v-2026-2-11-1770800440.MP4\"}}","route_key":"/dhlr/device/3eeba8usb86c0gs448oktxay41a0rm1v/event"}}
2026/02/11 17:01:06 数据快照已上报 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v)
2026/02/11 17:01:06 按指定级别上报事件完成 (DeviceUUID=3eeba8usb86c0gs448oktxay41a0rm1v, Level=fire_check)
2026/02/11 17:01:06 Sending WebSocket message: {"method":"metric_data","params":{"metric":"{\"type\":\"data\",\"args\":{\"address\":\"地址2\",\"alarm_time\":1770800445,\"camera_ip\":\"192.168.80.64\",\"camera_rtsp\":\"rtsp://admin:JH11223344@192.168.80.64:554\",\"confidence\":70,\"person_count\":0,\"task_id\":\"3319\",\"temperature\":27.800003}}","route_key":"/dhlr/device/3eeba8usb86c0gs448oktxay41a0rm1v/data"}}
BIN
View File
Binary file not shown.
+234
View File
@@ -0,0 +1,234 @@
package gpio_monitor
import (
"context"
"fmt"
"os"
"strconv"
"time"
"FireLeave_tool/logger"
"FireLeave_tool/reporter"
)
const (
// ButtonGPIO GPIO引脚编号
ButtonGPIO = 111
// DebounceTime 去抖动时间
DebounceTime = 200 * time.Millisecond
)
// GPIOButtonMonitor GPIO按钮监视器
type GPIOButtonMonitor struct {
gpioPin int
isRunning bool
lastState bool
lastChange time.Time
lastAlertTime time.Time
cooldownTime time.Duration
cooldownActive bool
}
// NewGPIOButtonMonitor 创建GPIO按钮监视器
func NewGPIOButtonMonitor(gpioPin int) *GPIOButtonMonitor {
return &GPIOButtonMonitor{
gpioPin: gpioPin,
isRunning: false,
lastState: false,
lastChange: time.Now(),
lastAlertTime: time.Now().Add(-6 * time.Second), // 初始化为6秒前,确保第一次可以触发
cooldownTime: 5 * time.Second, // 5秒冷却时间
cooldownActive: false, // 冷却期状态
}
}
// InitGPIO 初始化GPIO
func (m *GPIOButtonMonitor) InitGPIO() error {
gpioPath := fmt.Sprintf("/sys/class/gpio/gpio%d", m.gpioPin)
// 检查GPIO是否已导出
if _, err := os.Stat(gpioPath); os.IsNotExist(err) {
// 导出GPIO
if err := os.WriteFile("/sys/class/gpio/export", []byte(strconv.Itoa(m.gpioPin)), 0666); err != nil {
return fmt.Errorf("failed to export GPIO %d: %v", m.gpioPin, err)
}
// 等待导出完成
time.Sleep(50 * time.Millisecond)
}
// 检查并设置方向为输入
directionPath := fmt.Sprintf("%s/direction", gpioPath)
directionBytes, err := os.ReadFile(directionPath)
if err != nil {
return fmt.Errorf("failed to read GPIO direction: %v", err)
}
direction := string(directionBytes)
if direction != "in\n" && direction != "in" {
if err := os.WriteFile(directionPath, []byte("in"), 0666); err != nil {
return fmt.Errorf("failed to set GPIO direction to in: %v", err)
}
// 等待设置完成
time.Sleep(10 * time.Millisecond)
}
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("GPIO %d initialized successfully as input", m.gpioPin)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("GPIO %d initialized successfully as input", m.gpioPin)
}
return nil
}
// ReadGPIOValue 读取GPIO值
func (m *GPIOButtonMonitor) ReadGPIOValue() (bool, error) {
valuePath := fmt.Sprintf("/sys/class/gpio/gpio%d/value", m.gpioPin)
valueBytes, err := os.ReadFile(valuePath)
if err != nil {
return false, fmt.Errorf("failed to read GPIO value: %v", err)
}
value := string(valueBytes)
return value == "1\n" || value == "1", nil
}
// Start 启动GPIO监控
func (m *GPIOButtonMonitor) Start(ctx context.Context) error {
// 初始化GPIO
if err := m.InitGPIO(); err != nil {
return fmt.Errorf("failed to initialize GPIO: %v", err)
}
m.isRunning = true
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("Started monitoring GPIO %d for button alerts", m.gpioPin)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("Started monitoring GPIO %d for button alerts", m.gpioPin)
}
go func() {
defer func() {
m.isRunning = false
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("Stopped monitoring GPIO %d", m.gpioPin)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("Stopped monitoring GPIO %d", m.gpioPin)
}
}()
for {
select {
case <-ctx.Done():
return
default:
// 读取GPIO值
currentState, err := m.ReadGPIOValue()
if err != nil {
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("Error reading GPIO %d value: %v", m.gpioPin, err)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("Error reading GPIO %d value: %v", m.gpioPin, err)
}
time.Sleep(100 * time.Millisecond)
continue
}
// 检查状态是否变化
if currentState != m.lastState {
// 记录状态变化
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("GPIO %d state changed: %t → %t", m.gpioPin, m.lastState, currentState)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("GPIO %d state changed: %t → %t", m.gpioPin, m.lastState, currentState)
}
// 状态变化时更新lastChange
m.lastChange = time.Now()
// 立即更新lastState,避免重复触发状态变化日志
m.lastState = currentState
// 只输出true状态
if currentState {
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("GPIO %d current state: %t", m.gpioPin, currentState)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("GPIO %d current state: %t", m.gpioPin, currentState)
}
}
}
// 检查冷却期状态
if time.Since(m.lastAlertTime) < m.cooldownTime {
if !m.cooldownActive {
m.cooldownActive = true
// 只记录一次冷却期开始
}
} else {
if m.cooldownActive {
// 只记录一次冷却期结束
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("GPIO %d cooldown period ended", m.gpioPin)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("GPIO %d cooldown period ended", m.gpioPin)
}
m.cooldownActive = false
}
}
// 检查是否稳定,且当前状态为true(高电平)
if currentState && time.Since(m.lastChange) > DebounceTime {
// 检查冷却期
if time.Since(m.lastAlertTime) >= m.cooldownTime {
// 检测到稳定的高电平,切换事件周期
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("GPIO %d detected stable high level after debounce, switching event cycle...", m.gpioPin)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("GPIO %d detected stable high level after debounce, switching event cycle...", m.gpioPin)
}
// 切换事件周期
var targetCycle reporter.EventCycle
switch reporter.GetCurrentEventCycle() {
case reporter.EventCycleFireLeave:
targetCycle = reporter.EventCycleFireCheck
case reporter.EventCycleFireCheck:
targetCycle = reporter.EventCycleFireLeave
default:
targetCycle = reporter.EventCycleFireCheck
}
if err := reporter.SwitchEventCycle(targetCycle); err != nil {
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("Failed to switch event cycle: %v", err)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("Failed to switch event cycle: %v", err)
}
}
// 更新lastAlertTime,开始冷却期
m.lastAlertTime = time.Now()
if log := logger.GlobalLoggerManager.GetLogger("gpio"); log != nil {
log.Printf("GPIO %d alert sent, cooldown period started (5s)", m.gpioPin)
} else if log := logger.GlobalLoggerManager.GetLogger("reporter"); log != nil {
log.Printf("GPIO %d alert sent, cooldown period started (5s)", m.gpioPin)
}
// 重置lastChange,避免重复触发
m.lastChange = time.Now()
}
}
// 增加休眠时间,减少CPU占用和日志频率
time.Sleep(100 * time.Millisecond)
}
}
}()
return nil
}
// IsRunning 检查监视器是否正在运行
func (m *GPIOButtonMonitor) IsRunning() bool {
return m.isRunning
}
+177 -7
View File
@@ -14,19 +14,114 @@ var logFile *os.File
var logFileName string
var mu sync.Mutex
// InitLogger 初始化日志,创建 /data/logs 目录并按天命名文件
// LoggerManager 日志管理器
type LoggerManager struct {
loggers map[string]*log.Logger
files map[string]*os.File
mu sync.RWMutex
}
// GlobalLoggerManager 全局日志管理器
var GlobalLoggerManager = &LoggerManager{
loggers: make(map[string]*log.Logger),
files: make(map[string]*os.File),
}
// CreateLogger 创建带有指定路径的日志实例
func (lm *LoggerManager) CreateLogger(name string, logDir string) (*log.Logger, error) {
lm.mu.Lock()
defer lm.mu.Unlock()
// 确保日志目录存在
if err := os.MkdirAll(logDir, 0755); err != nil {
return nil, err
}
// 按天生成日志文件名
logFileName := filepath.Join(logDir, name+"_"+time.Now().Format("20060102")+".log")
file, err := os.OpenFile(logFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, err
}
logger := log.New(file, "", log.LstdFlags)
lm.loggers[name] = logger
lm.files[name] = file
// 每天凌晨检查是否需要切换日志文件
go func() {
for {
// 每天检查一次(凌晨 00:05 执行)
now := time.Now()
next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 5, 0, 0, now.Location())
time.Sleep(time.Until(next))
lm.mu.Lock()
newFileName := filepath.Join(logDir, name+"_"+time.Now().Format("20060102")+".log")
if newFileName != logFileName {
newFile, err := os.OpenFile(newFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Printf("Failed to switch the log file: %v", err)
lm.mu.Unlock()
continue
}
file.Close()
file = newFile
logFileName = newFileName
logger = log.New(newFile, "", log.LstdFlags)
lm.loggers[name] = logger
lm.files[name] = newFile
logger.Println("The log file has been switched:", newFileName)
}
lm.mu.Unlock()
}
}()
return logger, nil
}
// GetLogger 获取指定名称的日志实例
func (lm *LoggerManager) GetLogger(name string) *log.Logger {
lm.mu.RLock()
defer lm.mu.RUnlock()
return lm.loggers[name]
}
// CloseLogger 关闭指定名称的日志文件
func (lm *LoggerManager) CloseLogger(name string) {
lm.mu.Lock()
defer lm.mu.Unlock()
if file, exists := lm.files[name]; exists {
file.Close()
delete(lm.files, name)
delete(lm.loggers, name)
}
}
// CloseAllLoggers 关闭所有日志文件
func (lm *LoggerManager) CloseAllLoggers() {
lm.mu.Lock()
defer lm.mu.Unlock()
for name, file := range lm.files {
file.Close()
delete(lm.files, name)
delete(lm.loggers, name)
}
}
// InitLogger 初始化系统日志,创建 /data/logs/system 目录并按天命名文件
func InitLogger() error {
mu.Lock()
defer mu.Unlock()
// 创建 /data/logs 目录
logDir := "/data/logs"
// 创建 /data/logs/system 目录
logDir := "/data/logs/system"
if err := os.MkdirAll(logDir, 0755); err != nil {
return err
}
// 按天生成日志文件名:20260102.log
logFileName = filepath.Join(logDir, time.Now().Format("20060102")+".log")
// 按天生成日志文件名:system_20260102.log
logFileName = filepath.Join(logDir, "system_"+time.Now().Format("20060102")+".log")
file, err := os.OpenFile(logFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
@@ -43,7 +138,74 @@ func InitLogger() error {
time.Sleep(time.Until(next))
mu.Lock()
newFileName := filepath.Join(logDir, time.Now().Format("20060102")+".log")
newFileName := filepath.Join(logDir, "system_"+time.Now().Format("20060102")+".log")
if newFileName != logFileName {
newFile, err := os.OpenFile(newFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Printf("Failed to switch the log file: %v", err)
mu.Unlock()
continue
}
logFile.Close()
logFile = newFile
logFileName = newFileName
Logger = log.New(newFile, "", log.LstdFlags)
Logger.Println("The log file has been switched:", newFileName)
}
mu.Unlock()
}
}()
return nil
}
// InitReporterLogger 初始化上报管理器专用日志,创建 /data/logs/reporter 目录并按天命名文件
func InitReporterLogger() error {
// 创建上报管理器专用日志
_, err := GlobalLoggerManager.CreateLogger("reporter", "/data/logs/reporter")
return err
}
// InitGPILogger 初始化GPIO监控专用日志,创建 /data/logs/reporter 目录并按天命名文件
func InitGPILogger() error {
// 创建GPIO监控专用日志
_, err := GlobalLoggerManager.CreateLogger("gpio", "/data/logs/reporter")
return err
}
// InitLoggerWithPath 初始化日志,使用指定的日志文件路径
func InitLoggerWithPath(logPath string) error {
mu.Lock()
defer mu.Unlock()
// 创建日志目录
logDir := filepath.Dir(logPath)
if err := os.MkdirAll(logDir, 0755); err != nil {
return err
}
// 打开指定的日志文件
logFileName = logPath
file, err := os.OpenFile(logFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
logFile = file
Logger = log.New(file, "", log.LstdFlags)
// 每天凌晨检查是否需要切换日志文件
go func() {
for {
// 每天检查一次(凌晨 00:05 执行)
now := time.Now()
next := time.Date(now.Year(), now.Month(), now.Day()+1, 0, 5, 0, 0, now.Location())
time.Sleep(time.Until(next))
mu.Lock()
// 生成新的日志文件名,保留原有文件名的前缀和添加日期后缀
baseName := filepath.Base(logPath)
ext := filepath.Ext(baseName)
nameWithoutExt := baseName[:len(baseName)-len(ext)]
newFileName := filepath.Join(logDir, nameWithoutExt+"_"+time.Now().Format("20060102")+ext)
if newFileName != logFileName {
newFile, err := os.OpenFile(newFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
@@ -75,7 +237,15 @@ func CloseLogger() {
// CleanupOldLogs 删除超过30天的日志文件
func CleanupOldLogs() {
logDir := "/data/logs"
// 清理system目录的日志文件
cleanupLogDirectory("/data/logs/system")
// 清理reporter目录的日志文件
cleanupLogDirectory("/data/logs/reporter")
}
// cleanupLogDirectory 清理指定目录下超过30天的日志文件
func cleanupLogDirectory(logDir string) {
files, err := os.ReadDir(logDir)
if err != nil {
if Logger != nil {
BIN
View File
Binary file not shown.
+351 -173
View File
@@ -2,8 +2,10 @@ package main
import (
"FireLeave_tool/connect"
"FireLeave_tool/gpio_monitor"
"FireLeave_tool/httpreader"
"FireLeave_tool/logger"
"FireLeave_tool/reporter"
"FireLeave_tool/tcpreader"
"FireLeave_tool/video_server"
"context"
@@ -18,6 +20,7 @@ import (
"os/exec"
"os/signal"
"path/filepath"
"strconv"
"sync"
"syscall"
"time"
@@ -27,12 +30,18 @@ type deviceAlarmState struct {
InCondition bool
StartTime int64
AlarmPauseUntil int64
// fire_check火焰检测状态
FireCheckInCondition bool
FireCheckStartTime int64
}
// 全局 map:每个设备独立状态
var deviceStates sync.Map // map[string]*deviceAlarmState
var mergingAlarms sync.Map
// 全局上报管理器
var GlobalReporter = reporter.NewReporter()
// DeviceManager 设备管理器
type DeviceManager struct {
mu sync.Mutex
@@ -193,91 +202,172 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
deviceCtx, cancel := context.WithCancel(ctx)
defer cancel()
// 分配端口
port, err := connect.GlobalPortManager.AllocatePort(deviceData.DeviceUUID)
// 分配端口 - fire_leave推理程序
portFireLeave, err := connect.GlobalPortManager.AllocatePort(deviceData.DeviceUUID)
if err != nil {
logger.Logger.Printf(" DeviceUUID=%s Port allocation failed: %v", deviceData.DeviceUUID, err)
return
}
defer connect.GlobalPortManager.ReleasePort(deviceData.DeviceUUID)
logger.Logger.Printf("Port allocation successful: DeviceUUID=%s, Port=%d", deviceData.DeviceUUID, port)
inferenceServer := httpreader.NewInferenceServer(deviceData.DeviceUUID, port)
if err := inferenceServer.Start(); err != nil {
logger.Logger.Printf("Failed to start the inference data receiving server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
// 分配端口 - fire_check推理程序
portFireCheck, err := connect.GlobalPortManager.AllocatePort(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
if err != nil {
logger.Logger.Printf(" DeviceUUID=%s Fire check port allocation failed: %v", deviceData.DeviceUUID, err)
return
}
defer connect.GlobalPortManager.ReleasePort(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
logger.Logger.Printf("Port allocation successful: DeviceUUID=%s, FireLeavePort=%d, FireCheckPort=%d", deviceData.DeviceUUID, portFireLeave, portFireCheck)
// 创建fire_leave推理数据接收服务器
inferenceServerFireLeave := httpreader.NewInferenceServer(deviceData.DeviceUUID, portFireLeave)
if err := inferenceServerFireLeave.Start(); err != nil {
logger.Logger.Printf("Failed to start fire_leave inference server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 创建fire_check推理数据接收服务器
inferenceServerFireCheck := httpreader.NewInferenceServer(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), portFireCheck)
if err := inferenceServerFireCheck.Start(); err != nil {
logger.Logger.Printf("Failed to start fire_check inference server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 使用带上下文的停止
defer inferenceServer.StopWithContext(deviceCtx)
logger.Logger.Printf("Wait for the inference data receiving server to start... (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, port)
defer inferenceServerFireLeave.StopWithContext(deviceCtx)
defer inferenceServerFireCheck.StopWithContext(deviceCtx)
// 等待HTTP服务器启动
if !waitForHTTPService(port, 10*time.Second) {
logger.Logger.Printf("The inference data receiving server has timed out during startup (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, port)
if !waitForHTTPService(portFireLeave, 10*time.Second) {
logger.Logger.Printf("Fire_leave inference server startup timeout (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, portFireLeave)
return
}
logger.Logger.Printf("The inference data receiving server has been successfully started (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, port)
if !waitForHTTPService(portFireCheck, 10*time.Second) {
logger.Logger.Printf("Fire_check inference server startup timeout (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, portFireCheck)
return
}
logger.Logger.Printf("Inference servers started successfully: DeviceUUID=%s, FireLeavePort=%d, FireCheckPort=%d", deviceData.DeviceUUID, portFireLeave, portFireCheck)
// 处理检测区域为推理程序需要的格式
detectAreaStr := connect.ProcessDetectAreaForInference(deviceData.DetectArea)
logger.Logger.Printf("The processed detection area: %s", detectAreaStr)
// 为fire_leave推理程序获取检测区域
fireLeaveDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireLeaveDetectArea)
logger.Logger.Printf("The processed fire_leave detection area: %s", fireLeaveDetectAreaStr)
// 启动推理程序
inferenceCmd := exec.Command(
// 为fire_check推理程序获取检测区域
fireCheckDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireCheckDetectArea)
logger.Logger.Printf("The processed fire_check detection area: %s", fireCheckDetectAreaStr)
// 启动fire_leave推理程序
inferenceCmdFireLeave := exec.Command(
"./yolov5",
"-s", deviceData.CameraRTSP,
"-m", "model",
"-c", deviceData.DeviceUUID,
"-p", fmt.Sprintf("%d", port),
"-w", detectAreaStr,
"-p", fmt.Sprintf("%d", portFireLeave),
"-w", fireLeaveDetectAreaStr,
"-r", fmt.Sprintf("%d", deviceData.Confidence),
)
inferenceCmd.Stdout = os.Stdout
inferenceCmd.Stderr = os.Stderr
//logger.Logger.Printf("推理程序输出仅显示在控制台")
inferenceCmdFireLeave.Stdout = os.Stdout
inferenceCmdFireLeave.Stderr = os.Stderr
logger.Logger.Printf("Start the inference program command: %v", inferenceCmd.Args)
logger.Logger.Printf("Start fire_leave inference program command: %v", inferenceCmdFireLeave.Args)
if err := inferenceCmd.Start(); err != nil {
logger.Logger.Printf("推理程序启动失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
if err := inferenceCmdFireLeave.Start(); err != nil {
logger.Logger.Printf("Fire_leave inference program startup failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 关键:保存进程对象到全局 map
connect.InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmd)
// 保存fire_leave进程对象到全局 map
connect.InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmdFireLeave)
logger.Logger.Printf("推理程序已启动,PID=%d (DeviceUUID=%s)", inferenceCmd.Process.Pid, deviceData.DeviceUUID)
logger.Logger.Printf("Fire_leave inference program started, PID=%d (DeviceUUID=%s)", inferenceCmdFireLeave.Process.Pid, deviceData.DeviceUUID)
// 启动fire_check推理程序
inferenceCmdFireCheck := exec.Command(
"./yolov5",
"-s", deviceData.CameraRTSP,
"-m", "fire_check",
"-c", fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID),
"-p", fmt.Sprintf("%d", portFireCheck),
"-w", fireCheckDetectAreaStr,
"-r", fmt.Sprintf("%d", deviceData.Confidence),
"-R",
)
inferenceCmdFireCheck.Stdout = os.Stdout
inferenceCmdFireCheck.Stderr = os.Stderr
logger.Logger.Printf("Start fire_check inference program command: %v", inferenceCmdFireCheck.Args)
if err := inferenceCmdFireCheck.Start(); err != nil {
logger.Logger.Printf("Fire_check inference program startup failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// 保存fire_check进程对象到全局 map
connect.InferenceProcesses.Store(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID), inferenceCmdFireCheck)
logger.Logger.Printf("Fire_check inference program started, PID=%d (DeviceUUID=%s)", inferenceCmdFireCheck.Process.Pid, deviceData.DeviceUUID)
// 初始化心跳
connect.UpdateInferenceHeartbeat(deviceData.DeviceUUID)
connect.UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
// 检查推理程序是否启动成功
time.Sleep(2 * time.Second)
if inferenceCmd.Process == nil {
logger.Logger.Printf("The reasoning program process has not been created (DeviceUUID=%s)", deviceData.DeviceUUID)
// 检查fire_leave推理程序
if inferenceCmdFireLeave.Process == nil {
logger.Logger.Printf("Fire_leave inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID)
return
}
if err := inferenceCmd.Process.Signal(syscall.Signal(0)); err != nil {
logger.Logger.Printf("The reasoning program has exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
if err := inferenceCmdFireLeave.Process.Signal(syscall.Signal(0)); err != nil {
logger.Logger.Printf("Fire_leave inference program exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
logger.Logger.Printf("The reasoning program process has been startedPID: %d (DeviceUUID=%s)", inferenceCmd.Process.Pid, deviceData.DeviceUUID)
// 检查fire_check推理程序
if inferenceCmdFireCheck.Process == nil {
logger.Logger.Printf("Fire_check inference program process not created (DeviceUUID=%s)", deviceData.DeviceUUID)
return
}
if err := logProcessInfo(inferenceCmd.Process.Pid, deviceData.DeviceUUID); err != nil {
logger.Logger.Printf("Failed to record process information: %v", err)
if err := inferenceCmdFireCheck.Process.Signal(syscall.Signal(0)); err != nil {
logger.Logger.Printf("Fire_check inference program exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
logger.Logger.Printf("Both inference programs started successfully: DeviceUUID=%s", deviceData.DeviceUUID)
if err := logProcessInfo(inferenceCmdFireLeave.Process.Pid, deviceData.DeviceUUID); err != nil {
logger.Logger.Printf("Failed to record fire_leave process information: %v", err)
}
if err := logProcessInfo(inferenceCmdFireCheck.Process.Pid, fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID)); err != nil {
logger.Logger.Printf("Failed to record fire_check process information: %v", err)
}
defer func() {
logger.Logger.Printf("Start to stop the reasoning program (DeviceUUID=%s)", deviceData.DeviceUUID)
if inferenceCmd.Process != nil {
if err := inferenceCmd.Process.Kill(); err != nil {
logger.Logger.Printf("The termination of the reasoning program failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
logger.Logger.Printf("Start to stop inference programs (DeviceUUID=%s)", deviceData.DeviceUUID)
// 停止fire_leave推理程序
if inferenceCmdFireLeave.Process != nil {
if err := inferenceCmdFireLeave.Process.Kill(); err != nil {
logger.Logger.Printf("Fire_leave inference program termination failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
} else {
logger.Logger.Printf("The reasoning program has stopped (DeviceUUID=%s)", deviceData.DeviceUUID)
logger.Logger.Printf("Fire_leave inference program stopped (DeviceUUID=%s)", deviceData.DeviceUUID)
}
}
// 停止fire_check推理程序
if inferenceCmdFireCheck.Process != nil {
if err := inferenceCmdFireCheck.Process.Kill(); err != nil {
logger.Logger.Printf("Fire_check inference program termination failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
} else {
logger.Logger.Printf("Fire_check inference program stopped (DeviceUUID=%s)", deviceData.DeviceUUID)
}
}
}()
@@ -285,7 +375,7 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
// 启动温度程序
// 在 processDevice 函数中修改温度程序启动部分
// 启动温度程序
tempPort := port + 1000
tempPort := portFireLeave + 1000
tempCmdArgs := []string{
"-s", deviceData.CameraRTSP,
"-t", fmt.Sprintf("%d", tempPort),
@@ -371,8 +461,11 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
case <-deviceCtx.Done():
logger.Logger.Printf("Equipment processing has been cancelled: DeviceUUID=%s", deviceData.DeviceUUID)
// 强制停止所有子进程
if inferenceCmd.Process != nil {
inferenceCmd.Process.Kill()
if inferenceCmdFireLeave.Process != nil {
inferenceCmdFireLeave.Process.Kill()
}
if inferenceCmdFireCheck.Process != nil {
inferenceCmdFireCheck.Process.Kill()
}
if tempCmd.Process != nil {
tempCmd.Process.Kill()
@@ -380,9 +473,9 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
tempClient.Close()
return
case inferenceResult, ok := <-inferenceServer.GetDataChan():
case inferenceResult, ok := <-inferenceServerFireLeave.GetDataChan():
if !ok {
logger.Logger.Printf("The inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID)
logger.Logger.Printf("The fire_leave inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID)
return
}
currentPersonCount := 0
@@ -399,6 +492,62 @@ 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():
if !ok {
logger.Logger.Printf("The fire_check inference data channel is closed: DeviceUUID=%s", deviceData.DeviceUUID)
return
}
// 处理fire_check推理结果
logger.Logger.Printf("Received fire_check inference data: DeviceUUID=%s", deviceData.DeviceUUID)
// 检查是否检测到火焰
fireDetected := false
for _, param := range fireCheckResult.Params {
if param.ClassIdx == 0 && param.Number > 0 { // 假设ClassIdx=0表示火焰
fireDetected = true
logger.Logger.Printf("Fire detected! DeviceUUID=%s, Number=%d", deviceData.DeviceUUID, param.Number)
break
}
}
// 处理火焰检测的连续时间判断
state := getDeviceState(deviceData.DeviceUUID)
now := time.Now().Unix()
// 解析detection_time配置
detectionSec := 2 // 默认2秒
if deviceData.FireCheckParam.DetectionTime != "" {
if sec, err := strconv.Atoi(deviceData.FireCheckParam.DetectionTime); err == nil && sec > 0 {
detectionSec = sec
}
}
if fireDetected {
if !state.FireCheckInCondition {
// 开始计时
state.FireCheckStartTime = now
state.FireCheckInCondition = true
logger.Logger.Printf("Fire detection started, timing for %ds: DeviceUUID=%s", detectionSec, deviceData.DeviceUUID)
} else if now-state.FireCheckStartTime >= int64(detectionSec) {
// 连续检测到火焰达到指定时间,触发上报
logger.Logger.Printf("Fire detected continuously for %ds, triggering alarm: DeviceUUID=%s", detectionSec, deviceData.DeviceUUID)
go func() {
alarmTime := time.Now().Unix()
logger.Logger.Printf("[%.3f] FIRE DETECTED ALARM TRIGGERED (DeviceUUID=%s)", time.Since(time.Now()).Seconds(), deviceData.DeviceUUID)
// 调用专门的火焰检测上报函数
GlobalReporter.TriggerFireCheckAlarm(deviceData, alarmTime)
}()
// 重置状态
state.FireCheckInCondition = false
}
} else {
// 没有检测到火焰,重置状态
if state.FireCheckInCondition {
logger.Logger.Printf("Fire detection interrupted: lasted %ds < %ds (DeviceUUID=%s)", now-state.FireCheckStartTime, detectionSec, deviceData.DeviceUUID)
state.FireCheckInCondition = false
}
}
case <-monitorTicker.C:
now := time.Now().Unix()
@@ -428,6 +577,7 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
// 温度防抖
var tempStableHigh bool
threshold := deviceData.TemperatureThreshold
if deviceData.Temperature > threshold+0.5 {
tempStableHigh = true
} else if deviceData.Temperature < threshold-0.5 {
@@ -456,17 +606,42 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
logger.Logger.Printf("Alarm condition met, start timing (%ds required): PersonCount=%d, Temp=%.1f°C > %.1f°C (DeviceUUID=%s)",
detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
} else if now-state.StartTime >= int64(detectionSec) {
logger.Logger.Printf("ALARM TRIGGERED (after %ds): PersonCount=%d, Temp=%.1f°C > %.1f°C (DeviceUUID=%s)",
detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
currentTime := time.Now()
logger.Logger.Printf("[%d] ALARM TRIGGERED (after %ds): PersonCount=%d, Temp=%.1f°C > %.1f°C (DeviceUUID=%s)",
currentTime.Unix(), detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
go triggerAlarmWithDelay(deviceData, state.StartTime, alarmCooldown)
// 异步处理报警,避免阻塞主线程
go func() {
alarmStartTime := time.Now()
logger.Logger.Printf("[%d] 开始异步处理报警 (DeviceUUID=%s, alarmTime=%d, cooldown=%d)",
alarmStartTime.Unix(), deviceData.DeviceUUID, state.StartTime, alarmCooldown)
// 捕获所有异常,确保goroutine不会因为panic而崩溃
defer func() {
if err := recover(); err != nil {
logger.Logger.Printf("[%d] 处理报警发生panic: %v (DeviceUUID=%s)",
time.Now().Unix(), err, deviceData.DeviceUUID)
}
}()
// 调用报警处理函数
GlobalReporter.TriggerAlarmWithDelay(deviceData, state.StartTime, alarmCooldown)
logger.Logger.Printf("[%d] 报警处理完成 (DeviceUUID=%s, 耗时:%.3fs)",
time.Now().Unix(), deviceData.DeviceUUID, time.Since(alarmStartTime).Seconds())
}()
// 更新状态
state.AlarmPauseUntil = now + int64(alarmCooldown)
state.InCondition = false
logger.Logger.Printf("[%d] 更新报警状态: 冷却时间设置为 %d 秒 (DeviceUUID=%s)",
time.Now().Unix(), alarmCooldown, deviceData.DeviceUUID)
// 更新设备数据
deviceDataCopy := deviceData
deviceDataCopy.AlarmPauseUntil = state.AlarmPauseUntil
logger.Logger.Printf("[%d] 更新设备数据 (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID)
connect.UpdateDeviceData(deviceDataCopy)
logger.Logger.Printf("[%d] 设备数据更新完成 (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID)
}
} else {
if state.InCondition {
@@ -485,10 +660,13 @@ func waitForShutdown(ctx context.Context, cancel context.CancelFunc, wg *sync.Wa
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
select {
case <-stop:
logger.Logger.Printf("Upon receiving the stop signal, start cleaning...")
case sig := <-stop:
logger.Logger.Printf("Received stop signal: %v, start cleaning...", sig)
// 立即取消上下文,通知所有goroutine停止
cancel()
// 等待所有goroutine退出
done := make(chan struct{})
go func() {
wg.Wait()
@@ -498,14 +676,16 @@ func waitForShutdown(ctx context.Context, cancel context.CancelFunc, wg *sync.Wa
select {
case <-done:
logger.Logger.Printf("All goroutines exited normally")
case <-time.After(10 * time.Second):
logger.Logger.Printf("When the waiting time exceeds the limit, force exit")
// Log status before force exit
activeDevices := GlobalDeviceManager.GetAllDevices()
logger.Logger.Printf("It still exists when forced to exit %d An active device", len(activeDevices))
for uuid := range activeDevices {
logger.Logger.Printf("Forcibly stop the equipment: UUID=%s", uuid)
case <-time.After(15 * time.Second):
logger.Logger.Printf("Waiting time exceeded, force exit")
// 强制终止所有推理进程
connect.InferenceProcesses.Range(func(key, value interface{}) bool {
if cmd, ok := value.(*exec.Cmd); ok && cmd.Process != nil {
logger.Logger.Printf("Force kill process: %v", key)
cmd.Process.Kill()
}
return true
})
}
case <-ctx.Done():
@@ -553,12 +733,24 @@ 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
if err := connect.SubscribeTopic("/dhlr/alert", connect.HandleBuzzerAlert); err != nil {
logger.Logger.Printf("订阅告警topic失败 (/dhlr/alert): %v", err)
} else {
logger.Logger.Printf("成功订阅本地告警topic: /dhlr/alert")
}
// 新增:订阅禁止时间 topic
if err := connect.SubscribeTopic("/dhlr/forbid_time", handleForbidTimeMessage); err != nil {
logger.Logger.Printf("订阅禁止时间topic失败 (/dhlr/forbid_time): %v", err)
} else {
logger.Logger.Printf("成功订阅禁止时间topic: /dhlr/forbid_time")
}
}
func (dm *DeviceManager) GetAllDevices() map[string]*connect.DeviceData {
@@ -571,124 +763,7 @@ func (dm *DeviceManager) GetAllDevices() map[string]*connect.DeviceData {
}
return result
}
func jsonString(v any) string {
b, err := json.Marshal(v)
if err != nil {
logger.Logger.Printf("JSON编码失败: %v", err)
return ""
}
return string(b)
}
// main.go → triggerAlarmWithDelay
func triggerAlarmWithDelay(deviceData connect.DeviceData, alarmTime int64, cooldown int) {
key := fmt.Sprintf("%s_%d", deviceData.DeviceUUID, alarmTime)
if _, loaded := mergingAlarms.LoadOrStore(key, true); loaded {
return
}
defer mergingAlarms.Delete(key)
// === 1. 合并视频 ===
mergedFile, err := video_server.MergeVideo(deviceData.DeviceUUID, alarmTime)
if err != nil {
logger.Logger.Printf("视频合并失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
return
}
// === 2. 上传 Nginx ===
var hostUUID string
configs, err := connect.LoadServiceConfig()
if err == nil {
for _, cfg := range configs {
if cfg.DeviceUUID == deviceData.DeviceUUID {
hostUUID = cfg.HostUUID
break
}
}
}
eventParamsForNginx := map[string]any{
"alarm_time": alarmTime,
"description": "无人工作区且温度超阈值报警",
"device_UUID": deviceData.DeviceUUID,
"host_uuid": hostUUID,
"person_count": deviceData.PersonCount,
"temperature": deviceData.Temperature,
}
videoURL, err := video_server.UploadToNginx(mergedFile, eventParamsForNginx, deviceData.UploadURL)
videoURLStr := ""
if err != nil {
logger.Logger.Printf("Nginx上传失败: %v", err)
} else {
videoURLStr = videoURL
logger.Logger.Printf("Nginx上传成功: %s", videoURL)
}
client := connect.GetWSClient()
if client == nil || !client.IsConnected() {
logger.Logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID)
os.Remove(mergedFile)
return
}
// === 3. 上报事件 (event) ===
eventPayload := map[string]any{
"type": "event",
"args": map[string]any{
"level": "alarm",
"description": "无人工作区且温度超阈值报警",
"device_uuid": deviceData.DeviceUUID,
"host_uuid": hostUUID,
"video_url": videoURLStr,
"alarm_time": alarmTime,
"address": deviceData.Address,
},
}
eventMsg := map[string]any{
"method": "metric_data",
"params": map[string]any{
"route_key": fmt.Sprintf("/dhlr/device/%s/event", deviceData.DeviceUUID),
"metric": jsonString(eventPayload),
},
}
client.SendAsync(eventMsg)
logger.Logger.Printf("The alarm incident has been reported (DeviceUUID=%s)", deviceData.DeviceUUID)
// === 4. 上报数据快照 (data) ===
dataPayload := map[string]any{
"type": "data",
"args": map[string]any{
"person_count": deviceData.PersonCount,
"temperature": deviceData.Temperature,
"camera_rtsp": deviceData.CameraRTSP,
"task_id": deviceData.TaskID,
"camera_ip": deviceData.CameraIP,
"confidence": deviceData.Confidence,
"alarm_time": alarmTime,
"address": deviceData.Address,
},
}
dataMsg := map[string]any{
"method": "metric_data",
"params": map[string]any{
"route_key": fmt.Sprintf("/dhlr/device/%s/data", deviceData.DeviceUUID),
"metric": jsonString(dataPayload),
},
}
client.SendAsync(dataMsg)
logger.Logger.Printf("指标快照已上报 (DeviceUUID=%s, Temp=%.1f°C, 人:%d)",
deviceData.DeviceUUID, deviceData.Temperature, deviceData.PersonCount)
// === 5. 清理本地文件 ===
os.Remove(mergedFile)
video_server.CleanupOldRawVideos(deviceData.DeviceUUID)
video_server.CleanupOldVideos(deviceData.DeviceUUID)
}
func (dm *DeviceManager) RangeDevices(fn func(uuid string, device *connect.DeviceData) bool) {
dm.mu.Lock()
defer dm.mu.Unlock()
@@ -710,8 +785,19 @@ func main() {
if err := logger.InitLogger(); err != nil {
log.Fatalf("Logger initialization failed: %v", err)
}
defer logger.CloseLogger()
// 初始化上报管理器专用日志
if err := logger.InitReporterLogger(); err != nil {
logger.Logger.Printf("Reporter logger initialization failed: %v", err)
}
// 初始化GPIO监控专用日志
if err := logger.InitGPILogger(); err != nil {
logger.Logger.Printf("GPIO logger initialization failed: %v", err)
}
go func() {
for {
logger.CleanupOldLogs()
@@ -752,6 +838,18 @@ func main() {
logger.Logger.Printf("Step 4: Asynchronously initialize WebSocket connection")
go initWebSocketAsync(ctx, &wg)
logger.Logger.Printf("Step 5: Start GPIO button monitoring")
go func() {
// 创建GPIO按钮监视器
monitor := gpio_monitor.NewGPIOButtonMonitor(gpio_monitor.ButtonGPIO)
// 启动GPIO监控
if err := monitor.Start(ctx); err != nil {
logger.Logger.Printf("Failed to start GPIO button monitor: %v", err)
} else {
logger.Logger.Printf("GPIO button monitor started successfully")
}
}()
logger.Logger.Printf("=== Main program startup completed ===")
logger.Logger.Printf("System status: Running devices=%d", len(deviceDataList))
@@ -763,13 +861,24 @@ 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("推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid)
logger.Logger.Printf("fire_leave推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid)
connect.StopInferenceProcess(uuid)
time.Sleep(6 * time.Second)
connect.StartInferenceProcess(device)
connect.UpdateInferenceHeartbeat(uuid)
}
// 检查fire_check推理程序心跳
fireCheckUUID := fmt.Sprintf("%s_fire_check", uuid)
if connect.ShouldRestartInference(fireCheckUUID) {
logger.Logger.Printf("fire_check推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid)
connect.StopInferenceProcess(fireCheckUUID)
time.Sleep(6 * time.Second)
connect.StartInferenceProcess(device)
connect.UpdateInferenceHeartbeat(fireCheckUUID)
}
return true
})
}
@@ -855,3 +964,72 @@ func debugDeviceData(deviceData connect.DeviceData) {
logger.Logger.Printf(" PersonCount: %d", deviceData.PersonCount)
logger.Logger.Printf(" Temperature: %f", deviceData.Temperature)
}
// handleForbidTimeMessage 处理禁止时间配置消息
func handleForbidTimeMessage(message []byte) {
logger.Logger.Printf("Received forbid time message: %s", string(message))
// 解析消息
var msg struct {
UUID string `json:"uuid"`
Topic string `json:"topic"`
QoS int `json:"qos"`
Content json.RawMessage `json:"content"`
Timeout int `json:"timeout"`
}
if err := json.Unmarshal(message, &msg); err != nil {
logger.Logger.Printf("Failed to parse forbid time message: %v", err)
return
}
// 解析content字段
var content struct {
Minute string `json:"minute"`
FireLeaveTimeout string `json:"fire_leave_timeout,omitempty"`
FireCheckTimeout string `json:"fire_check_timeout,omitempty"`
}
if err := json.Unmarshal(msg.Content, &content); err != nil {
logger.Logger.Printf("Failed to parse content: %v", err)
return
}
// 处理禁止时间配置
if content.Minute != "" {
logger.Logger.Printf("Forbid time config received: Minute=%s", content.Minute)
// 保存到缓存文件
if err := reporter.SaveForbidTimeConfig(content.Minute); err != nil {
logger.Logger.Printf("Failed to save forbid time config: %v", err)
} else {
logger.Logger.Printf("Forbid time config saved successfully: Minute=%s", content.Minute)
}
}
// 处理事件周期时限配置
if content.FireLeaveTimeout != "" || content.FireCheckTimeout != "" {
// 解析时间
fireLeaveTimeout, err1 := time.ParseDuration(content.FireLeaveTimeout)
fireCheckTimeout, err2 := time.ParseDuration(content.FireCheckTimeout)
// 如果解析失败,使用默认值
if err1 != nil {
fireLeaveTimeout = 30 * time.Minute
logger.Logger.Printf("Invalid fire_leave_timeout, using default: 30m0s")
}
if err2 != nil {
fireCheckTimeout = 30 * time.Minute
logger.Logger.Printf("Invalid fire_check_timeout, using default: 30m0s")
}
logger.Logger.Printf("Event cycle timeout config received: FireLeaveTimeout=%v, FireCheckTimeout=%v", fireLeaveTimeout, fireCheckTimeout)
// 保存到缓存文件
if err := reporter.SaveEventCycleTimeoutConfig(fireLeaveTimeout, fireCheckTimeout); err != nil {
logger.Logger.Printf("Failed to save event cycle timeout config: %v", err)
} else {
logger.Logger.Printf("Event cycle timeout config saved successfully: FireLeaveTimeout=%v, FireCheckTimeout=%v", fireLeaveTimeout, fireCheckTimeout)
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 883 KiB

View File
+278
View File
@@ -0,0 +1,278 @@
package portmanager
import (
"errors"
"fmt"
"net"
"sync"
"time"
"FireLeave_tool/logger"
)
// PortManager 管理端口分配
type PortManager struct {
mu sync.Mutex
allocated map[string]DevicePorts // DeviceUUID 到端口映射的映射
portPool []int // 推理端口池(HTTP
available map[int]bool // 可用端口标记
}
// NewPortManager 创建端口管理器
func NewPortManager() *PortManager {
// 初始化端口池
var ports []int
for port := BasePortStart; port <= BasePortEnd; port++ {
ports = append(ports, port)
}
// 初始化可用端口映射
available := make(map[int]bool)
for _, port := range ports {
available[port] = true
}
return &PortManager{
allocated: make(map[string]DevicePorts),
portPool: ports,
available: available,
}
}
// AllocatePort 为设备分配基础端口
func (pm *PortManager) AllocatePort(deviceUUID string) (int, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
// 检查设备是否已分配端口
if ports, exists := pm.allocated[deviceUUID]; exists {
logger.Logger.Printf("Device %s already allocated port: %d", deviceUUID, ports.Inference)
return ports.Inference, nil
}
// 查找可用端口
for _, port := range pm.portPool {
if !pm.available[port] {
continue // 端口已被分配
}
tempPort := port + TemperaturePortOffset // 温度端口(TCP
// 检查推理端口(HTTP)是否可用
inferenceLn, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
logger.Logger.Printf("Port %d (inference) is not available: %v", port, err)
continue
}
inferenceLn.Close()
// 检查温度端口(TCP)是否可用
tempLn, err := net.Listen("tcp", fmt.Sprintf(":%d", tempPort))
if err != nil {
logger.Logger.Printf("Port %d (temperature) is not available: %v", tempPort, err)
continue
}
tempLn.Close()
// 端口可用,记录分配
pm.allocated[deviceUUID] = DevicePorts{
Inference: port,
Temperature: tempPort,
}
pm.available[port] = false
logger.Logger.Printf("Port allocated for device %s: inference=%d, temperature=%d",
deviceUUID, port, tempPort)
return port, nil
}
return 0, errors.New("no available ports for inference and temperature")
}
// AllocateFireCheckPort 为火焰检测程序分配端口
func (pm *PortManager) AllocateFireCheckPort(deviceUUID string) (int, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
// 检查设备是否已分配端口
ports, exists := pm.allocated[deviceUUID]
if !exists {
// 先分配基础端口
_, err := pm.AllocatePort(deviceUUID)
if err != nil {
return 0, fmt.Errorf("failed to allocate base port: %v", err)
}
// 重新获取更新后的端口映射
ports = pm.allocated[deviceUUID]
}
// 计算火焰检测端口
fireCheckPort := ports.Inference + FireCheckPortOffset
// 检查火焰检测端口是否可用
fireCheckLn, err := net.Listen("tcp", fmt.Sprintf(":%d", fireCheckPort))
if err != nil {
logger.Logger.Printf("Port %d (fire_check) is not available: %v", fireCheckPort, err)
return 0, fmt.Errorf("fire check port %d not available: %v", fireCheckPort, err)
}
fireCheckLn.Close()
// 更新端口映射
ports.FireCheck = fireCheckPort
pm.allocated[deviceUUID] = ports
logger.Logger.Printf("Fire check port allocated for device %s: %d", deviceUUID, fireCheckPort)
return fireCheckPort, nil
}
// ReleasePort 释放设备端口
func (pm *PortManager) ReleasePort(deviceUUID string) {
pm.mu.Lock()
defer pm.mu.Unlock()
// 删除设备端口映射
if ports, exists := pm.allocated[deviceUUID]; exists {
delete(pm.allocated, deviceUUID)
pm.available[ports.Inference] = true // 标记基础端口为可用
logger.Logger.Printf("Port released for device %s: inference=%d, temperature=%d, fire_check=%d",
deviceUUID, ports.Inference, ports.Temperature, ports.FireCheck)
}
}
// GetPort 获取设备基础端口
func (pm *PortManager) GetPort(deviceUUID string) (int, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
if ports, exists := pm.allocated[deviceUUID]; exists {
return ports.Inference, nil
}
return 0, fmt.Errorf("device %s has no allocated port", deviceUUID)
}
// GetFireCheckPort 获取火焰检测程序端口
func (pm *PortManager) GetFireCheckPort(deviceUUID string) (int, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
if ports, exists := pm.allocated[deviceUUID]; exists {
if ports.FireCheck > 0 {
return ports.FireCheck, nil
}
return 0, fmt.Errorf("device %s has no allocated fire check port", deviceUUID)
}
return 0, fmt.Errorf("device %s has no allocated ports", deviceUUID)
}
// GetTemperaturePort 获取温度程序端口
func (pm *PortManager) GetTemperaturePort(deviceUUID string) (int, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
if ports, exists := pm.allocated[deviceUUID]; exists {
return ports.Temperature, nil
}
return 0, fmt.Errorf("device %s has no allocated ports", deviceUUID)
}
// GetAllocatedPorts 获取所有已分配的端口信息
func (pm *PortManager) GetAllocatedPorts() []PortInfo {
pm.mu.Lock()
defer pm.mu.Unlock()
var portInfos []PortInfo
now := time.Now().Unix()
for deviceUUID, ports := range pm.allocated {
// 基础推理端口
portInfos = append(portInfos, PortInfo{
DeviceUUID: deviceUUID,
PortType: PortTypeInference,
Port: ports.Inference,
AllocatedAt: now,
})
// 温度端口
portInfos = append(portInfos, PortInfo{
DeviceUUID: deviceUUID,
PortType: PortTypeTemperature,
Port: ports.Temperature,
AllocatedAt: now,
})
// 火焰检测端口
if ports.FireCheck > 0 {
portInfos = append(portInfos, PortInfo{
DeviceUUID: deviceUUID,
PortType: PortTypeFireCheck,
Port: ports.FireCheck,
AllocatedAt: now,
})
}
// 新推理端口
if ports.NewInference > 0 {
portInfos = append(portInfos, PortInfo{
DeviceUUID: deviceUUID,
PortType: PortTypeNewInference,
Port: ports.NewInference,
AllocatedAt: now,
})
}
}
return portInfos
}
// GetDevicePorts 获取设备的所有端口
func (pm *PortManager) GetDevicePorts(deviceUUID string) (DevicePorts, error) {
pm.mu.Lock()
defer pm.mu.Unlock()
if ports, exists := pm.allocated[deviceUUID]; exists {
return ports, nil
}
return DevicePorts{}, fmt.Errorf("device %s has no allocated ports", deviceUUID)
}
// IsPortAvailable 检查端口是否可用
func (pm *PortManager) IsPortAvailable(port int) bool {
pm.mu.Lock()
defer pm.mu.Unlock()
// 检查是否在端口池中
for _, p := range pm.portPool {
if p == port {
return pm.available[port]
}
}
// 对于非端口池中的端口,直接检查可用性
ln, err := net.Listen("tcp", fmt.Sprintf(":%d", port))
if err != nil {
return false
}
ln.Close()
return true
}
// GetAvailablePortCount 获取可用端口数量
func (pm *PortManager) GetAvailablePortCount() int {
pm.mu.Lock()
defer pm.mu.Unlock()
count := 0
for _, available := range pm.available {
if available {
count++
}
}
return count
}
// GlobalPortManager 全局端口管理器
var GlobalPortManager = NewPortManager()
+47
View File
@@ -0,0 +1,47 @@
package portmanager
// PortType 端口类型
type PortType string
// 端口类型常量
const (
PortTypeInference PortType = "inference"
PortTypeTemperature PortType = "temperature"
PortTypeFireCheck PortType = "fire_check"
PortTypeNewInference PortType = "new_inference"
)
// DevicePorts 设备端口映射
type DevicePorts struct {
Inference int `json:"inference"`
Temperature int `json:"temperature"`
FireCheck int `json:"fire_check,omitempty"`
NewInference int `json:"new_inference,omitempty"`
}
// PortInfo 端口信息
type PortInfo struct {
DeviceUUID string `json:"device_uuid"`
PortType PortType `json:"port_type"`
Port int `json:"port"`
AllocatedAt int64 `json:"allocated_at"`
}
// PortAllocation 端口分配结构
type PortAllocation struct {
DeviceUUID string `json:"device_uuid"`
Ports DevicePorts `json:"ports"`
AllocatedAt int64 `json:"allocated_at"`
}
// 端口常量
const (
// 基础端口池
BasePortStart = 58881
BasePortEnd = 58890
// 端口偏移
TemperaturePortOffset = 1000
FireCheckPortOffset = 2000
NewInferenceOffset = 3000
)
+870
View File
@@ -0,0 +1,870 @@
package reporter
import (
"encoding/json"
"fmt"
"log"
"os"
"sync"
"time"
"FireLeave_tool/connect"
"FireLeave_tool/logger"
"FireLeave_tool/video_server"
)
var (
// mergingAlarms 用于合并报警,避免重复处理
mergingAlarms sync.Map
// 事件周期状态
eventCycleMutex sync.RWMutex
currentEventCycle EventCycle
cycleTimeout time.Time
)
// EventCycle 事件周期类型
type EventCycle string
const (
// EventCycleFireLeave 动火离人上报周期
EventCycleFireLeave EventCycle = "fire_leave"
// EventCycleFireCheck 火焰检测上报周期
EventCycleFireCheck EventCycle = "fire_check"
// 周期时限缓存文件
CycleTimeoutFile = "/data/cache/event_cycle_timeout.json"
)
// Reporter 上报管理器
type Reporter struct {
wsClient *connect.WSClient
uploadURL string
logger *Logger
}
// Logger 上报管理器专用日志
type Logger struct {
*log.Logger
}
// NewLogger 创建上报管理器专用日志
func NewLogger() *Logger {
return &Logger{
Logger: logger.GlobalLoggerManager.GetLogger("reporter"),
}
}
// NewReporter 创建上报管理器
func NewReporter() *Reporter {
// 初始化默认事件周期
eventCycleMutex.Lock()
currentEventCycle = EventCycleFireLeave
eventCycleMutex.Unlock()
return &Reporter{
wsClient: connect.GetWSClient(),
uploadURL: "",
logger: NewLogger(),
}
}
// getLogger 获取日志实例,确保不为nil
func (r *Reporter) getLogger() *log.Logger {
if r.logger != nil && r.logger.Logger != nil {
return r.logger.Logger
}
// 如果logger未初始化,使用全局Logger
return logger.Logger
}
// GetCurrentEventCycle 获取当前事件周期
func GetCurrentEventCycle() EventCycle {
eventCycleMutex.RLock()
defer eventCycleMutex.RUnlock()
return currentEventCycle
}
// IsCycleTimeout 检查事件周期是否超时
func IsCycleTimeout() bool {
eventCycleMutex.RLock()
defer eventCycleMutex.RUnlock()
return time.Now().After(cycleTimeout)
}
// SwitchEventCycle 切换事件周期
func SwitchEventCycle(cycle EventCycle) error {
eventCycleMutex.Lock()
defer eventCycleMutex.Unlock()
// 读取周期时限
timeout, err := loadCycleTimeout()
if err != nil {
// 使用默认值
timeout = 30 * time.Minute
}
// 更新事件周期和超时时间
oldCycle := currentEventCycle
currentEventCycle = cycle
cycleTimeout = time.Now().Add(timeout)
// 记录日志
if logger := logger.GlobalLoggerManager.GetLogger("reporter"); logger != nil {
logger.Printf("事件周期切换: %s → %s, 时限: %v", oldCycle, cycle, timeout)
}
return nil
}
// CycleTimeoutConfig 周期时限配置
type CycleTimeoutConfig struct {
FireLeaveTimeout time.Duration `json:"fire_leave_timeout"`
FireCheckTimeout time.Duration `json:"fire_check_timeout"`
}
// loadCycleTimeout 加载周期时限
func loadCycleTimeout() (time.Duration, error) {
data, err := os.ReadFile(CycleTimeoutFile)
if err != nil {
return 0, err
}
var config CycleTimeoutConfig
if err := json.Unmarshal(data, &config); err != nil {
return 0, err
}
// 根据当前周期返回对应时限
switch currentEventCycle {
case EventCycleFireLeave:
return config.FireLeaveTimeout, nil
case EventCycleFireCheck:
return config.FireCheckTimeout, nil
default:
return 30 * time.Minute, nil
}
}
// SaveCycleTimeout 保存周期时限
func SaveCycleTimeout(config CycleTimeoutConfig) error {
data, err := json.Marshal(config)
if err != nil {
return err
}
// 确保目录存在
if err := os.MkdirAll("/data/cache", 0755); err != nil {
return err
}
return os.WriteFile(CycleTimeoutFile, data, 0644)
}
// ForbidTimeConfig 禁止时间配置
var (
// ForbidTimeFile 禁止时间缓存文件
ForbidTimeFile = "/data/cache/forbid_time_config.json"
)
// SaveForbidTimeConfig 保存禁止时间配置
func SaveForbidTimeConfig(minutr string) error {
config := struct {
Minutr string `json:"minutr"`
}{
Minutr: minutr,
}
data, err := json.Marshal(config)
if err != nil {
return err
}
// 确保目录存在
if err := os.MkdirAll("/data/cache", 0755); err != nil {
return err
}
return os.WriteFile(ForbidTimeFile, data, 0644)
}
// SaveEventCycleTimeoutConfig 保存事件周期时限配置
func SaveEventCycleTimeoutConfig(fireLeaveTimeout, fireCheckTimeout time.Duration) error {
config := CycleTimeoutConfig{
FireLeaveTimeout: fireLeaveTimeout,
FireCheckTimeout: fireCheckTimeout,
}
data, err := json.Marshal(config)
if err != nil {
return err
}
// 确保目录存在
if err := os.MkdirAll("/data/cache", 0755); err != nil {
return err
}
return os.WriteFile(CycleTimeoutFile, data, 0644)
}
// GetForbidTimeConfig 获取禁止时间配置
func GetForbidTimeConfig() string {
// 读取缓存文件
data, err := os.ReadFile(ForbidTimeFile)
if err != nil {
// 缓存中没有值时,默认返回0
return "0"
}
var config struct {
Minutr string `json:"minutr"`
}
if err := json.Unmarshal(data, &config); err != nil {
// 解析失败时,默认返回0
return "0"
}
return config.Minutr
}
// ShouldReportBothEvents 判断是否应该同时上报两种事件
func ShouldReportBothEvents() bool {
// 注意:此逻辑已被修改,现在只在特定情况下使用
// 正常情况下,应根据当前事件周期上报对应类型的事件
return false
}
// jsonString 将任意类型转换为JSON字符串
func jsonString(v interface{}) string {
b, err := json.Marshal(v)
if err != nil {
if logger := logger.GlobalLoggerManager.GetLogger("reporter"); logger != nil {
logger.Printf("JSON编码失败: %v", err)
}
return ""
}
return string(b)
}
// UploadVideoAndReport 上传视频并上报事件
func (r *Reporter) UploadVideoAndReport(deviceData connect.DeviceData, alarmTime int64, mergedFile string) error {
// 异步执行视频上传和上报,避免阻塞主线程
go func() {
logger := r.getLogger()
logger.Printf("开始异步处理视频上传和事件上报 (DeviceUUID=%s, 文件=%s)", deviceData.DeviceUUID, mergedFile)
defer func() {
if err := recover(); err != nil {
logger.Printf("上传视频和上报事件发生 panic: %v", err)
// 确保临时文件被清理
os.Remove(mergedFile)
}
}()
// 检查事件周期是否超时
logger.Printf("检查事件周期状态 (DeviceUUID=%s)", deviceData.DeviceUUID)
if IsCycleTimeout() {
// 超时后切换回默认周期
logger.Printf("事件周期超时,切换回默认周期 (DeviceUUID=%s)", deviceData.DeviceUUID)
if err := SwitchEventCycle(EventCycleFireLeave); err != nil {
logger.Printf("事件周期超时切换失败: %v", err)
}
}
// 获取当前事件周期
currentCycle := GetCurrentEventCycle()
logger.Printf("当前事件周期: %s (DeviceUUID=%s)", currentCycle, deviceData.DeviceUUID)
// 检查是否应该同时上报两种事件
if ShouldReportBothEvents() {
logger.Printf("Forbid time config is 0, will report both event types (DeviceUUID=%s)", deviceData.DeviceUUID)
// 复制文件路径,避免在goroutine中使用同一个变量
fileToClean := mergedFile
// 上报fire_leave事件
logger.Printf("开始上报fire_leave事件 (DeviceUUID=%s)", deviceData.DeviceUUID)
r.reportEventWithLevel(deviceData, alarmTime, mergedFile, ReportLevelFireLeave)
// 上报fire_check事件
logger.Printf("开始上报fire_check事件 (DeviceUUID=%s)", deviceData.DeviceUUID)
r.reportEventWithLevel(deviceData, alarmTime, mergedFile, ReportLevelFireCheck)
// 延迟清理临时文件,确保上传完成
go func() {
// 等待一段时间,确保上传操作有足够时间完成
time.Sleep(5 * time.Second)
logger.Printf("清理临时文件: %s (DeviceUUID=%s)", fileToClean, deviceData.DeviceUUID)
os.Remove(fileToClean)
}()
logger.Printf("双事件上报完成 (DeviceUUID=%s)", deviceData.DeviceUUID)
return
}
// 根据当前事件周期确定上报级别
var reportLevel ReportLevel
switch currentCycle {
case EventCycleFireLeave:
reportLevel = ReportLevelFireLeave
case EventCycleFireCheck:
reportLevel = ReportLevelFireCheck
default:
reportLevel = ReportLevelFireLeave
}
logger.Printf("确定上报级别: %s (DeviceUUID=%s)", reportLevel, deviceData.DeviceUUID)
// 生成Nginx上传参数
logger.Printf("生成Nginx上传参数 (DeviceUUID=%s)", deviceData.DeviceUUID)
nginxParams := r.GenerateNginxParams(deviceData, alarmTime, reportLevel)
// 上传视频到Nginx
logger.Printf("开始上传视频到Nginx (DeviceUUID=%s, 文件=%s, URL=%s)", deviceData.DeviceUUID, mergedFile, deviceData.UploadURL)
videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL)
videoURLStr := ""
if err != nil {
logger.Printf("Nginx上传失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
videoURLStr = videoURL
logger.Printf("Nginx上传成功: %s (DeviceUUID=%s)", videoURL, deviceData.DeviceUUID)
}
// 检查WebSocket客户端
logger.Printf("检查WebSocket客户端连接状态 (DeviceUUID=%s)", deviceData.DeviceUUID)
if r.wsClient == nil {
logger.Printf("WebSocket客户端未初始化,尝试重新获取 (DeviceUUID=%s)", deviceData.DeviceUUID)
// 尝试重新获取WebSocket客户端
r.wsClient = connect.GetWSClient()
if r.wsClient == nil {
logger.Printf("WebSocket客户端仍然为nil,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID)
os.Remove(mergedFile)
return
}
}
if !r.wsClient.IsConnected() {
logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID)
os.Remove(mergedFile)
return
}
logger.Printf("WebSocket客户端连接状态正常 (DeviceUUID=%s)", deviceData.DeviceUUID)
// 上报事件
logger.Printf("开始上报事件 (DeviceUUID=%s, Level=%s, VideoURL=%s)", deviceData.DeviceUUID, reportLevel, videoURLStr)
if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(reportLevel)); err != nil {
logger.Printf("事件上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
logger.Printf("事件上报成功 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, reportLevel)
}
// 上报数据快照
logger.Printf("开始上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID)
if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil {
logger.Printf("数据快照上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
logger.Printf("数据快照上报成功 (DeviceUUID=%s)", deviceData.DeviceUUID)
}
// 清理临时文件
logger.Printf("清理临时文件: %s (DeviceUUID=%s)", mergedFile, deviceData.DeviceUUID)
os.Remove(mergedFile)
logger.Printf("视频上传和事件上报完成 (DeviceUUID=%s)", deviceData.DeviceUUID)
}()
return nil
}
// UploadVideoAndReportFireLeave 上传视频并上报温度报警事件(只上报fire_leave类型)
func (r *Reporter) UploadVideoAndReportFireLeave(deviceData connect.DeviceData, alarmTime int64, mergedFile string) error {
// 异步执行视频上传和上报,避免阻塞主线程
go func() {
logger := r.getLogger()
logger.Printf("开始异步处理温度报警视频上传和事件上报 (DeviceUUID=%s, 文件=%s)", deviceData.DeviceUUID, mergedFile)
defer func() {
if err := recover(); err != nil {
logger.Printf("上传温度报警视频和上报事件发生 panic: %v", err)
// 确保临时文件被清理
os.Remove(mergedFile)
}
}()
// 检查事件周期是否超时
logger.Printf("检查事件周期状态 (DeviceUUID=%s)", deviceData.DeviceUUID)
if IsCycleTimeout() {
// 超时后切换回默认周期
logger.Printf("事件周期超时,切换回默认周期 (DeviceUUID=%s)", deviceData.DeviceUUID)
if err := SwitchEventCycle(EventCycleFireLeave); err != nil {
logger.Printf("事件周期超时切换失败: %v", err)
}
}
// 生成Nginx上传参数
logger.Printf("生成Nginx上传参数 (DeviceUUID=%s)", deviceData.DeviceUUID)
nginxParams := r.GenerateNginxParams(deviceData, alarmTime, ReportLevelFireLeave)
// 上传视频到Nginx
logger.Printf("开始上传视频到Nginx (DeviceUUID=%s, 文件=%s, URL=%s)", deviceData.DeviceUUID, mergedFile, deviceData.UploadURL)
videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL)
videoURLStr := ""
if err != nil {
logger.Printf("Nginx上传失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
videoURLStr = videoURL
logger.Printf("Nginx上传成功: %s (DeviceUUID=%s)", videoURL, deviceData.DeviceUUID)
}
// 检查WebSocket客户端
logger.Printf("检查WebSocket客户端连接状态 (DeviceUUID=%s)", deviceData.DeviceUUID)
if r.wsClient == nil {
logger.Printf("WebSocket客户端未初始化,尝试重新获取 (DeviceUUID=%s)", deviceData.DeviceUUID)
// 尝试重新获取WebSocket客户端
r.wsClient = connect.GetWSClient()
if r.wsClient == nil {
logger.Printf("WebSocket客户端仍然为nil,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID)
os.Remove(mergedFile)
return
}
}
if !r.wsClient.IsConnected() {
logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID)
os.Remove(mergedFile)
return
}
logger.Printf("WebSocket客户端连接状态正常 (DeviceUUID=%s)", deviceData.DeviceUUID)
// 上报事件(只上报fire_leave类型)
logger.Printf("开始上报温度报警事件 (DeviceUUID=%s, Level=%s, VideoURL=%s)", deviceData.DeviceUUID, ReportLevelFireLeave, videoURLStr)
if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(ReportLevelFireLeave)); err != nil {
logger.Printf("温度报警事件上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
logger.Printf("温度报警事件上报成功 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, ReportLevelFireLeave)
}
// 上报数据快照
logger.Printf("开始上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID)
if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil {
logger.Printf("数据快照上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
logger.Printf("数据快照上报成功 (DeviceUUID=%s)", deviceData.DeviceUUID)
}
// 清理临时文件
logger.Printf("清理临时文件: %s (DeviceUUID=%s)", mergedFile, deviceData.DeviceUUID)
os.Remove(mergedFile)
logger.Printf("温度报警视频上传和事件上报完成 (DeviceUUID=%s)", deviceData.DeviceUUID)
}()
return nil
}
// UploadVideoAndReportFireCheck 上传视频并上报火焰检测报警事件(只上报fire_check类型)
func (r *Reporter) UploadVideoAndReportFireCheck(deviceData connect.DeviceData, alarmTime int64, mergedFile string) error {
// 异步执行视频上传和上报,避免阻塞主线程
go func() {
logger := r.getLogger()
logger.Printf("开始异步处理火焰检测报警视频上传和事件上报 (DeviceUUID=%s, 文件=%s)", deviceData.DeviceUUID, mergedFile)
defer func() {
if err := recover(); err != nil {
logger.Printf("上传火焰检测报警视频和上报事件发生 panic: %v", err)
// 确保临时文件被清理
os.Remove(mergedFile)
}
}()
// 检查事件周期是否超时
logger.Printf("检查事件周期状态 (DeviceUUID=%s)", deviceData.DeviceUUID)
if IsCycleTimeout() {
// 超时后切换到fire_check周期
logger.Printf("事件周期超时,切换到fire_check周期 (DeviceUUID=%s)", deviceData.DeviceUUID)
if err := SwitchEventCycle(EventCycleFireCheck); err != nil {
logger.Printf("事件周期超时切换失败: %v", err)
}
}
// 生成Nginx上传参数
logger.Printf("生成Nginx上传参数 (DeviceUUID=%s)", deviceData.DeviceUUID)
nginxParams := r.GenerateNginxParams(deviceData, alarmTime, ReportLevelFireCheck)
// 上传视频到Nginx
logger.Printf("开始上传视频到Nginx (DeviceUUID=%s, 文件=%s, URL=%s)", deviceData.DeviceUUID, mergedFile, deviceData.UploadURL)
videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL)
videoURLStr := ""
if err != nil {
logger.Printf("Nginx上传失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
videoURLStr = videoURL
logger.Printf("Nginx上传成功: %s (DeviceUUID=%s)", videoURL, deviceData.DeviceUUID)
}
// 检查WebSocket客户端
logger.Printf("检查WebSocket客户端连接状态 (DeviceUUID=%s)", deviceData.DeviceUUID)
if r.wsClient == nil {
logger.Printf("WebSocket客户端未初始化,尝试重新获取 (DeviceUUID=%s)", deviceData.DeviceUUID)
// 尝试重新获取WebSocket客户端
r.wsClient = connect.GetWSClient()
if r.wsClient == nil {
logger.Printf("WebSocket客户端仍然为nil,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID)
os.Remove(mergedFile)
return
}
}
if !r.wsClient.IsConnected() {
logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID)
os.Remove(mergedFile)
return
}
logger.Printf("WebSocket客户端连接状态正常 (DeviceUUID=%s)", deviceData.DeviceUUID)
// 上报事件(只上报fire_check类型)
logger.Printf("开始上报火焰检测报警事件 (DeviceUUID=%s, Level=%s, VideoURL=%s)", deviceData.DeviceUUID, ReportLevelFireCheck, videoURLStr)
if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(ReportLevelFireCheck)); err != nil {
logger.Printf("火焰检测报警事件上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
logger.Printf("火焰检测报警事件上报成功 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, ReportLevelFireCheck)
}
// 上报数据快照
logger.Printf("开始上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID)
if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil {
logger.Printf("数据快照上报失败: %v (DeviceUUID=%s)", err, deviceData.DeviceUUID)
} else {
logger.Printf("数据快照上报成功 (DeviceUUID=%s)", deviceData.DeviceUUID)
}
// 清理临时文件
logger.Printf("清理临时文件: %s (DeviceUUID=%s)", mergedFile, deviceData.DeviceUUID)
os.Remove(mergedFile)
logger.Printf("火焰检测报警视频上传和事件上报完成 (DeviceUUID=%s)", deviceData.DeviceUUID)
}()
return nil
}
// reportEventWithLevel 按指定级别上报事件
func (r *Reporter) reportEventWithLevel(deviceData connect.DeviceData, alarmTime int64, mergedFile string, reportLevel ReportLevel) error {
// 异步执行视频上传和上报,避免阻塞主线程
go func() {
logger := r.getLogger()
defer func() {
if err := recover(); err != nil {
logger.Printf("按指定级别上报事件发生 panic: %v", err)
}
}()
// 生成Nginx上传参数
nginxParams := r.GenerateNginxParams(deviceData, alarmTime, reportLevel)
// 上传视频到Nginx
videoURL, err := video_server.UploadToNginx(mergedFile, nginxParams, deviceData.UploadURL)
videoURLStr := ""
if err != nil {
logger.Printf("Nginx上传失败: %v", err)
} else {
videoURLStr = videoURL
logger.Printf("Nginx上传成功: %s", videoURL)
}
// 检查WebSocket客户端
if r.wsClient == nil || !r.wsClient.IsConnected() {
logger.Printf("WS未就绪,延迟上报 (DeviceUUID=%s)", deviceData.DeviceUUID)
return
}
// 上报事件
if err := r.ReportEvent(deviceData, alarmTime, videoURLStr, string(reportLevel)); err != nil {
logger.Printf("事件上报失败: %v", err)
}
// 上报数据快照
if err := r.ReportDataSnapshot(deviceData, alarmTime); err != nil {
logger.Printf("数据快照上报失败: %v", err)
}
logger.Printf("按指定级别上报事件完成 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, string(reportLevel))
}()
return nil
}
// ReportEvent 上报事件
func (r *Reporter) ReportEvent(deviceData connect.DeviceData, alarmTime int64, videoURL string, level string) error {
logger := r.getLogger()
// 检查WebSocket客户端
if r.wsClient == nil {
logger.Printf("WebSocket客户端未初始化,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID)
return fmt.Errorf("websocket client not initialized")
}
if !r.wsClient.IsConnected() {
logger.Printf("WebSocket客户端未连接,无法上报事件 (DeviceUUID=%s)", deviceData.DeviceUUID)
return fmt.Errorf("websocket client not connected")
}
// 生成事件消息
eventMsg := r.GenerateEventMessage(deviceData, alarmTime, videoURL, level)
// 发送事件消息
r.wsClient.SendAsync(eventMsg)
logger.Printf("报警事件已上报 (DeviceUUID=%s, Level=%s)", deviceData.DeviceUUID, level)
return nil
}
// ReportDataSnapshot 上报数据快照
func (r *Reporter) ReportDataSnapshot(deviceData connect.DeviceData, alarmTime int64) error {
logger := r.getLogger()
// 检查WebSocket客户端
if r.wsClient == nil {
logger.Printf("WebSocket客户端未初始化,无法上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID)
return fmt.Errorf("websocket client not initialized")
}
if !r.wsClient.IsConnected() {
logger.Printf("WebSocket客户端未连接,无法上报数据快照 (DeviceUUID=%s)", deviceData.DeviceUUID)
return fmt.Errorf("websocket client not connected")
}
// 生成数据快照消息
dataMsg := r.GenerateDataMessage(deviceData, alarmTime)
// 发送数据快照消息
r.wsClient.SendAsync(dataMsg)
logger.Printf("数据快照已上报 (DeviceUUID=%s)", deviceData.DeviceUUID)
return nil
}
// GenerateEventMessage 生成事件消息
func (r *Reporter) GenerateEventMessage(deviceData connect.DeviceData, alarmTime int64, videoURL string, level string) map[string]interface{} {
// 获取HostUUID
hostUUID := r.getHostUUID(deviceData.DeviceUUID)
// 生成描述
description := "无人工作区且温度超阈值报警"
if level == string(ReportLevelFireCheck) {
description = "检测到异常火苗"
}
// 构建事件负载
eventPayload := EventPayload{
Type: string(ReportTypeEvent),
Args: map[string]interface{}{
"level": level,
"description": description,
"device_uuid": deviceData.DeviceUUID,
"host_uuid": hostUUID,
"video_url": videoURL,
"alarm_time": alarmTime,
"address": deviceData.Address,
},
}
// 构建上报消息
eventMsg := map[string]interface{}{
"method": "metric_data",
"params": map[string]interface{}{
"route_key": fmt.Sprintf("/dhlr/device/%s/event", deviceData.DeviceUUID),
"metric": jsonString(eventPayload),
},
}
return eventMsg
}
// GenerateDataMessage 生成数据快照消息
func (r *Reporter) GenerateDataMessage(deviceData connect.DeviceData, alarmTime int64) map[string]interface{} {
// 构建数据快照负载
dataPayload := DataPayload{
Type: string(ReportTypeData),
Args: map[string]interface{}{
"person_count": deviceData.PersonCount,
"temperature": deviceData.Temperature,
"camera_rtsp": deviceData.CameraRTSP,
"task_id": deviceData.TaskID,
"camera_ip": deviceData.CameraIP,
"confidence": deviceData.Confidence,
"alarm_time": alarmTime,
"address": deviceData.Address,
},
}
// 构建上报消息
dataMsg := map[string]interface{}{
"method": "metric_data",
"params": map[string]interface{}{
"route_key": fmt.Sprintf("/dhlr/device/%s/data", deviceData.DeviceUUID),
"metric": jsonString(dataPayload),
},
}
return dataMsg
}
// GenerateNginxParams 生成Nginx上传参数
func (r *Reporter) GenerateNginxParams(deviceData connect.DeviceData, alarmTime int64, level ReportLevel) map[string]interface{} {
// 获取HostUUID
hostUUID := r.getHostUUID(deviceData.DeviceUUID)
// 生成描述
description := "无人工作区且温度超阈值报警"
if level == ReportLevelFireCheck {
description = "检测到异常火苗"
}
// 构建参数
params := map[string]interface{}{
"alarm_time": alarmTime,
"description": description,
"device_UUID": deviceData.DeviceUUID,
"host_uuid": hostUUID,
"person_count": deviceData.PersonCount,
"level": string(level),
}
// 火焰检测不需要温度数据
if level != ReportLevelFireCheck {
params["temperature"] = deviceData.Temperature
}
return params
}
// TriggerAlarmWithDelay 延迟触发报警并上报(温度报警)
func (r *Reporter) TriggerAlarmWithDelay(deviceData connect.DeviceData, alarmTime int64, cooldown int) {
startTime := time.Now()
logger := r.getLogger()
logger.Printf("[%.3f] 开始处理温度报警事件 (DeviceUUID=%s, alarmTime=%d, cooldown=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime, cooldown)
// 捕获所有异常,确保函数不会因为panic而崩溃
defer func() {
if err := recover(); err != nil {
logger.Printf("[%.3f] 处理温度报警事件发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID)
// 确保锁被释放
key := fmt.Sprintf("%s_%d", deviceData.DeviceUUID, alarmTime)
mergingAlarms.Delete(key)
}
}()
// 防止重复处理
key := fmt.Sprintf("%s_%d", deviceData.DeviceUUID, alarmTime)
logger.Printf("[%.3f] 检查温度报警事件是否已在处理中 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key)
if _, loaded := mergingAlarms.LoadOrStore(key, true); loaded {
logger.Printf("[%.3f] 温度报警事件已在处理中,跳过重复处理 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key)
return
}
defer mergingAlarms.Delete(key)
logger.Printf("[%.3f] 成功获取处理锁 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key)
// 合并视频
logger.Printf("[%.3f] 开始合并视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime)
mergeStartTime := time.Now()
mergedFile, err := video_server.MergeVideo(deviceData.DeviceUUID, alarmTime)
if err != nil {
logger.Printf("[%.3f] 视频合并失败 (DeviceUUID=%s, 耗时:%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds(), err)
return
}
logger.Printf("[%.3f] 视频合并成功,生成文件: %s (DeviceUUID=%s, 耗时:%.3fs)", time.Since(startTime).Seconds(), mergedFile, deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds())
// 上传视频并上报(只上报温度报警,即fire_leave类型)
logger.Printf("[%.3f] 开始上传视频并上报温度报警事件 (DeviceUUID=%s, 文件=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, mergedFile)
uploadStartTime := time.Now()
if err := r.UploadVideoAndReportFireLeave(deviceData, alarmTime, mergedFile); err != nil {
logger.Printf("[%.3f] 温度报警上报失败 (DeviceUUID=%s, 耗时:%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds(), err)
// 清理临时文件
if err := os.Remove(mergedFile); err != nil {
logger.Printf("[%.3f] 清理临时文件失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID)
}
return
}
logger.Printf("[%.3f] 温度报警上报成功 (DeviceUUID=%s, 耗时:%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds())
logger.Printf("[%.3f] 温度报警事件处理完成 (DeviceUUID=%s, 总耗时:%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(startTime).Seconds())
}
// TriggerFireCheckAlarm 触发火焰检测报警并上报
func (r *Reporter) TriggerFireCheckAlarm(deviceData connect.DeviceData, alarmTime int64) {
startTime := time.Now()
logger := r.getLogger()
logger.Printf("[%.3f] 开始处理火焰检测报警事件 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime)
// 捕获所有异常,确保函数不会因为panic而崩溃
defer func() {
if err := recover(); err != nil {
logger.Printf("[%.3f] 处理火焰检测报警事件发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID)
// 确保锁被释放
key := fmt.Sprintf("%s_fire_%d", deviceData.DeviceUUID, alarmTime)
mergingAlarms.Delete(key)
}
}()
// 防止重复处理
key := fmt.Sprintf("%s_fire_%d", deviceData.DeviceUUID, alarmTime)
logger.Printf("[%.3f] 检查火焰检测报警事件是否已在处理中 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key)
if _, loaded := mergingAlarms.LoadOrStore(key, true); loaded {
logger.Printf("[%.3f] 火焰检测报警事件已在处理中,跳过重复处理 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key)
return
}
defer mergingAlarms.Delete(key)
logger.Printf("[%.3f] 成功获取处理锁 (DeviceUUID=%s, key=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, key)
// 合并视频(使用fire_check目录的视频)
logger.Printf("[%.3f] 开始合并火焰检测视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, alarmTime)
mergeStartTime := time.Now()
// 这里可以修改MergeVideo函数,使其支持指定目录,或者创建一个新的函数用于合并fire_check视频
mergedFile, err := video_server.MergeVideo(deviceData.DeviceUUID+"_fire_check", alarmTime)
if err != nil {
logger.Printf("[%.3f] 火焰检测视频合并失败 (DeviceUUID=%s, 耗时:%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds(), err)
return
}
logger.Printf("[%.3f] 火焰检测视频合并成功,生成文件: %s (DeviceUUID=%s, 耗时:%.3fs)", time.Since(startTime).Seconds(), mergedFile, deviceData.DeviceUUID, time.Since(mergeStartTime).Seconds())
// 上传视频并上报(只上报火焰检测,即fire_check类型)
logger.Printf("[%.3f] 开始上传视频并上报火焰检测报警事件 (DeviceUUID=%s, 文件=%s)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, mergedFile)
uploadStartTime := time.Now()
if err := r.UploadVideoAndReportFireCheck(deviceData, alarmTime, mergedFile); err != nil {
logger.Printf("[%.3f] 火焰检测报警上报失败 (DeviceUUID=%s, 耗时:%.3fs): %v", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds(), err)
// 清理临时文件
if err := os.Remove(mergedFile); err != nil {
logger.Printf("[%.3f] 清理临时文件失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceData.DeviceUUID)
}
return
}
logger.Printf("[%.3f] 火焰检测报警上报成功 (DeviceUUID=%s, 耗时:%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(uploadStartTime).Seconds())
logger.Printf("[%.3f] 火焰检测报警事件处理完成 (DeviceUUID=%s, 总耗时:%.3fs)", time.Since(startTime).Seconds(), deviceData.DeviceUUID, time.Since(startTime).Seconds())
}
// getHostUUID 获取设备的HostUUID
func (r *Reporter) getHostUUID(deviceUUID string) string {
configs, err := connect.LoadServiceConfig()
if err != nil {
r.logger.Printf("加载服务配置失败: %v", err)
return ""
}
for _, cfg := range configs {
if cfg.DeviceUUID == deviceUUID {
return cfg.HostUUID
}
}
return ""
}
// SetWSClient 设置WebSocket客户端
func (r *Reporter) SetWSClient(client *connect.WSClient) {
r.wsClient = client
}
// SetUploadURL 设置上传URL
func (r *Reporter) SetUploadURL(url string) {
r.uploadURL = url
}
// IsWSReady 检查WebSocket是否就绪
func (r *Reporter) IsWSReady() bool {
return r.wsClient != nil && r.wsClient.IsConnected()
}
+57
View File
@@ -0,0 +1,57 @@
package reporter
// ReportLevel 上报级别类型
type ReportLevel string
// 上报级别常量
const (
ReportLevelAlarm ReportLevel = "alarm"
ReportLevelFireLeave ReportLevel = "fire_leave"
ReportLevelFireCheck ReportLevel = "fire_check"
)
// ReportType 上报类型
type ReportType string
// 上报类型常量
const (
ReportTypeEvent ReportType = "event"
ReportTypeData ReportType = "data"
)
// EventPayload 事件上报负载
type EventPayload struct {
Type string `json:"type"`
Args map[string]interface{} `json:"args"`
}
// DataPayload 数据快照上报负载
type DataPayload struct {
Type string `json:"type"`
Args map[string]interface{} `json:"args"`
}
// ReportMessage 上报消息结构
type ReportMessage struct {
Method string `json:"method"`
Params map[string]interface{} `json:"params"`
}
// NginxUploadParams Nginx上传参数
type NginxUploadParams struct {
AlarmTime int64 `json:"alarm_time"`
Description string `json:"description"`
DeviceUUID string `json:"device_UUID"`
HostUUID string `json:"host_uuid"`
PersonCount int `json:"person_count"`
Temperature float64 `json:"temperature,omitempty"`
Level string `json:"level,omitempty"`
}
// ReportOptions 上报选项
type ReportOptions struct {
Level ReportLevel
Description string
VideoURL string
AlarmTime int64
}
+10
View File
@@ -0,0 +1,10 @@
#!/bin/sh
set -e
echo "Starting efka_flow_new inference..."
cd /usr/local/models
export LD_LIBRARY_PATH=/usr/lib:$LD_LIBRARY_PATH
# 启动主程序,在前台运行
./fireleave_tool
+150 -20
View File
@@ -1,6 +1,7 @@
package video_server
import (
"context"
"fmt"
"io"
"os"
@@ -16,14 +17,33 @@ import (
// MergeVideo 合并视频文件 - 完全适配 mp4_merge --out 模式
func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
videoDir := fmt.Sprintf("/usr/data/camera/%s", deviceUUID)
if _, err := os.Stat(videoDir); os.IsNotExist(err) {
return "", fmt.Errorf("video directory not exist: %s", videoDir)
startTime := time.Now()
logger.Logger.Printf("[%.3f] 开始合并视频 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
// 捕获所有异常,确保函数不会因为panic而崩溃
defer func() {
if err := recover(); err != nil {
logger.Logger.Printf("[%.3f] 合并视频发生panic: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
}
}()
// 检查视频目录
videoDir := fmt.Sprintf("/usr/data/camera/%s", deviceUUID)
logger.Logger.Printf("[%.3f] 检查视频目录: %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)
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)
return "", fmt.Errorf("check video directory failed: %v", err)
}
logger.Logger.Printf("[%.3f] 视频目录检查成功: %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)
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)
beforeTime := alarmTimeAligned - 10
afterTime := alarmTimeAligned + 10
@@ -37,19 +57,28 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
afterStr: filepath.Join(videoDir, afterStr+".mp4"),
}
// 查找视频文件
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 {
videoFiles = append(videoFiles, path)
logger.Logger.Printf("Found video: %s", name+".mp4")
logger.Logger.Printf("[%.3f] Found video: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), name+".mp4", deviceUUID)
} else {
logger.Logger.Printf("Missing video: %s", name+".mp4")
logger.Logger.Printf("[%.3f] Missing video: %s (DeviceUUID=%s, error: %v)", time.Since(startTime).Seconds(), name+".mp4", deviceUUID, err)
}
}
// === 2. 补充替代文件 ===
logger.Logger.Printf("[%.3f] 当前找到 %d 个视频文件,开始补充替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
if len(videoFiles) < 3 {
allFiles, _ := os.ReadDir(videoDir)
logger.Logger.Printf("[%.3f] 视频文件不足3个,开始查找替代文件 (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)
} else {
logger.Logger.Printf("[%.3f] 读取目录所有文件,共 %d 个文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(allFiles), deviceUUID)
var candidates []struct {
path string
ts int64
@@ -64,6 +93,7 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
}
}
}
logger.Logger.Printf("[%.3f] 找到 %d 个候选视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(candidates), deviceUUID)
targets := []int64{beforeTime, alarmTimeAligned, afterTime}
for _, target := range targets {
if containsTimestamp(videoFiles, target) {
@@ -72,32 +102,40 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
closest := findClosest(candidates, target)
if closest != "" && !contains(videoFiles, closest) {
videoFiles = append(videoFiles, closest)
logger.Logger.Printf("Using fallback: %s", filepath.Base(closest))
logger.Logger.Printf("[%.3f] Using fallback: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), filepath.Base(closest), deviceUUID)
}
}
}
}
logger.Logger.Printf("[%.3f] 最终收集到 %d 个视频文件 (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)
return "", fmt.Errorf("insufficient video files: %d", len(videoFiles))
}
// === 3. 排序 ===
logger.Logger.Printf("[%.3f] 开始排序视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
sort.Slice(videoFiles, func(i, j int) bool {
return getTimestampFromPath(videoFiles[i]) < getTimestampFromPath(videoFiles[j])
})
logger.Logger.Printf("Merging %d videos:", len(videoFiles))
logger.Logger.Printf("[%.3f] Merging %d videos (DeviceUUID=%s):", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
for i, f := range videoFiles {
logger.Logger.Printf(" [%d] %s", i, filepath.Base(f))
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] 清理旧的输出文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
if err := os.Remove(joinedPath); err != nil && !os.IsNotExist(err) {
logger.Logger.Printf("Failed to remove old output file %s: %v", joinedPath, 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)
}
// === 5. 调用 mp4_merge,显式 --out ===
@@ -105,42 +143,61 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
cmd := exec.Command("mp4_merge", args...)
cmd.Stdout = logger.Logger.Writer()
cmd.Stderr = logger.Logger.Writer()
logger.Logger.Printf("[%.3f] 开始执行视频合并命令: %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] 视频合并命令执行成功 (耗时:%.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)
if _, err := os.Stat(joinedPath); err != nil {
logger.Logger.Printf("[%.3f] 输出文件未创建: %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)
return "", fmt.Errorf("stat output file failed: %v", err)
}
logger.Logger.Printf("Merged file created: %s (size: %d bytes)", outputFile, info.Size())
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)
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)
finalPath := filepath.Join("/data/upload", finalName)
logger.Logger.Printf("[%.3f] 最终文件路径: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), finalPath, deviceUUID)
logger.Logger.Printf("[%.3f] 确保 /data/upload 目录存在 (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)
return "", fmt.Errorf("create /data/upload failed: %v", err)
}
// 跨分区复制 + 删除
logger.Logger.Printf("[%.3f] 开始复制文件: %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)
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)
if err := os.Remove(joinedPath); err != nil {
logger.Logger.Printf("Failed to delete temp file %s: %v", joinedPath, err)
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("Video merged and moved successfully: %s", finalPath)
logger.Logger.Printf("[%.3f] Video merged and moved successfully: %s (DeviceUUID=%s, 总耗时:%.3fs)", time.Since(startTime).Seconds(), finalPath, deviceUUID, time.Since(startTime).Seconds())
return finalPath, nil
}
@@ -181,24 +238,89 @@ func abs(x int64) int64 {
return x
}
// copyFile 支持跨分区复制
// 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
}
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 {
return err
resultChan <- copyResult{0, err}
return
}
defer in.Close()
// 创建目标文件
logger.Logger.Printf("[%.3f] 创建目标文件: %s", time.Since(startTime).Seconds(), dst)
out, err := os.Create(dst)
if err != nil {
return err
resultChan <- copyResult{0, err}
return
}
defer out.Close()
if _, err = io.Copy(out, in); err != nil {
return err
// 获取源文件大小
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())
}
return out.Sync()
// 执行复制
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)
}
logger.Logger.Printf("[%.3f] 文件复制操作完成", time.Since(startTime).Seconds())
return nil
}
// alignTo10Seconds 将时间对齐到最近的10秒间隔
@@ -294,7 +416,15 @@ func CleanupOldVideos(deviceUUID string) {
// CleanupOldRawVideos 清理推理程序生成的原始10s视频,保留最近60条
func CleanupOldRawVideos(deviceUUID string) {
rawDir := filepath.Join("/usr/data/camera", deviceUUID)
// 清理主设备目录
cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID))
// 清理火焰检测目录
cleanupRawVideosDirectory(filepath.Join("/usr/data/camera", deviceUUID+"_fire_check"))
}
// cleanupRawVideosDirectory 清理单个目录中的原始视频文件
func cleanupRawVideosDirectory(rawDir string) {
if _, err := os.Stat(rawDir); os.IsNotExist(err) {
return
}
+50 -1
View File
@@ -16,36 +16,62 @@ import (
// UploadToNginx 上传视频到 Nginx 服务器,返回 URL
func UploadToNginx(filePath string, eventParams map[string]any, uploadURL string) (string, error) {
logger.Logger.Printf("开始上传视频到 Nginx: %s → %s", filePath, uploadURL)
// 打开文件
logger.Logger.Printf("打开文件: %s", filePath)
startTime := time.Now()
file, err := os.Open(filePath)
if err != nil {
logger.Logger.Printf("打开文件失败: %v, 耗时: %v", err, time.Since(startTime))
return "", fmt.Errorf("open file failed: %w", err)
}
defer file.Close()
logger.Logger.Printf("文件打开成功, 耗时: %v", time.Since(startTime))
// 创建多部分表单
logger.Logger.Printf("创建多部分表单")
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
// 创建表单文件字段
logger.Logger.Printf("创建表单文件字段: %s", filepath.Base(filePath))
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
if err != nil {
logger.Logger.Printf("创建表单文件字段失败: %v", err)
return "", fmt.Errorf("create form file failed: %w", err)
}
// 复制文件内容到表单
logger.Logger.Printf("开始复制文件内容到表单")
copyStartTime := time.Now()
if _, err = io.Copy(part, file); err != nil {
logger.Logger.Printf("复制文件内容失败: %v, 耗时: %v", err, time.Since(copyStartTime))
return "", fmt.Errorf("copy file failed: %w", err)
}
logger.Logger.Printf("文件内容复制成功, 耗时: %v", time.Since(copyStartTime))
// 序列化事件参数
logger.Logger.Printf("序列化事件参数")
eventJSON, err := json.Marshal(eventParams)
if err != nil {
logger.Logger.Printf("序列化事件参数失败: %v", err)
return "", fmt.Errorf("marshal event failed: %w", err)
}
logger.Logger.Printf("[Upload Event parameters]eventParams: %v", eventParams)
logger.Logger.Printf("[Upload Event parameters]eventJSON: %s", string(eventJSON))
// 写入事件字段
logger.Logger.Printf("写入事件字段到表单")
if err := writer.WriteField("event", string(eventJSON)); err != nil {
logger.Logger.Printf("写入事件字段失败: %v", err)
return "", fmt.Errorf("write event field failed: %w", err)
}
// 关闭表单写入器
logger.Logger.Printf("关闭表单写入器")
if err := writer.Close(); err != nil {
logger.Logger.Printf("关闭表单写入器失败: %v", err)
return "", fmt.Errorf("close writer failed: %w", err)
}
@@ -53,41 +79,64 @@ func UploadToNginx(filePath string, eventParams map[string]any, uploadURL string
logger.Logger.Printf("[HTTP Request]Content-Type: %s", writer.FormDataContentType())
logger.Logger.Printf("[HTTP Request]Body 长度: %d bytes", body.Len())
// 创建HTTP请求
logger.Logger.Printf("创建HTTP请求")
req, err := http.NewRequest("POST", uploadURL, body)
if err != nil {
logger.Logger.Printf("创建HTTP请求失败: %v", err)
return "", fmt.Errorf("create request failed: %w", err)
}
req.Header.Set("Content-Type", writer.FormDataContentType())
// 发送HTTP请求
logger.Logger.Printf("发送HTTP请求到 Nginx")
httpStartTime := time.Now()
client := &http.Client{Timeout: 180 * time.Second}
resp, err := client.Do(req)
if err != nil {
logger.Logger.Printf("发送HTTP请求失败: %v, 耗时: %v", err, time.Since(httpStartTime))
return "", fmt.Errorf("upload failed: %w", err)
}
defer resp.Body.Close()
logger.Logger.Printf("HTTP请求发送成功, 耗时: %v", time.Since(httpStartTime))
// 读取响应
logger.Logger.Printf("读取HTTP响应")
respStartTime := time.Now()
respBody, _ := io.ReadAll(resp.Body)
logger.Logger.Printf("响应读取完成, 耗时: %v", time.Since(respStartTime))
logger.Logger.Printf("[nginx response]Status: %d", resp.StatusCode)
logger.Logger.Printf("[nginx response]Body: %s", string(respBody))
if resp.StatusCode != http.StatusOK {
logger.Logger.Printf("Nginx返回错误状态码: %d", resp.StatusCode)
return "", fmt.Errorf("nginx error %d: %s", resp.StatusCode, string(respBody))
}
// 解析响应
logger.Logger.Printf("解析Nginx响应")
var result struct {
Code int `json:"code"`
Result json.RawMessage `json:"result"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
logger.Logger.Printf("解析响应失败: %v", err)
return "", fmt.Errorf("parse response failed: %w", err)
}
if result.Code != 0 {
logger.Logger.Printf("Nginx返回错误代码: %d", result.Code)
return "", fmt.Errorf("nginx error: %s", string(respBody))
}
// 提取视频URL
logger.Logger.Printf("提取视频URL")
var videoURL string
json.Unmarshal(result.Result, &videoURL)
if err := json.Unmarshal(result.Result, &videoURL); err != nil {
logger.Logger.Printf("提取视频URL失败: %v", err)
return "", fmt.Errorf("extract video URL failed: %w", err)
}
logger.Logger.Printf("[ Upload Successful ] Local file: %s → Remote URL: %s", filePath, videoURL)
logger.Logger.Printf("视频上传完成, 总耗时: %v", time.Since(startTime))
return videoURL, nil
}
+60
View File
@@ -0,0 +1,60 @@
flowchart TD
A[前端配置设备/检测区/参数] --> B[后端下发或本地生成 service.conf]
B --> C[微服务启动]
C --> D[读取 /usr/local/etc/service.conf]
D --> E[按设备初始化]
E --> F1[启动 fire_leave 推理程序<br/>./yolov5 -m model]
E --> F2[启动 fire_check 推理程序<br/>./yolov5 -m fire_check -R]
E --> F3[启动测温程序<br/>run_thermometry.sh]
F1 --> G1[HTTP 回传人员检测结果]
F2 --> G2[HTTP 回传火焰检测结果]
F3 --> G3[TCP 回传温度值]
G1 --> H1[更新当前人数 PersonCount]
G3 --> H2[更新当前温度 Temperature]
H1 --> I1{PersonCount == 0?}
H2 --> I2{温度 > 阈值?}
I1 -->|是| J1[无人条件成立]
I2 -->|是| J2[超温条件成立]
J1 --> K{无人 + 超温<br/>是否同时成立?}
J2 --> K
K -->|否| K0[重置持续计时]
K -->|是| L[开始累计持续时间]
L --> M{持续时间 >= detection_time?}
M -->|否| L
M -->|是| N{是否在 alarm_time 冷却期?}
N -->|是| N0[不上报,继续监测]
N -->|否| O[触发 fire_leave 动火离人报警]
G2 --> P{检测到火焰<br/>ClassIdx=0 且 Number>0?}
P -->|否| P0[重置火焰计时]
P -->|是| Q[累计火焰持续时间]
Q --> R{持续时间 >= fire_check detection_time?}
R -->|否| Q
R -->|是| S[触发 fire_check 火焰报警]
O --> T[查找报警前后视频片段]
S --> T
T --> U[拼接约30秒报警视频]
U --> V[复制到 /data/upload]
V --> W[上传视频到后端/Nginx]
W --> X[WebSocket 上报告警事件]
X --> Y[后端转发前端展示]
Z1[GPIO按钮] --> Z2[目标业务:暂停 fire_leave 上报]
Z2 --> Z3[暂停期内人员离开报警不上报]
Z3 --> K
Z2 -.不影响.-> P
WS1[WebSocket /dhlr/alert] --> BEEP[控制蜂鸣器 GPIO109]
WS2[WebSocket /dhlr/forbid_time] --> Z2