chore: upload current project state
This commit is contained in:
Generated
+10
@@ -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
@@ -9,11 +9,191 @@ import (
|
|||||||
"strings"
|
"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 (
|
var (
|
||||||
devicesBaseDir = "/data/devices"
|
devicesBaseDir = "/data/devices"
|
||||||
deviceDataList []DeviceData
|
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) {
|
func InitializeDevicesFromServiceConfig() ([]DeviceData, error) {
|
||||||
deviceDataMutex.Lock()
|
deviceDataMutex.Lock()
|
||||||
defer deviceDataMutex.Unlock()
|
defer deviceDataMutex.Unlock()
|
||||||
@@ -94,8 +274,10 @@ func cleanDeviceData(data DeviceData) DeviceData {
|
|||||||
if cleaned.Temperature < 0 {
|
if cleaned.Temperature < 0 {
|
||||||
cleaned.Temperature = 0
|
cleaned.Temperature = 0
|
||||||
}
|
}
|
||||||
if cleaned.TemperatureThreshold <= 0 {
|
// 只有当温度阈值真正为0时才设置默认值45.0°C,避免覆盖用户配置的值
|
||||||
|
if cleaned.TemperatureThreshold == 0 {
|
||||||
cleaned.TemperatureThreshold = 45.0
|
cleaned.TemperatureThreshold = 45.0
|
||||||
|
logger.Logger.Printf("Set default temperature threshold: 45.0°C (DeviceUUID=%s)", cleaned.DeviceUUID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return cleaned
|
return cleaned
|
||||||
@@ -195,3 +377,46 @@ func UpdateDeviceData(data DeviceData) error {
|
|||||||
logger.Logger.Printf("Device data update completed: DeviceUUID=%s", cleanedData.DeviceUUID)
|
logger.Logger.Printf("Device data update completed: DeviceUUID=%s", cleanedData.DeviceUUID)
|
||||||
return nil
|
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
@@ -4,10 +4,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"FireLeave_tool/logger"
|
"FireLeave_tool/logger"
|
||||||
|
"FireLeave_tool/portmanager"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -20,7 +22,12 @@ var (
|
|||||||
// UpdateInferenceHeartbeat 每收到一帧 /video/post 就调用这个
|
// UpdateInferenceHeartbeat 每收到一帧 /video/post 就调用这个
|
||||||
func UpdateInferenceHeartbeat(deviceUUID string) {
|
func UpdateInferenceHeartbeat(deviceUUID string) {
|
||||||
inferenceLastHeartbeat.Store(deviceUUID, time.Now())
|
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 判断是否需要重启推理程序
|
// ShouldRestartInference 判断是否需要重启推理程序
|
||||||
@@ -48,41 +55,84 @@ func StopInferenceProcess(deviceUUID string) {
|
|||||||
// StartInferenceProcess 重启推理程序(保持原端口!)
|
// StartInferenceProcess 重启推理程序(保持原端口!)
|
||||||
func StartInferenceProcess(deviceData *DeviceData) {
|
func StartInferenceProcess(deviceData *DeviceData) {
|
||||||
|
|
||||||
|
// 重启fire_leave推理程序
|
||||||
StopInferenceProcess(deviceData.DeviceUUID)
|
StopInferenceProcess(deviceData.DeviceUUID)
|
||||||
|
|
||||||
port, err := GlobalPortManager.GetPort(deviceData.DeviceUUID)
|
portFireLeave, err := portmanager.GlobalPortManager.GetPort(deviceData.DeviceUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Printf("获取端口失败,无法重启推理程序 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
logger.Logger.Printf("获取fire_leave端口失败,无法重启推理程序 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
||||||
return
|
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",
|
"./yolov5",
|
||||||
"-s", deviceData.CameraRTSP,
|
"-s", deviceData.CameraRTSP,
|
||||||
"-m", "model",
|
"-m", "model",
|
||||||
"-c", deviceData.DeviceUUID,
|
"-c", deviceData.DeviceUUID,
|
||||||
"-p", fmt.Sprintf("%d", port),
|
"-p", fmt.Sprintf("%d", portFireLeave),
|
||||||
"-w", detectAreaStr,
|
"-w", fireLeaveDetectAreaStr,
|
||||||
"-r", fmt.Sprintf("%d", deviceData.Confidence),
|
"-r", fmt.Sprintf("%d", deviceData.Confidence),
|
||||||
)
|
)
|
||||||
|
|
||||||
inferenceCmd.Stdout = os.Stdout
|
inferenceCmdFireLeave.Stdout = os.Stdout
|
||||||
inferenceCmd.Stderr = os.Stderr
|
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 {
|
if err := inferenceCmdFireLeave.Start(); err != nil {
|
||||||
logger.Logger.Printf("推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
logger.Logger.Printf("fire_leave推理程序重启失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 保存进程对象
|
// 保存fire_leave进程对象
|
||||||
InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmd)
|
InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmdFireLeave)
|
||||||
logger.Logger.Printf("推理程序已成功重启,PID=%d (DeviceUUID=%s, Port=%d)",
|
logger.Logger.Printf("fire_leave推理程序已成功重启,PID=%d (DeviceUUID=%s, Port=%d)",
|
||||||
inferenceCmd.Process.Pid, deviceData.DeviceUUID, port)
|
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 检测又触发
|
// 重启后立即刷新一次心跳,防止 30s 检测又触发
|
||||||
UpdateInferenceHeartbeat(deviceData.DeviceUUID)
|
UpdateInferenceHeartbeat(deviceData.DeviceUUID)
|
||||||
|
UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-130
@@ -4,8 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -49,9 +47,7 @@ func (c *WSClient) IsConnected() bool {
|
|||||||
return c.connected
|
return c.connected
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
serviceConfigFile = "/usr/local/etc/service.conf"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DeviceData struct
|
// DeviceData struct
|
||||||
type DeviceData struct {
|
type DeviceData struct {
|
||||||
@@ -74,6 +70,11 @@ type DeviceData struct {
|
|||||||
AlarmPauseUntil int64 `json:"-"` // Pause alarm untilw
|
AlarmPauseUntil int64 `json:"-"` // Pause alarm untilw
|
||||||
HostUUID string `json:"host_uuid"`
|
HostUUID string `json:"host_uuid"`
|
||||||
Address string `json:"address"`
|
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 {
|
type InferenceParams struct {
|
||||||
@@ -85,16 +86,6 @@ type InferenceParams struct {
|
|||||||
Port int `json:"port"` // HTTP server port
|
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
|
// WSMessage struct
|
||||||
type WSMessage struct {
|
type WSMessage struct {
|
||||||
ID int64 `json:"id,omitempty"`
|
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{}) {
|
func (c *WSClient) SendAsync(message interface{}) {
|
||||||
go func() {
|
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.可能是你不清楚现在的运行环境,我先描述一下现在的业务环境,
|
||||||
@@ -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
@@ -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
@@ -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`,也没有看到覆盖关键链路的自动化测试。
|
||||||
|
|
||||||
|
### 风险
|
||||||
|
|
||||||
|
- 协议层改造、重启链路改造、报警暂停逻辑改造容易引入回归
|
||||||
|
|
||||||
|
### 准备怎么改
|
||||||
|
|
||||||
|
至少补最小化验证路径。
|
||||||
@@ -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"}}
|
||||||
Binary file not shown.
@@ -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
@@ -14,19 +14,114 @@ var logFile *os.File
|
|||||||
var logFileName string
|
var logFileName string
|
||||||
var mu sync.Mutex
|
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 {
|
func InitLogger() error {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
// 创建 /data/logs 目录
|
// 创建 /data/logs/system 目录
|
||||||
logDir := "/data/logs"
|
logDir := "/data/logs/system"
|
||||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按天生成日志文件名:20260102.log
|
// 按天生成日志文件名:system_20260102.log
|
||||||
logFileName = filepath.Join(logDir, time.Now().Format("20060102")+".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)
|
file, err := os.OpenFile(logFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -43,7 +138,74 @@ func InitLogger() error {
|
|||||||
time.Sleep(time.Until(next))
|
time.Sleep(time.Until(next))
|
||||||
|
|
||||||
mu.Lock()
|
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 {
|
if newFileName != logFileName {
|
||||||
newFile, err := os.OpenFile(newFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
newFile, err := os.OpenFile(newFileName, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -75,7 +237,15 @@ func CloseLogger() {
|
|||||||
|
|
||||||
// CleanupOldLogs 删除超过30天的日志文件
|
// CleanupOldLogs 删除超过30天的日志文件
|
||||||
func CleanupOldLogs() {
|
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)
|
files, err := os.ReadDir(logDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if Logger != nil {
|
if Logger != nil {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"FireLeave_tool/connect"
|
"FireLeave_tool/connect"
|
||||||
|
"FireLeave_tool/gpio_monitor"
|
||||||
"FireLeave_tool/httpreader"
|
"FireLeave_tool/httpreader"
|
||||||
"FireLeave_tool/logger"
|
"FireLeave_tool/logger"
|
||||||
|
"FireLeave_tool/reporter"
|
||||||
"FireLeave_tool/tcpreader"
|
"FireLeave_tool/tcpreader"
|
||||||
"FireLeave_tool/video_server"
|
"FireLeave_tool/video_server"
|
||||||
"context"
|
"context"
|
||||||
@@ -18,6 +20,7 @@ import (
|
|||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,12 +30,18 @@ type deviceAlarmState struct {
|
|||||||
InCondition bool
|
InCondition bool
|
||||||
StartTime int64
|
StartTime int64
|
||||||
AlarmPauseUntil int64
|
AlarmPauseUntil int64
|
||||||
|
// fire_check火焰检测状态
|
||||||
|
FireCheckInCondition bool
|
||||||
|
FireCheckStartTime int64
|
||||||
}
|
}
|
||||||
|
|
||||||
// 全局 map:每个设备独立状态
|
// 全局 map:每个设备独立状态
|
||||||
var deviceStates sync.Map // map[string]*deviceAlarmState
|
var deviceStates sync.Map // map[string]*deviceAlarmState
|
||||||
var mergingAlarms sync.Map
|
var mergingAlarms sync.Map
|
||||||
|
|
||||||
|
// 全局上报管理器
|
||||||
|
var GlobalReporter = reporter.NewReporter()
|
||||||
|
|
||||||
// DeviceManager 设备管理器
|
// DeviceManager 设备管理器
|
||||||
type DeviceManager struct {
|
type DeviceManager struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
@@ -193,91 +202,172 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
|
|||||||
deviceCtx, cancel := context.WithCancel(ctx)
|
deviceCtx, cancel := context.WithCancel(ctx)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
// 分配端口
|
// 分配端口 - fire_leave推理程序
|
||||||
port, err := connect.GlobalPortManager.AllocatePort(deviceData.DeviceUUID)
|
portFireLeave, err := connect.GlobalPortManager.AllocatePort(deviceData.DeviceUUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Logger.Printf(" DeviceUUID=%s Port allocation failed: %v", deviceData.DeviceUUID, err)
|
logger.Logger.Printf(" DeviceUUID=%s Port allocation failed: %v", deviceData.DeviceUUID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer connect.GlobalPortManager.ReleasePort(deviceData.DeviceUUID)
|
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)
|
// 分配端口 - fire_check推理程序
|
||||||
if err := inferenceServer.Start(); err != nil {
|
portFireCheck, err := connect.GlobalPortManager.AllocatePort(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
|
||||||
logger.Logger.Printf("Failed to start the inference data receiving server (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用带上下文的停止
|
// 使用带上下文的停止
|
||||||
defer inferenceServer.StopWithContext(deviceCtx)
|
defer inferenceServerFireLeave.StopWithContext(deviceCtx)
|
||||||
logger.Logger.Printf("Wait for the inference data receiving server to start... (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, port)
|
defer inferenceServerFireCheck.StopWithContext(deviceCtx)
|
||||||
|
|
||||||
// 等待HTTP服务器启动
|
// 等待HTTP服务器启动
|
||||||
if !waitForHTTPService(port, 10*time.Second) {
|
if !waitForHTTPService(portFireLeave, 10*time.Second) {
|
||||||
logger.Logger.Printf("The inference data receiving server has timed out during startup (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, port)
|
logger.Logger.Printf("Fire_leave inference server startup timeout (DeviceUUID=%s, Port=%d)", deviceData.DeviceUUID, portFireLeave)
|
||||||
return
|
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)
|
// 为fire_leave推理程序获取检测区域
|
||||||
logger.Logger.Printf("The processed detection area: %s", detectAreaStr)
|
fireLeaveDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireLeaveDetectArea)
|
||||||
|
logger.Logger.Printf("The processed fire_leave detection area: %s", fireLeaveDetectAreaStr)
|
||||||
|
|
||||||
// 启动推理程序
|
// 为fire_check推理程序获取检测区域
|
||||||
inferenceCmd := exec.Command(
|
fireCheckDetectAreaStr := connect.ProcessDetectAreaForInference(deviceData.FireCheckDetectArea)
|
||||||
|
logger.Logger.Printf("The processed fire_check detection area: %s", fireCheckDetectAreaStr)
|
||||||
|
|
||||||
|
// 启动fire_leave推理程序
|
||||||
|
inferenceCmdFireLeave := exec.Command(
|
||||||
"./yolov5",
|
"./yolov5",
|
||||||
"-s", deviceData.CameraRTSP,
|
"-s", deviceData.CameraRTSP,
|
||||||
"-m", "model",
|
"-m", "model",
|
||||||
"-c", deviceData.DeviceUUID,
|
"-c", deviceData.DeviceUUID,
|
||||||
"-p", fmt.Sprintf("%d", port),
|
"-p", fmt.Sprintf("%d", portFireLeave),
|
||||||
"-w", detectAreaStr,
|
"-w", fireLeaveDetectAreaStr,
|
||||||
"-r", fmt.Sprintf("%d", deviceData.Confidence),
|
"-r", fmt.Sprintf("%d", deviceData.Confidence),
|
||||||
)
|
)
|
||||||
inferenceCmd.Stdout = os.Stdout
|
inferenceCmdFireLeave.Stdout = os.Stdout
|
||||||
inferenceCmd.Stderr = os.Stderr
|
inferenceCmdFireLeave.Stderr = os.Stderr
|
||||||
//logger.Logger.Printf("推理程序输出仅显示在控制台")
|
|
||||||
|
|
||||||
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 {
|
if err := inferenceCmdFireLeave.Start(); err != nil {
|
||||||
logger.Logger.Printf("推理程序启动失败 (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
logger.Logger.Printf("Fire_leave inference program startup failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 关键:保存进程对象到全局 map
|
// 保存fire_leave进程对象到全局 map
|
||||||
connect.InferenceProcesses.Store(deviceData.DeviceUUID, inferenceCmd)
|
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(deviceData.DeviceUUID)
|
||||||
|
connect.UpdateInferenceHeartbeat(fmt.Sprintf("%s_fire_check", deviceData.DeviceUUID))
|
||||||
|
|
||||||
// 检查推理程序是否启动成功
|
// 检查推理程序是否启动成功
|
||||||
time.Sleep(2 * time.Second)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := inferenceCmd.Process.Signal(syscall.Signal(0)); err != nil {
|
if err := inferenceCmdFireLeave.Process.Signal(syscall.Signal(0)); err != nil {
|
||||||
logger.Logger.Printf("The reasoning program has exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
logger.Logger.Printf("Fire_leave inference program exited (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Logger.Printf("The reasoning program process has been started,PID: %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 {
|
if err := inferenceCmdFireCheck.Process.Signal(syscall.Signal(0)); err != nil {
|
||||||
logger.Logger.Printf("Failed to record process information: %v", err)
|
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() {
|
defer func() {
|
||||||
logger.Logger.Printf("Start to stop the reasoning program (DeviceUUID=%s)", deviceData.DeviceUUID)
|
logger.Logger.Printf("Start to stop inference programs (DeviceUUID=%s)", deviceData.DeviceUUID)
|
||||||
if inferenceCmd.Process != nil {
|
|
||||||
if err := inferenceCmd.Process.Kill(); err != nil {
|
// 停止fire_leave推理程序
|
||||||
logger.Logger.Printf("The termination of the reasoning program failed (DeviceUUID=%s): %v", deviceData.DeviceUUID, err)
|
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 {
|
} 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 函数中修改温度程序启动部分
|
// 在 processDevice 函数中修改温度程序启动部分
|
||||||
// 启动温度程序
|
// 启动温度程序
|
||||||
tempPort := port + 1000
|
tempPort := portFireLeave + 1000
|
||||||
tempCmdArgs := []string{
|
tempCmdArgs := []string{
|
||||||
"-s", deviceData.CameraRTSP,
|
"-s", deviceData.CameraRTSP,
|
||||||
"-t", fmt.Sprintf("%d", tempPort),
|
"-t", fmt.Sprintf("%d", tempPort),
|
||||||
@@ -371,8 +461,11 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
|
|||||||
case <-deviceCtx.Done():
|
case <-deviceCtx.Done():
|
||||||
logger.Logger.Printf("Equipment processing has been cancelled: DeviceUUID=%s", deviceData.DeviceUUID)
|
logger.Logger.Printf("Equipment processing has been cancelled: DeviceUUID=%s", deviceData.DeviceUUID)
|
||||||
// 强制停止所有子进程
|
// 强制停止所有子进程
|
||||||
if inferenceCmd.Process != nil {
|
if inferenceCmdFireLeave.Process != nil {
|
||||||
inferenceCmd.Process.Kill()
|
inferenceCmdFireLeave.Process.Kill()
|
||||||
|
}
|
||||||
|
if inferenceCmdFireCheck.Process != nil {
|
||||||
|
inferenceCmdFireCheck.Process.Kill()
|
||||||
}
|
}
|
||||||
if tempCmd.Process != nil {
|
if tempCmd.Process != nil {
|
||||||
tempCmd.Process.Kill()
|
tempCmd.Process.Kill()
|
||||||
@@ -380,9 +473,9 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
|
|||||||
tempClient.Close()
|
tempClient.Close()
|
||||||
return
|
return
|
||||||
|
|
||||||
case inferenceResult, ok := <-inferenceServer.GetDataChan():
|
case inferenceResult, ok := <-inferenceServerFireLeave.GetDataChan():
|
||||||
if !ok {
|
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
|
return
|
||||||
}
|
}
|
||||||
currentPersonCount := 0
|
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)
|
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:
|
case <-monitorTicker.C:
|
||||||
now := time.Now().Unix()
|
now := time.Now().Unix()
|
||||||
|
|
||||||
@@ -428,6 +577,7 @@ func processDevice(ctx context.Context, deviceData connect.DeviceData) {
|
|||||||
// 温度防抖
|
// 温度防抖
|
||||||
var tempStableHigh bool
|
var tempStableHigh bool
|
||||||
threshold := deviceData.TemperatureThreshold
|
threshold := deviceData.TemperatureThreshold
|
||||||
|
|
||||||
if deviceData.Temperature > threshold+0.5 {
|
if deviceData.Temperature > threshold+0.5 {
|
||||||
tempStableHigh = true
|
tempStableHigh = true
|
||||||
} else if deviceData.Temperature < threshold-0.5 {
|
} 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)",
|
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)
|
detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
|
||||||
} else if now-state.StartTime >= int64(detectionSec) {
|
} else if now-state.StartTime >= int64(detectionSec) {
|
||||||
logger.Logger.Printf("ALARM TRIGGERED (after %ds): PersonCount=%d, Temp=%.1f°C > %.1f°C (DeviceUUID=%s)",
|
currentTime := time.Now()
|
||||||
detectionSec, deviceData.PersonCount, deviceData.Temperature, threshold, deviceData.DeviceUUID)
|
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.AlarmPauseUntil = now + int64(alarmCooldown)
|
||||||
state.InCondition = false
|
state.InCondition = false
|
||||||
|
logger.Logger.Printf("[%d] 更新报警状态: 冷却时间设置为 %d 秒 (DeviceUUID=%s)",
|
||||||
|
time.Now().Unix(), alarmCooldown, deviceData.DeviceUUID)
|
||||||
|
|
||||||
|
// 更新设备数据
|
||||||
deviceDataCopy := deviceData
|
deviceDataCopy := deviceData
|
||||||
deviceDataCopy.AlarmPauseUntil = state.AlarmPauseUntil
|
deviceDataCopy.AlarmPauseUntil = state.AlarmPauseUntil
|
||||||
|
logger.Logger.Printf("[%d] 更新设备数据 (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID)
|
||||||
connect.UpdateDeviceData(deviceDataCopy)
|
connect.UpdateDeviceData(deviceDataCopy)
|
||||||
|
logger.Logger.Printf("[%d] 设备数据更新完成 (DeviceUUID=%s)", time.Now().Unix(), deviceData.DeviceUUID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if state.InCondition {
|
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)
|
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-stop:
|
case sig := <-stop:
|
||||||
logger.Logger.Printf("Upon receiving the stop signal, start cleaning...")
|
logger.Logger.Printf("Received stop signal: %v, start cleaning...", sig)
|
||||||
|
|
||||||
|
// 立即取消上下文,通知所有goroutine停止
|
||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
|
// 等待所有goroutine退出
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@@ -498,14 +676,16 @@ func waitForShutdown(ctx context.Context, cancel context.CancelFunc, wg *sync.Wa
|
|||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
logger.Logger.Printf("All goroutines exited normally")
|
logger.Logger.Printf("All goroutines exited normally")
|
||||||
case <-time.After(10 * time.Second):
|
case <-time.After(15 * time.Second):
|
||||||
logger.Logger.Printf("When the waiting time exceeds the limit, force exit")
|
logger.Logger.Printf("Waiting time exceeded, force exit")
|
||||||
// Log status before force exit
|
// 强制终止所有推理进程
|
||||||
activeDevices := GlobalDeviceManager.GetAllDevices()
|
connect.InferenceProcesses.Range(func(key, value interface{}) bool {
|
||||||
logger.Logger.Printf("It still exists when forced to exit %d An active device", len(activeDevices))
|
if cmd, ok := value.(*exec.Cmd); ok && cmd.Process != nil {
|
||||||
for uuid := range activeDevices {
|
logger.Logger.Printf("Force kill process: %v", key)
|
||||||
logger.Logger.Printf("Forcibly stop the equipment: UUID=%s", uuid)
|
cmd.Process.Kill()
|
||||||
}
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -553,12 +733,24 @@ register:
|
|||||||
logger.Logger.Printf("Service registration successful: ServiceID=%s", serviceID)
|
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
|
// 新增:订阅本地告警 topic
|
||||||
if err := connect.SubscribeTopic("/dhlr/alert", connect.HandleBuzzerAlert); err != nil {
|
if err := connect.SubscribeTopic("/dhlr/alert", connect.HandleBuzzerAlert); err != nil {
|
||||||
logger.Logger.Printf("订阅告警topic失败 (/dhlr/alert): %v", err)
|
logger.Logger.Printf("订阅告警topic失败 (/dhlr/alert): %v", err)
|
||||||
} else {
|
} else {
|
||||||
logger.Logger.Printf("成功订阅本地告警topic: /dhlr/alert")
|
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 {
|
func (dm *DeviceManager) GetAllDevices() map[string]*connect.DeviceData {
|
||||||
@@ -571,124 +763,7 @@ func (dm *DeviceManager) GetAllDevices() map[string]*connect.DeviceData {
|
|||||||
}
|
}
|
||||||
return result
|
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) {
|
func (dm *DeviceManager) RangeDevices(fn func(uuid string, device *connect.DeviceData) bool) {
|
||||||
dm.mu.Lock()
|
dm.mu.Lock()
|
||||||
defer dm.mu.Unlock()
|
defer dm.mu.Unlock()
|
||||||
@@ -710,8 +785,19 @@ func main() {
|
|||||||
if err := logger.InitLogger(); err != nil {
|
if err := logger.InitLogger(); err != nil {
|
||||||
log.Fatalf("Logger initialization failed: %v", err)
|
log.Fatalf("Logger initialization failed: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer logger.CloseLogger()
|
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() {
|
go func() {
|
||||||
for {
|
for {
|
||||||
logger.CleanupOldLogs()
|
logger.CleanupOldLogs()
|
||||||
@@ -752,6 +838,18 @@ func main() {
|
|||||||
logger.Logger.Printf("Step 4: Asynchronously initialize WebSocket connection")
|
logger.Logger.Printf("Step 4: Asynchronously initialize WebSocket connection")
|
||||||
go initWebSocketAsync(ctx, &wg)
|
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("=== Main program startup completed ===")
|
||||||
logger.Logger.Printf("System status: Running devices=%d", len(deviceDataList))
|
logger.Logger.Printf("System status: Running devices=%d", len(deviceDataList))
|
||||||
|
|
||||||
@@ -763,13 +861,24 @@ func main() {
|
|||||||
defer ticker.Stop()
|
defer ticker.Stop()
|
||||||
for range ticker.C {
|
for range ticker.C {
|
||||||
GlobalDeviceManager.RangeDevices(func(uuid string, device *connect.DeviceData) bool {
|
GlobalDeviceManager.RangeDevices(func(uuid string, device *connect.DeviceData) bool {
|
||||||
|
// 检查fire_leave推理程序心跳
|
||||||
if connect.ShouldRestartInference(uuid) {
|
if connect.ShouldRestartInference(uuid) {
|
||||||
logger.Logger.Printf("推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid)
|
logger.Logger.Printf("fire_leave推理程序心跳超时(>90s),准备重启: DeviceUUID=%s", uuid)
|
||||||
connect.StopInferenceProcess(uuid)
|
connect.StopInferenceProcess(uuid)
|
||||||
time.Sleep(6 * time.Second)
|
time.Sleep(6 * time.Second)
|
||||||
connect.StartInferenceProcess(device)
|
connect.StartInferenceProcess(device)
|
||||||
connect.UpdateInferenceHeartbeat(uuid)
|
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
|
return true
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -855,3 +964,72 @@ func debugDeviceData(deviceData connect.DeviceData) {
|
|||||||
logger.Logger.Printf(" PersonCount: %d", deviceData.PersonCount)
|
logger.Logger.Printf(" PersonCount: %d", deviceData.PersonCount)
|
||||||
logger.Logger.Printf(" Temperature: %f", deviceData.Temperature)
|
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 |
@@ -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()
|
||||||
@@ -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
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
+175
-45
@@ -1,6 +1,7 @@
|
|||||||
package video_server
|
package video_server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -16,14 +17,33 @@ import (
|
|||||||
|
|
||||||
// MergeVideo 合并视频文件 - 完全适配 mp4_merge --out 模式
|
// MergeVideo 合并视频文件 - 完全适配 mp4_merge --out 模式
|
||||||
func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
|
func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
|
||||||
|
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)
|
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) {
|
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)
|
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 秒 ===
|
// === 1. 时间对齐到 10 秒 ===
|
||||||
|
logger.Logger.Printf("[%.3f] 开始时间对齐处理 (DeviceUUID=%s, alarmTime=%d)", time.Since(startTime).Seconds(), deviceUUID, alarmTime)
|
||||||
alarmTimeAligned := alignTo10Seconds(alarmTime)
|
alarmTimeAligned := alignTo10Seconds(alarmTime)
|
||||||
alarmTimeStr := time.Unix(alarmTimeAligned, 0).Format("20060102150405")
|
alarmTimeStr := time.Unix(alarmTimeAligned, 0).Format("20060102150405")
|
||||||
|
logger.Logger.Printf("[%.3f] 时间对齐结果: alarmTime=%d → aligned=%d (DeviceUUID=%s)", time.Since(startTime).Seconds(), alarmTime, alarmTimeAligned, deviceUUID)
|
||||||
|
|
||||||
beforeTime := alarmTimeAligned - 10
|
beforeTime := alarmTimeAligned - 10
|
||||||
afterTime := alarmTimeAligned + 10
|
afterTime := alarmTimeAligned + 10
|
||||||
@@ -37,67 +57,85 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
|
|||||||
afterStr: filepath.Join(videoDir, afterStr+".mp4"),
|
afterStr: filepath.Join(videoDir, afterStr+".mp4"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 查找视频文件
|
||||||
videoFiles := []string{}
|
videoFiles := []string{}
|
||||||
|
logger.Logger.Printf("[%.3f] 开始查找视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
|
||||||
for name, path := range paths {
|
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 {
|
if _, err := os.Stat(path); err == nil {
|
||||||
videoFiles = append(videoFiles, path)
|
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 {
|
} 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. 补充替代文件 ===
|
// === 2. 补充替代文件 ===
|
||||||
|
logger.Logger.Printf("[%.3f] 当前找到 %d 个视频文件,开始补充替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(videoFiles), deviceUUID)
|
||||||
if len(videoFiles) < 3 {
|
if len(videoFiles) < 3 {
|
||||||
allFiles, _ := os.ReadDir(videoDir)
|
logger.Logger.Printf("[%.3f] 视频文件不足3个,开始查找替代文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
|
||||||
var candidates []struct {
|
allFiles, err := os.ReadDir(videoDir)
|
||||||
path string
|
if err != nil {
|
||||||
ts int64
|
logger.Logger.Printf("[%.3f] 读取目录失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
|
||||||
}
|
} else {
|
||||||
for _, f := range allFiles {
|
logger.Logger.Printf("[%.3f] 读取目录所有文件,共 %d 个文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(allFiles), deviceUUID)
|
||||||
if !f.IsDir() && strings.HasSuffix(f.Name(), ".mp4") && !strings.Contains(f.Name(), "_joined") {
|
var candidates []struct {
|
||||||
if ts, err := parseSimpleTimestamp(f.Name()); err == nil {
|
path string
|
||||||
candidates = append(candidates, struct {
|
ts int64
|
||||||
path string
|
}
|
||||||
ts int64
|
for _, f := range allFiles {
|
||||||
}{filepath.Join(videoDir, f.Name()), ts})
|
if !f.IsDir() && strings.HasSuffix(f.Name(), ".mp4") && !strings.Contains(f.Name(), "_joined") {
|
||||||
|
if ts, err := parseSimpleTimestamp(f.Name()); err == nil {
|
||||||
|
candidates = append(candidates, struct {
|
||||||
|
path string
|
||||||
|
ts int64
|
||||||
|
}{filepath.Join(videoDir, f.Name()), ts})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
logger.Logger.Printf("[%.3f] 找到 %d 个候选视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), len(candidates), deviceUUID)
|
||||||
targets := []int64{beforeTime, alarmTimeAligned, afterTime}
|
targets := []int64{beforeTime, alarmTimeAligned, afterTime}
|
||||||
for _, target := range targets {
|
for _, target := range targets {
|
||||||
if containsTimestamp(videoFiles, target) {
|
if containsTimestamp(videoFiles, target) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
closest := findClosest(candidates, target)
|
closest := findClosest(candidates, target)
|
||||||
if closest != "" && !contains(videoFiles, closest) {
|
if closest != "" && !contains(videoFiles, closest) {
|
||||||
videoFiles = append(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 {
|
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))
|
return "", fmt.Errorf("insufficient video files: %d", len(videoFiles))
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 3. 排序 ===
|
// === 3. 排序 ===
|
||||||
|
logger.Logger.Printf("[%.3f] 开始排序视频文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
|
||||||
sort.Slice(videoFiles, func(i, j int) bool {
|
sort.Slice(videoFiles, func(i, j int) bool {
|
||||||
return getTimestampFromPath(videoFiles[i]) < getTimestampFromPath(videoFiles[j])
|
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 {
|
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% 可控)===
|
// === 4. 显式指定输出文件(100% 可控)===
|
||||||
outputFile := fmt.Sprintf("%s.mp4_joined.mp4", alarmTimeStr)
|
outputFile := fmt.Sprintf("%s.mp4_joined.mp4", alarmTimeStr)
|
||||||
joinedPath := filepath.Join(videoDir, outputFile)
|
joinedPath := filepath.Join(videoDir, outputFile)
|
||||||
|
logger.Logger.Printf("[%.3f] 输出文件路径: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
|
||||||
|
|
||||||
// 清理旧文件(防止冲突)
|
// 清理旧文件(防止冲突)
|
||||||
|
logger.Logger.Printf("[%.3f] 清理旧的输出文件 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
|
||||||
if err := os.Remove(joinedPath); err != nil && !os.IsNotExist(err) {
|
if err := os.Remove(joinedPath); err != nil && !os.IsNotExist(err) {
|
||||||
logger.Logger.Printf("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 ===
|
// === 5. 调用 mp4_merge,显式 --out ===
|
||||||
@@ -105,42 +143,61 @@ func MergeVideo(deviceUUID string, alarmTime int64) (string, error) {
|
|||||||
cmd := exec.Command("mp4_merge", args...)
|
cmd := exec.Command("mp4_merge", args...)
|
||||||
cmd.Stdout = logger.Logger.Writer()
|
cmd.Stdout = logger.Logger.Writer()
|
||||||
cmd.Stderr = logger.Logger.Writer()
|
cmd.Stderr = logger.Logger.Writer()
|
||||||
|
logger.Logger.Printf("[%.3f] 开始执行视频合并命令: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), cmd.Args, deviceUUID)
|
||||||
|
|
||||||
|
mergeStartTime := time.Now()
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
|
logger.Logger.Printf("[%.3f] 视频合并命令执行失败 (耗时:%.3fs): %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), time.Since(mergeStartTime).Seconds(), err, deviceUUID)
|
||||||
return "", fmt.Errorf("mp4_merge failed: %v", err)
|
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. 验证输出文件存在 ===
|
// === 6. 验证输出文件存在 ===
|
||||||
|
logger.Logger.Printf("[%.3f] 验证输出文件是否存在: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, deviceUUID)
|
||||||
if _, err := os.Stat(joinedPath); err != nil {
|
if _, err := os.Stat(joinedPath); err != nil {
|
||||||
|
logger.Logger.Printf("[%.3f] 输出文件未创建: %s, error: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), joinedPath, err, deviceUUID)
|
||||||
return "", fmt.Errorf("output file not created: %s, error: %v", joinedPath, err)
|
return "", fmt.Errorf("output file not created: %s, error: %v", joinedPath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := os.Stat(joinedPath)
|
info, err := os.Stat(joinedPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Logger.Printf("[%.3f] 获取输出文件信息失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
|
||||||
return "", fmt.Errorf("stat output file failed: %v", err)
|
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/ ===
|
// === 7. 移动到 /data/upload/ ===
|
||||||
|
logger.Logger.Printf("[%.3f] 开始移动文件到 /data/upload/ 目录 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
|
||||||
t := time.Unix(alarmTimeAligned, 0)
|
t := time.Unix(alarmTimeAligned, 0)
|
||||||
year, month, day := t.Date()
|
year, month, day := t.Date()
|
||||||
timestampPart := fmt.Sprintf("%d-%d-%d-%d", year, int(month), day, alarmTimeAligned)
|
timestampPart := fmt.Sprintf("%d-%d-%d-%d", year, int(month), day, alarmTimeAligned)
|
||||||
finalName := fmt.Sprintf("%s-%s.MP4", deviceUUID, timestampPart)
|
finalName := fmt.Sprintf("%s-%s.MP4", deviceUUID, timestampPart)
|
||||||
finalPath := filepath.Join("/data/upload", finalName)
|
finalPath := filepath.Join("/data/upload", finalName)
|
||||||
|
logger.Logger.Printf("[%.3f] 最终文件路径: %s (DeviceUUID=%s)", time.Since(startTime).Seconds(), finalPath, deviceUUID)
|
||||||
|
|
||||||
|
logger.Logger.Printf("[%.3f] 确保 /data/upload 目录存在 (DeviceUUID=%s)", time.Since(startTime).Seconds(), deviceUUID)
|
||||||
if err := os.MkdirAll("/data/upload", 0755); err != nil {
|
if err := os.MkdirAll("/data/upload", 0755); err != nil {
|
||||||
|
logger.Logger.Printf("[%.3f] 创建 /data/upload 目录失败: %v (DeviceUUID=%s)", time.Since(startTime).Seconds(), err, deviceUUID)
|
||||||
return "", fmt.Errorf("create /data/upload failed: %v", err)
|
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 {
|
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)
|
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 {
|
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
|
return finalPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,24 +238,89 @@ func abs(x int64) int64 {
|
|||||||
return x
|
return x
|
||||||
}
|
}
|
||||||
|
|
||||||
// copyFile 支持跨分区复制
|
// copyFile 支持跨分区复制,带超时机制
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
in, err := os.Open(src)
|
startTime := time.Now()
|
||||||
if err != nil {
|
logger.Logger.Printf("[%.3f] 开始复制文件: %s → %s", time.Since(startTime).Seconds(), src, dst)
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer in.Close()
|
|
||||||
|
|
||||||
out, err := os.Create(dst)
|
// 创建带超时的上下文
|
||||||
if err != nil {
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
return err
|
defer cancel()
|
||||||
}
|
|
||||||
defer out.Close()
|
|
||||||
|
|
||||||
if _, err = io.Copy(out, in); err != nil {
|
// 创建通道用于接收复制结果
|
||||||
return err
|
type copyResult struct {
|
||||||
|
n int64
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
return out.Sync()
|
resultChan := make(chan copyResult, 1)
|
||||||
|
|
||||||
|
// 在goroutine中执行复制操作
|
||||||
|
go func() {
|
||||||
|
// 打开源文件
|
||||||
|
logger.Logger.Printf("[%.3f] 打开源文件: %s", time.Since(startTime).Seconds(), src)
|
||||||
|
in, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
resultChan <- copyResult{0, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
|
||||||
|
// 创建目标文件
|
||||||
|
logger.Logger.Printf("[%.3f] 创建目标文件: %s", time.Since(startTime).Seconds(), dst)
|
||||||
|
out, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
resultChan <- copyResult{0, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
// 获取源文件大小
|
||||||
|
fileInfo, err := in.Stat()
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Printf("[%.3f] 获取源文件信息失败: %v", time.Since(startTime).Seconds(), err)
|
||||||
|
} else {
|
||||||
|
logger.Logger.Printf("[%.3f] 源文件大小: %d 字节", time.Since(startTime).Seconds(), fileInfo.Size())
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行复制
|
||||||
|
logger.Logger.Printf("[%.3f] 开始执行文件复制操作", time.Since(startTime).Seconds())
|
||||||
|
copyStartTime := time.Now()
|
||||||
|
n, err := io.Copy(out, in)
|
||||||
|
elapsed := time.Since(copyStartTime)
|
||||||
|
if err != nil {
|
||||||
|
logger.Logger.Printf("[%.3f] 文件复制失败: %v, 已复制 %d 字节, 耗时 %v", time.Since(startTime).Seconds(), err, n, elapsed)
|
||||||
|
resultChan <- copyResult{n, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Logger.Printf("[%.3f] 文件复制成功: 复制 %d 字节, 耗时 %v", time.Since(startTime).Seconds(), n, elapsed)
|
||||||
|
|
||||||
|
// 同步文件到磁盘
|
||||||
|
logger.Logger.Printf("[%.3f] 开始同步文件到磁盘", time.Since(startTime).Seconds())
|
||||||
|
syncStartTime := time.Now()
|
||||||
|
if err = out.Sync(); err != nil {
|
||||||
|
logger.Logger.Printf("[%.3f] 文件同步失败: %v, 耗时 %v", time.Since(startTime).Seconds(), err, time.Since(syncStartTime))
|
||||||
|
resultChan <- copyResult{n, err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Logger.Printf("[%.3f] 文件同步成功, 耗时 %v", time.Since(startTime).Seconds(), time.Since(syncStartTime))
|
||||||
|
|
||||||
|
resultChan <- copyResult{n, nil}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 等待复制结果或超时
|
||||||
|
select {
|
||||||
|
case result := <-resultChan:
|
||||||
|
if result.err != nil {
|
||||||
|
return result.err
|
||||||
|
}
|
||||||
|
case <-ctx.Done():
|
||||||
|
err := ctx.Err()
|
||||||
|
logger.Logger.Printf("[%.3f] 文件复制操作超时: %v", time.Since(startTime).Seconds(), err)
|
||||||
|
return fmt.Errorf("copy file timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Logger.Printf("[%.3f] 文件复制操作完成", time.Since(startTime).Seconds())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// alignTo10Seconds 将时间对齐到最近的10秒间隔
|
// alignTo10Seconds 将时间对齐到最近的10秒间隔
|
||||||
@@ -294,7 +416,15 @@ func CleanupOldVideos(deviceUUID string) {
|
|||||||
|
|
||||||
// CleanupOldRawVideos 清理推理程序生成的原始10s视频,保留最近60条
|
// CleanupOldRawVideos 清理推理程序生成的原始10s视频,保留最近60条
|
||||||
func CleanupOldRawVideos(deviceUUID string) {
|
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) {
|
if _, err := os.Stat(rawDir); os.IsNotExist(err) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,36 +16,62 @@ import (
|
|||||||
|
|
||||||
// UploadToNginx 上传视频到 Nginx 服务器,返回 URL
|
// UploadToNginx 上传视频到 Nginx 服务器,返回 URL
|
||||||
func UploadToNginx(filePath string, eventParams map[string]any, uploadURL string) (string, error) {
|
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)
|
file, err := os.Open(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Logger.Printf("打开文件失败: %v, 耗时: %v", err, time.Since(startTime))
|
||||||
return "", fmt.Errorf("open file failed: %w", err)
|
return "", fmt.Errorf("open file failed: %w", err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
logger.Logger.Printf("文件打开成功, 耗时: %v", time.Since(startTime))
|
||||||
|
|
||||||
|
// 创建多部分表单
|
||||||
|
logger.Logger.Printf("创建多部分表单")
|
||||||
body := new(bytes.Buffer)
|
body := new(bytes.Buffer)
|
||||||
writer := multipart.NewWriter(body)
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
// 创建表单文件字段
|
||||||
|
logger.Logger.Printf("创建表单文件字段: %s", filepath.Base(filePath))
|
||||||
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
|
part, err := writer.CreateFormFile("file", filepath.Base(filePath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Logger.Printf("创建表单文件字段失败: %v", err)
|
||||||
return "", fmt.Errorf("create form file failed: %w", err)
|
return "", fmt.Errorf("create form file failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 复制文件内容到表单
|
||||||
|
logger.Logger.Printf("开始复制文件内容到表单")
|
||||||
|
copyStartTime := time.Now()
|
||||||
if _, err = io.Copy(part, file); err != nil {
|
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)
|
return "", fmt.Errorf("copy file failed: %w", err)
|
||||||
}
|
}
|
||||||
|
logger.Logger.Printf("文件内容复制成功, 耗时: %v", time.Since(copyStartTime))
|
||||||
|
|
||||||
|
// 序列化事件参数
|
||||||
|
logger.Logger.Printf("序列化事件参数")
|
||||||
eventJSON, err := json.Marshal(eventParams)
|
eventJSON, err := json.Marshal(eventParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Logger.Printf("序列化事件参数失败: %v", err)
|
||||||
return "", fmt.Errorf("marshal event failed: %w", err)
|
return "", fmt.Errorf("marshal event failed: %w", err)
|
||||||
}
|
}
|
||||||
logger.Logger.Printf("[Upload Event parameters]eventParams: %v", eventParams)
|
logger.Logger.Printf("[Upload Event parameters]eventParams: %v", eventParams)
|
||||||
logger.Logger.Printf("[Upload Event parameters]eventJSON: %s", string(eventJSON))
|
logger.Logger.Printf("[Upload Event parameters]eventJSON: %s", string(eventJSON))
|
||||||
|
|
||||||
|
// 写入事件字段
|
||||||
|
logger.Logger.Printf("写入事件字段到表单")
|
||||||
if err := writer.WriteField("event", string(eventJSON)); err != nil {
|
if err := writer.WriteField("event", string(eventJSON)); err != nil {
|
||||||
|
logger.Logger.Printf("写入事件字段失败: %v", err)
|
||||||
return "", fmt.Errorf("write event field failed: %w", err)
|
return "", fmt.Errorf("write event field failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭表单写入器
|
||||||
|
logger.Logger.Printf("关闭表单写入器")
|
||||||
if err := writer.Close(); err != nil {
|
if err := writer.Close(); err != nil {
|
||||||
|
logger.Logger.Printf("关闭表单写入器失败: %v", err)
|
||||||
return "", fmt.Errorf("close writer failed: %w", 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]Content-Type: %s", writer.FormDataContentType())
|
||||||
logger.Logger.Printf("[HTTP Request]Body 长度: %d bytes", body.Len())
|
logger.Logger.Printf("[HTTP Request]Body 长度: %d bytes", body.Len())
|
||||||
|
|
||||||
|
// 创建HTTP请求
|
||||||
|
logger.Logger.Printf("创建HTTP请求")
|
||||||
req, err := http.NewRequest("POST", uploadURL, body)
|
req, err := http.NewRequest("POST", uploadURL, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Logger.Printf("创建HTTP请求失败: %v", err)
|
||||||
return "", fmt.Errorf("create request failed: %w", err)
|
return "", fmt.Errorf("create request failed: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
|
||||||
|
// 发送HTTP请求
|
||||||
|
logger.Logger.Printf("发送HTTP请求到 Nginx")
|
||||||
|
httpStartTime := time.Now()
|
||||||
client := &http.Client{Timeout: 180 * time.Second}
|
client := &http.Client{Timeout: 180 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logger.Logger.Printf("发送HTTP请求失败: %v, 耗时: %v", err, time.Since(httpStartTime))
|
||||||
return "", fmt.Errorf("upload failed: %w", err)
|
return "", fmt.Errorf("upload failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
logger.Logger.Printf("HTTP请求发送成功, 耗时: %v", time.Since(httpStartTime))
|
||||||
|
|
||||||
|
// 读取响应
|
||||||
|
logger.Logger.Printf("读取HTTP响应")
|
||||||
|
respStartTime := time.Now()
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
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]Status: %d", resp.StatusCode)
|
||||||
logger.Logger.Printf("[nginx response]Body: %s", string(respBody))
|
logger.Logger.Printf("[nginx response]Body: %s", string(respBody))
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
logger.Logger.Printf("Nginx返回错误状态码: %d", resp.StatusCode)
|
||||||
return "", fmt.Errorf("nginx error %d: %s", resp.StatusCode, string(respBody))
|
return "", fmt.Errorf("nginx error %d: %s", resp.StatusCode, string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 解析响应
|
||||||
|
logger.Logger.Printf("解析Nginx响应")
|
||||||
var result struct {
|
var result struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Result json.RawMessage `json:"result"`
|
Result json.RawMessage `json:"result"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(respBody, &result); err != nil {
|
if err := json.Unmarshal(respBody, &result); err != nil {
|
||||||
|
logger.Logger.Printf("解析响应失败: %v", err)
|
||||||
return "", fmt.Errorf("parse response failed: %w", err)
|
return "", fmt.Errorf("parse response failed: %w", err)
|
||||||
}
|
}
|
||||||
if result.Code != 0 {
|
if result.Code != 0 {
|
||||||
|
logger.Logger.Printf("Nginx返回错误代码: %d", result.Code)
|
||||||
return "", fmt.Errorf("nginx error: %s", string(respBody))
|
return "", fmt.Errorf("nginx error: %s", string(respBody))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取视频URL
|
||||||
|
logger.Logger.Printf("提取视频URL")
|
||||||
var videoURL string
|
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("[ Upload Successful ] Local file: %s → Remote URL: %s", filePath, videoURL)
|
||||||
|
logger.Logger.Printf("视频上传完成, 总耗时: %v", time.Since(startTime))
|
||||||
return videoURL, nil
|
return videoURL, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+60
@@ -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
|
||||||
Reference in New Issue
Block a user