Browse Source

Merge branch 'lq' of terry/ipole_edge_server into master

lq 3 months ago
parent
commit
a5060698f2
100 changed files with 4067 additions and 1830 deletions
  1. 1 0
      cloud/ipolesvr/bizalarm.go
  2. 1 0
      cloud/ipolesvr/cableGuardianHandler.go
  3. 1 0
      cloud/ipolesvr/chzbconcentrator.go
  4. 1 0
      cloud/ipolesvr/chzbconcentratormgr.go
  5. 1 0
      cloud/ipolesvr/cltledmgr.go
  6. 1 2
      cloud/ipolesvr/conf/main.ini
  7. 13 12
      cloud/ipolesvr/config.go
  8. 1 0
      cloud/ipolesvr/datastat.go
  9. 1 0
      cloud/ipolesvr/eventmgr.go
  10. 54 6
      cloud/ipolesvr/gwhandler.go
  11. 1 0
      cloud/ipolesvr/hlzbconcentrator.go
  12. 2 1
      cloud/ipolesvr/hlzbconcentratormgr.go
  13. 1 0
      cloud/ipolesvr/ipcmgr.go
  14. 1 0
      cloud/ipolesvr/itsmgr.go
  15. 1 0
      cloud/ipolesvr/lampstrategy.go
  16. 15 3
      cloud/ipolesvr/main.go
  17. 1 0
      cloud/ipolesvr/mbdevhandler.go
  18. 0 128
      cloud/ipolesvr/mqttclient.go
  19. 3 4
      cloud/ipolesvr/mqttmgr.go
  20. 3 4
      cloud/ipolesvr/mqttmgr_hl.go
  21. 1 0
      cloud/ipolesvr/radarhandler.go
  22. 1 0
      cloud/ipolesvr/sosmgr.go
  23. 4 2
      cloud/ipolesvr/srsmgr.go
  24. 11 4
      cloud/ipolesvr/ymlampcontrollermgr.go
  25. 1 2
      cloud/mediasvr/main.go
  26. 0 128
      cloud/mqttforward/mqttclient.go
  27. 6 6
      cloud/mqttforward/mqttmgr.go
  28. 0 1
      cloud/websvr/controllers/cbase.go
  29. 9 9
      cloud/websvr/controllers/ccamera.go
  30. 862 0
      cloud/websvr/controllers/cgateway.go
  31. 2 2
      cloud/websvr/controllers/ciotmodel.go
  32. 8 5
      cloud/websvr/controllers/clampcontroller.go
  33. 1 0
      cloud/websvr/controllers/eventmgr.go
  34. 23 10
      cloud/websvr/controllers/mqtthandler.go
  35. 23 10
      cloud/websvr/controllers/mqtthandlerHL.go
  36. 189 0
      cloud/websvr/routers/commentsRouter_.go
  37. 1 2
      common/configor/utils.go
  38. 23 0
      common/models/gateway_deploy.go
  39. 19 0
      common/models/gatewaymodel.go
  40. 6 0
      common/models/init.go
  41. 145 0
      common/mqtt/client_wrapper.go
  42. 216 0
      common/mqtt/mgr.go
  43. 14 17
      common/mqtt/mqtt.go
  44. 0 3
      common/onvif/examples/discovery/discovery.go
  45. 0 3
      common/onvif/examples/recording/recording.go
  46. 2 2
      common/onvif/soap/soap.go
  47. 9 7
      common/protocol/config.go
  48. 120 0
      common/protocol/deploy.go
  49. 91 0
      common/protocol/deploy_test.go
  50. 7 2
      common/protocol/topic.go
  51. 2 2
      common/util/gzip.go
  52. 233 0
      common/util/taglog.go
  53. 1 0
      edge/camera/ipc_device.go
  54. 2 3
      edge/camera/ipc_media.go
  55. 3 0
      edge/camera/main.go
  56. 37 0
      edge/camera/mqtt_init.go
  57. 0 125
      edge/camera/mqttclient.go
  58. 0 187
      edge/camera/mqttmgr.go
  59. 12 13
      edge/ipole/auto_reload.go
  60. 114 0
      edge/ipole/cleanlog.go
  61. 44 44
      edge/ipole/concentrator.go
  62. 2 0
      edge/ipole/conf/app.json
  63. 2 3
      edge/ipole/config.go
  64. 239 0
      edge/ipole/deploy.html
  65. 6 0
      edge/ipole/deployhtml.go
  66. 217 0
      edge/ipole/deployhttp.go
  67. 382 0
      edge/ipole/deploymgr.go
  68. 23 0
      edge/ipole/deploymgr_linux.go
  69. 9 0
      edge/ipole/deploymgr_win.go
  70. 62 4
      edge/ipole/devmgr.go
  71. 29 12
      edge/ipole/handlefile.go
  72. 339 0
      edge/ipole/healthcheck.go
  73. 69 0
      edge/ipole/logcfg.go
  74. 39 19
      edge/ipole/main.go
  75. 41 37
      edge/ipole/modbusrtu.go
  76. 3 3
      edge/ipole/monitor.go
  77. 40 0
      edge/ipole/mqtt_init.go
  78. 0 125
      edge/ipole/mqttclient.go
  79. 50 16
      edge/ipole/mqtthandle.go
  80. 0 183
      edge/ipole/mqttmgr.go
  81. 7 7
      edge/ipole/radar.go
  82. 16 4
      edge/ipole/serialmgr.go
  83. 1 2
      edge/ipole/uploaddata.go
  84. 48 36
      edge/ipole/ym485.go
  85. 1 0
      edge/its/datamgr.go
  86. 6 6
      edge/its/main.go
  87. 37 0
      edge/its/mqtt_init.go
  88. 0 125
      edge/its/mqttclient.go
  89. 0 180
      edge/its/mqttmgr.go
  90. 2 1
      edge/its/websvr.go
  91. 3 0
      edge/its_win/main.go
  92. 37 0
      edge/its_win/mqtt_init.go
  93. 0 125
      edge/its_win/mqttclient.go
  94. 0 180
      edge/its_win/mqttmgr.go
  95. 2 1
      edge/its_win/websvr.go
  96. 3 3
      edge/led_screen/clt_client/delete.go
  97. 3 3
      edge/led_screen/clt_client/get.go
  98. 2 3
      edge/led_screen/clt_client/post.go
  99. 2 3
      edge/led_screen/clt_client/put.go
  100. 0 0
      edge/led_screen/led_device.go

+ 1 - 0
cloud/ipolesvr/bizalarm.go

@@ -71,6 +71,7 @@ func (o *BizAlarmMgr) PushData(value *BizValue) {
 func (o *BizAlarmMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("BizAlarmMgr.Handler发生异常:%s", string(debug.Stack()))
 		}

+ 1 - 0
cloud/ipolesvr/cableGuardianHandler.go

@@ -68,6 +68,7 @@ func (o *cableGuardianHandler) HandlerData(m mqtt.Message) {
 func (o *cableGuardianHandler) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("cableGuardianHandler.Handler:%v发生异常:%s", args, string(debug.Stack()))
 		}

+ 1 - 0
cloud/ipolesvr/chzbconcentrator.go

@@ -76,6 +76,7 @@ func (o *ChZigbeeConcentrator) HandleQueue() {
 	defer func() {
 		if err := recover(); err != nil {
 			logrus.Error("ChZigbeeConcentrator.HandleQueue发生异常:", string(debug.Stack()))
+			time.Sleep(5 * time.Second)
 			go o.HandleQueue()
 		}
 	}()

+ 1 - 0
cloud/ipolesvr/chzbconcentratormgr.go

@@ -60,6 +60,7 @@ func (o *ChzbConcentratorMgr) HandlerData(m mqtt.Message) {
 func (o *ChzbConcentratorMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("ChzbConcentratorMgr.Handler发生异常:%v", err)
 			logrus.Errorf("ChzbConcentratorMgr.Handler发生异常,堆栈信息:%s", string(debug.Stack()))

+ 1 - 0
cloud/ipolesvr/cltledmgr.go

@@ -51,6 +51,7 @@ func (o *CltLedMgr) HandlerData(m mqtt.Message) {
 func (o *CltLedMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("CltLedMgr.Handler发生异常:%v", err)
 			logrus.Errorf("CltLedMgr.Handler发生异常,堆栈信息:%s", string(debug.Stack()))

+ 1 - 2
cloud/ipolesvr/conf/main.ini

@@ -1,5 +1,4 @@
 [ipolesvr]
 GB28181API = http://192.168.110.28:1985/api/v1/gb28181
 SRSLIVEDIR = /data/live
-
-
+HLDefaultTenant = 100000

+ 13 - 12
cloud/ipolesvr/config.go

@@ -39,13 +39,13 @@ func ParseTopic(topic string) (string, string, string, string, error) {
 }
 
 // ParseTopicHL 获取租户、设备类型、设备编码、MQTT协议topic固定部分(定义在topic.go文件)
-func ParseTopicHL(topic string) (string, string, error) {
+func ParseTopicHL(topic string, defaultTenant string) (string, string, error) {
 	strList := strings.Split(topic, "/")
 	if len(strList) < 4 {
 		return "", "", errors.New("不支持的topic")
 	}
-	if strList[3] == "longchi" {
-		strList[0] = "100000"
+	if strList[3] == "longchi" && defaultTenant != "" {
+		strList[0] = defaultTenant
 	}
 	return strList[0], topic, nil
 }
@@ -61,15 +61,16 @@ func ParseTopicCltLed(topic string) (string, string, string, string, string, err
 }
 
 type MainConfig struct {
-	GB28181API    string
-	SRSLIVEDIR    string
-	SRSLIVEMP4    int
-	SDKAppId      string
-	SecretId      string
-	SecretKey     string
-	SMSKey        string
-	SMSTemplateId string
-	SMSReceiver   []string
+	GB28181API      string
+	SRSLIVEDIR      string
+	SRSLIVEMP4      int
+	SDKAppId        string
+	SecretId        string
+	SecretKey       string
+	SMSKey          string
+	SMSTemplateId   string
+	SMSReceiver     []string
+	HLDefaultTenant string
 }
 
 func (o *MainConfig) ReadConfig(path, sec string) bool {

+ 1 - 0
cloud/ipolesvr/datastat.go

@@ -61,6 +61,7 @@ func StatData(args ...interface{}) interface{} {
 		if err := recover(); err != nil {
 			logrus.Error("StatData发生异常:", err)
 			logrus.Error("StatData发生异常,堆栈:", string(debug.Stack()))
+			time.Sleep(time.Second)
 			gopool.Add(StatData, 1)
 		}
 	}()

+ 1 - 0
cloud/ipolesvr/eventmgr.go

@@ -52,6 +52,7 @@ func (o *EventMgr) PushEvent(eo *EventObject) {
 func (o *EventMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("EventMgr.Handler发生异常:%s", string(debug.Stack()))
 		}

+ 54 - 6
cloud/ipolesvr/gwhandler.go

@@ -1,7 +1,6 @@
 package main
 
 import (
-	"io/ioutil"
 	"os"
 	"path/filepath"
 	"runtime"
@@ -34,8 +33,8 @@ type GwHandler struct {
 }
 
 func (o *GwHandler) SubscribeTopics() {
-	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_ONLINE), mqtt.AtMostOnce, o.HandlerData)
-	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_WILL), mqtt.AtMostOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_ONLINE), mqtt.AtLeastOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_WILL), mqtt.AtLeastOnce, o.HandlerData)
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_APP_ACK), mqtt.AtMostOnce, o.HandlerData)
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_SET_APP_ACK), mqtt.AtMostOnce, o.HandlerData)
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_SERIAL_ACK), mqtt.AtMostOnce, o.HandlerData)
@@ -46,9 +45,12 @@ func (o *GwHandler) SubscribeTopics() {
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_SET_MODEL_ACK), mqtt.AtMostOnce, o.HandlerData)
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_LOG_ACK), mqtt.AtMostOnce, o.HandlerData)
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_REMOVE_LOG_ACK), mqtt.AtMostOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_LOG_CFG_ACK), mqtt.AtMostOnce, o.HandlerData)
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_SYS_ACK), mqtt.AtMostOnce, o.HandlerData)
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_ITS_ACK), mqtt.AtMostOnce, o.HandlerData)
 	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_ONVIFDEV_ACK), mqtt.AtMostOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_DEPLOY_ACK), mqtt.AtMostOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_GATEWAY, protocol.TP_GW_DEPLOY_HEALTH), mqtt.AtMostOnce, o.HandlerData)
 }
 
 func (o *GwHandler) HandlerData(m mqtt.Message) {
@@ -66,6 +68,7 @@ func (o *GwHandler) HandlerData(m mqtt.Message) {
 func (o *GwHandler) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("GwHandler.Handler:%v发生异常:%s", args, string(debug.Stack()))
 		}
@@ -100,7 +103,7 @@ func (o *GwHandler) Handler(args ...interface{}) interface{} {
 				cacheState(obj.Id, obj.Time, 1)
 				GetEventMgr().PushEvent(&EventObject{ID: obj.Id, EventType: models.ET_OFFLINE, Time: util.MlNow()})
 			}
-		case protocol.TP_GW_SET_APP_ACK, protocol.TP_GW_SET_MODEL_ACK, protocol.TP_GW_SET_RTU_ACK, protocol.TP_GW_SET_SERIAL_ACK, protocol.TP_GW_REMOVE_LOG_ACK:
+		case protocol.TP_GW_SET_APP_ACK, protocol.TP_GW_SET_MODEL_ACK, protocol.TP_GW_SET_RTU_ACK, protocol.TP_GW_SET_SERIAL_ACK, protocol.TP_GW_REMOVE_LOG_ACK, protocol.TP_GW_LOG_CFG_ACK:
 			var obj protocol.Pack_Ack
 			if err := obj.DeCode(m.PayloadString()); err == nil {
 				o := models.DeviceCmdRecord{
@@ -314,6 +317,51 @@ func (o *GwHandler) Handler(args ...interface{}) interface{} {
 					}
 				}
 			}
+		case protocol.TP_GW_DEPLOY_ACK:
+			var ack protocol.Pack_DeployAck
+			if err := ack.DeCode(m.PayloadString()); err != nil {
+				util.GetTagLog().Errorf("sys", "DEPLOY_ACK DeCode失败: %v", err)
+				break
+			}
+			util.GetTagLog().Infof("sys", "DEPLOY_ACK: gid=%s version=%s success=%v", ack.Header.Gid, ack.Data.Version, ack.Data.Success)
+			status := uint8(1)
+			if !ack.Data.Success {
+				status = 2
+			}
+			models.G_db.Model(&models.GatewayDeploy{}).
+				Where("gid = ? AND to_version = ? AND status = 0", ack.Header.Gid, ack.Data.Version).
+				Updates(map[string]interface{}{
+					"status":      status,
+					"error_msg":   ack.Data.Error,
+					"update_time": time.Now(),
+				})
+
+		case protocol.TP_GW_DEPLOY_HEALTH:
+			var health protocol.Pack_HealthCheck
+			if err := health.DeCode(m.PayloadString()); err != nil {
+				util.GetTagLog().Errorf("sys", "DEPLOY_HEALTH DeCode失败: %v", err)
+				break
+			}
+			util.GetTagLog().Infof("sys", "DEPLOY_HEALTH: gid=%s version=%s phase=%d success=%v",
+				health.Header.Gid, health.Data.Version, health.Data.Phase, health.Data.Success)
+
+			resultJSON, _ := json.Marshal(health.Data)
+			updateFields := map[string]interface{}{
+				"update_time": time.Now(),
+			}
+			if health.Data.Phase == 1 {
+				updateFields["phase1_result"] = string(resultJSON)
+				if !health.Data.Success {
+					updateFields["status"] = uint8(3)
+				}
+			} else {
+				updateFields["phase2_result"] = string(resultJSON)
+			}
+
+			models.G_db.Model(&models.GatewayDeploy{}).
+				Where("gid = ? AND to_version = ?", health.Header.Gid, health.Data.Version).
+				Updates(updateFields)
+
 		default:
 			logrus.Warnf("GwHandler.Handler:收到暂不支持的主题:%s", topic)
 		}
@@ -321,13 +369,13 @@ func (o *GwHandler) Handler(args ...interface{}) interface{} {
 }
 
 func SaveFile(tenant, GID, strType string, fo []protocol.FileObject) {
-	Dir := tenant + string(filepath.Separator) + GID + string(filepath.Separator) + strType + string(filepath.Separator)
+	Dir := "/opt/ipole_data/gateway_logs" + string(filepath.Separator) + tenant + string(filepath.Separator) + GID + string(filepath.Separator) + strType + string(filepath.Separator)
 	err := os.MkdirAll(Dir, os.ModePerm)
 	if err != nil {
 		return
 	}
 	for _, v := range fo {
-		if err := ioutil.WriteFile(Dir+v.File, []byte(v.Content), os.ModePerm); err != nil {
+		if err := os.WriteFile(Dir+v.File, []byte(v.Content), os.ModePerm); err != nil {
 			logrus.Errorf("SaveFile:保存模型文件失败,文件名:%s,原因:%s", Dir+v.File, err.Error())
 		}
 	}

+ 1 - 0
cloud/ipolesvr/hlzbconcentrator.go

@@ -74,6 +74,7 @@ func (o *HlZigbeeConcentrator) HandleQueue() {
 	defer func() {
 		if err := recover(); err != nil {
 			logrus.Error("HlZigbeeConcentrator.HandleQueue发生异常:", string(debug.Stack()))
+			time.Sleep(5 * time.Second)
 			go o.HandleQueue()
 		}
 	}()

+ 2 - 1
cloud/ipolesvr/hlzbconcentratormgr.go

@@ -53,6 +53,7 @@ func (o *HlZigbeeConcentratorMgr) HandlerData(m mqtt.Message) {
 func (o *HlZigbeeConcentratorMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("HlZigbeeConcentratorMgr.Handler发生异常:%v", err)
 			logrus.Errorf("HlZigbeeConcentratorMgr.Handler发生异常,堆栈信息:%s", string(debug.Stack()))
@@ -72,7 +73,7 @@ func (o *HlZigbeeConcentratorMgr) Handler(args ...interface{}) interface{} {
 			continue
 		}
 
-		Tenant, t, err := ParseTopicHL(m.Topic())
+		Tenant, t, err := ParseTopicHL(m.Topic(), mainConfig.HLDefaultTenant)
 		if err != nil {
 			continue
 		}

+ 1 - 0
cloud/ipolesvr/ipcmgr.go

@@ -49,6 +49,7 @@ func (o *IPCMgr) SubscribeTopics() {
 func (o *IPCMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("IPCMgr.Handler发生异常:%v", err)
 			logrus.Errorf("IPCMgr.Handler发生异常:%s", string(debug.Stack()))

+ 1 - 0
cloud/ipolesvr/itsmgr.go

@@ -124,6 +124,7 @@ func (o *ItsMgr) HandlerTpItsVehicleStatic(m *mqtt.Message) {
 func (o *ItsMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("ItsMgr.Handler发生异常:%v", err)
 			logrus.Errorf("ItsMgr.Handler发生异常:%s", string(debug.Stack()))

+ 1 - 0
cloud/ipolesvr/lampstrategy.go

@@ -41,6 +41,7 @@ type LampStrategyMgr struct {
 func (o *LampStrategyMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("LampStrategyMgr.Handler发生异常:%s", string(debug.Stack()))
 		}

+ 15 - 3
cloud/ipolesvr/main.go

@@ -40,14 +40,26 @@ func DestroyMysql() {
 }
 
 func ReadConfig() bool {
-	dir, _ := os.Getwd() //filepath.Abs(filepath.Dir(os.Args[0]))
+	dir, _ := os.Getwd()
 	fPath := dir + string(filepath.Separator) + "conf" + string(filepath.Separator)
 	errA := RedisConfig.ReadConfig(fPath, "redis")
 	errB := MySQLConfig.ReadConfig(fPath, "mysql")
 	errC := MQTTConfig.ReadConfig(fPath, "broker")
 	errD := mainConfig.ReadConfig(fPath, "ipolesvr")
 	_ = MqttconfigHl.ReadConfig(fPath, "broker_highland")
-	return errA && errB && errC && errD //&& err_e
+	if !errA {
+		logrus.Error("读取redis配置失败")
+	}
+	if !errB {
+		logrus.Error("读取mysql配置失败")
+	}
+	if !errC {
+		logrus.Error("读取mqtt broker配置失败")
+	}
+	if !errD {
+		logrus.Error("读取ipolesvr配置失败")
+	}
+	return errA && errB && errC && errD
 }
 
 func main() {
@@ -67,7 +79,7 @@ func main() {
 	}()
 
 	if !ReadConfig() {
-		panic(nil)
+		logrus.Fatal("读取配置文件失败,服务退出")
 	}
 	GetMQTTMgr()
 	GetHlMqttMgr() //hl

+ 1 - 0
cloud/ipolesvr/mbdevhandler.go

@@ -62,6 +62,7 @@ func (o *ModbusDeviceHandler) HandlerData(m mqtt.Message) {
 func (o *ModbusDeviceHandler) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("ModbusDeviceHandler.Handler发生异常:%s", string(debug.Stack()))
 		}

+ 0 - 128
cloud/ipolesvr/mqttclient.go

@@ -1,128 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-)
-
-type BaseMqttOnline interface {
-	GetOnlineMsg() (string, string)
-	GetWillMsg() (string, string)
-}
-
-type EmptyMqttOnline struct {
-}
-
-func (o *EmptyMqttOnline) GetOnlineMsg() (string, string) {
-	return "", ""
-}
-func (o *EmptyMqttOnline) GetWillMsg() (string, string) {
-	return "", ""
-}
-
-type MqttClient struct {
-	mqtt       *mqtt.Client        //
-	mu         sync.Mutex          //保护mapTopics
-	mapTopics  map[string]mqtt.QOS //订阅的主题
-	timeout    uint                //超时时间,毫秒为单位
-	MqttOnline BaseMqttOnline      //是否发布上线消息&遗嘱消息
-}
-
-func NewMqttClient(server, clientId, user, password string, timeout uint, mqttOnline BaseMqttOnline) *MqttClient {
-	o := MqttClient{
-		mapTopics:  make(map[string]mqtt.QOS),
-		timeout:    timeout,
-		MqttOnline: mqttOnline,
-	}
-	client, err := mqtt.NewClient(mqtt.ClientOptions{
-		Servers:       []string{server},
-		ClientID:      clientId,
-		Username:      user,
-		Password:      password,
-		AutoReconnect: true,
-	}, &o)
-	if err != nil {
-		panic(fmt.Sprintf("MQTT错误:", err.Error()))
-		return nil
-	}
-	o.mqtt = client
-	err = client.Connect(o.Ctx())
-	return &o
-}
-
-func (o *MqttClient) ConnectionLostHandler(err error) {
-	logrus.Errorln("MqttClient.ConnectionLostHandler:MQTT连接已经断开,原因:", err)
-}
-
-func (o *MqttClient) OnConnectHandler() {
-	logrus.Infoln("MqttClient.OnConnectHandler:MQTT连接成功")
-	//连接成功则订阅主题
-	for k, v := range o.mapTopics {
-		err := o.Subscribe(k, v)
-		if err != nil {
-			return
-		}
-	}
-	topic, str := o.MqttOnline.GetOnlineMsg()
-	if topic != "" {
-		err := o.PublishString(topic, str, 0)
-		if err != nil {
-			return
-		}
-	}
-}
-
-func (o *MqttClient) GetWill() (topic string, payload string) {
-	return o.MqttOnline.GetWillMsg()
-}
-
-func (o *MqttClient) Connect() error {
-	return o.mqtt.Connect(o.Ctx())
-}
-
-func (o *MqttClient) IsConnected() bool {
-	return o.mqtt.IsConnected()
-}
-
-func (o *MqttClient) Publish(topic string, payload []byte, qos mqtt.QOS) error {
-	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishString(topic string, payload string, qos mqtt.QOS) error {
-	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishJSON(topic string, payload interface{}, qos mqtt.QOS) error {
-	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
-}
-
-func (o *MqttClient) Subscribe(topic string, qos mqtt.QOS) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; !ok {
-		o.mapTopics[topic] = qos
-	}
-	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
-}
-
-func (o *MqttClient) Unsubscribe(topic string) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; ok {
-		delete(o.mapTopics, topic)
-	}
-	return o.mqtt.Unsubscribe(o.Ctx(), topic)
-}
-
-func (o *MqttClient) Handle(topic string, handler mqtt.MessageHandler) mqtt.Route {
-	return o.mqtt.Handle(topic, handler)
-}
-
-func (o *MqttClient) Ctx() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.timeout))
-	return ctx
-}

+ 3 - 4
cloud/ipolesvr/mqttmgr.go

@@ -17,18 +17,17 @@ func GetMQTTMgr() *MQTTMgr {
 }
 
 type MQTTMgr struct {
-	Cloud *MqttClient
+	Cloud *mqtt.MqttClient
 }
 
 func _newMQTTMgr() *MQTTMgr {
 	return &MQTTMgr{
-		Cloud: NewMqttClient(MQTTConfig.Mqtt_Server,
+		Cloud: mqtt.NewMqttClient(MQTTConfig.Mqtt_Server,
 			MQTTConfig.Mqtt_Id,
 			MQTTConfig.Mqtt_User,
 			MQTTConfig.Mqtt_Password,
-			3000, &EmptyMqttOnline{}),
+			3000, &mqtt.EmptyMqttOnline{}),
 	}
-
 }
 
 func (o *MQTTMgr) Subscribe(topic string, qos mqtt.QOS, handler mqtt.MessageHandler) {

+ 3 - 4
cloud/ipolesvr/mqttmgr_hl.go

@@ -18,18 +18,17 @@ func GetHlMqttMgr() *HlMqttMgr {
 }
 
 type HlMqttMgr struct {
-	Cloud *MqttClient
+	Cloud *mqtt.MqttClient
 }
 
 func _newHlMqttMgr() *HlMqttMgr {
 	return &HlMqttMgr{
-		Cloud: NewMqttClient(MqttconfigHl.Mqtt_Server,
+		Cloud: mqtt.NewMqttClient(MqttconfigHl.Mqtt_Server,
 			MqttconfigHl.Mqtt_Id,
 			MqttconfigHl.Mqtt_User,
 			MqttconfigHl.Mqtt_Password,
-			3000, &EmptyMqttOnline{}),
+			3000, &mqtt.EmptyMqttOnline{}),
 	}
-
 }
 
 func (o *HlMqttMgr) Subscribe(topic string, qos mqtt.QOS, handler mqtt.MessageHandler) {

+ 1 - 0
cloud/ipolesvr/radarhandler.go

@@ -50,6 +50,7 @@ func (o *radarHandler) HandlerData(m mqtt.Message) {
 func (o *radarHandler) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("radarHandler.Handler:%v发生异常:%s", args, string(debug.Stack()))
 		}

+ 1 - 0
cloud/ipolesvr/sosmgr.go

@@ -43,6 +43,7 @@ func (o *SosMgr) SubscribeTopics() {
 func (o *SosMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("SosMgr.Handler发生异常:%s", string(debug.Stack()))
 		}

+ 4 - 2
cloud/ipolesvr/srsmgr.go

@@ -1,7 +1,7 @@
 package main
 
 import (
-	"io/ioutil"
+	"io"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -128,6 +128,7 @@ func (o *SrsMgr) QueryChannel(client *resty.Client) *QueryChannel {
 func (o *SrsMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("GB28181Mgr.Handler发生异常:%s", string(debug.Stack()))
 		}
@@ -168,6 +169,7 @@ func (o *SrsMgr) Handler(args ...interface{}) interface{} {
 func (o *SrsMgr) StartWeb(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.StartWeb, args)
 			logrus.Errorf("SrsMgr.StartWeb发生异常:%v", err)
 			logrus.Errorf("SrsMgr.StartWeb发生异常,堆栈信息:%s", string(debug.Stack()))
@@ -190,7 +192,7 @@ type H map[string]interface{}
 
 func (o *SrsMgr) HookHandler(c echo.Context) error {
 	if c.Request().Body != nil {
-		buf, err := ioutil.ReadAll(c.Request().Body)
+		buf, err := io.ReadAll(c.Request().Body)
 		var obj CallbackAPIObject
 		err = json.Unmarshal(buf, &obj)
 		if err != nil {

+ 11 - 4
cloud/ipolesvr/ymlampcontrollermgr.go

@@ -41,10 +41,10 @@ type YMLampControllerMgr struct {
 }
 
 func (o *YMLampControllerMgr) SubscribeTopics() {
-	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_LAMPCONTROLLER, protocol.TP_YM_DATA), mqtt.AtMostOnce, o.HandlerData)
-	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_LAMPCONTROLLER, protocol.TP_YM_ALARM), mqtt.AtMostOnce, o.HandlerData)
-	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_LAMPCONTROLLER, protocol.TP_YM_SET_SWITCH_ACK), mqtt.AtMostOnce, o.HandlerData)
-	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_LAMPCONTROLLER, protocol.TP_YM_SET_ONOFFTIME_ACK), mqtt.AtMostOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_LAMPCONTROLLER, protocol.TP_YM_DATA), mqtt.AtLeastOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_LAMPCONTROLLER, protocol.TP_YM_ALARM), mqtt.AtLeastOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_LAMPCONTROLLER, protocol.TP_YM_SET_SWITCH_ACK), mqtt.AtLeastOnce, o.HandlerData)
+	GetMQTTMgr().Subscribe(GetVagueTopic(protocol.DT_LAMPCONTROLLER, protocol.TP_YM_SET_ONOFFTIME_ACK), mqtt.AtLeastOnce, o.HandlerData)
 }
 
 func (o *YMLampControllerMgr) HandlerData(m mqtt.Message) {
@@ -64,6 +64,7 @@ func (o *YMLampControllerMgr) Stop() {
 func (o *YMLampControllerMgr) Handler(args ...interface{}) interface{} {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(time.Second)
 			gopool.Add(o.Handler, args)
 			logrus.Errorf("YMLampControllerMgr.Handler发生异常:%v", err)
 			logrus.Errorf("YMLampControllerMgr.Handler发生异常,堆栈信息:%s", string(debug.Stack()))
@@ -113,6 +114,7 @@ func (o *YMLampControllerMgr) handleQueue() uint32 {
 
 	Tenant, _, DID, topic, err := ParseTopic(m.Topic())
 	if err != nil {
+		logrus.Errorf("YMLampControllerMgr.handleQueue:ParseTopic失败,topic=%s,err=%v", m.Topic(), err)
 		return quantity
 	}
 	pymlc, ok := o.mapYMLampController[DID]
@@ -128,6 +130,8 @@ func (o *YMLampControllerMgr) handleQueue() uint32 {
 		o.handleALARM(pymlc, m)
 	case protocol.TP_YM_SET_SWITCH_ACK, protocol.TP_YM_SET_ONOFFTIME_ACK:
 		o.handleACK(m)
+	default:
+		logrus.Errorf("YMLampControllerMgr.handleQueue:未知topic=%s,全topic=%s", topic, m.Topic())
 	}
 	return quantity
 }
@@ -135,6 +139,7 @@ func (o *YMLampControllerMgr) handleQueue() uint32 {
 func (o *YMLampControllerMgr) handleDATA(lp *YMLampController, m *mqtt.Message) {
 	var obj protocol.Pack_CHZB_UploadData
 	if err := obj.DeCode(m.PayloadString()); err != nil {
+		logrus.Errorf("YMLampControllerMgr.handleDATA:DeCode失败,DID=%s,topic=%s,err=%v", lp.did, m.Topic(), err)
 		return
 	}
 	t, err := util.MlParseTime(obj.Time)
@@ -150,6 +155,7 @@ func (o *YMLampControllerMgr) handleDATA(lp *YMLampController, m *mqtt.Message)
 func (o *YMLampControllerMgr) handleALARM(lp *YMLampController, m *mqtt.Message) {
 	var obj protocol.Pack_CHZB_LampAlarm
 	if err := obj.DeCode(m.PayloadString()); err != nil {
+		logrus.Errorf("YMLampControllerMgr.handleALARM:DeCode失败,DID=%s,topic=%s,err=%v", lp.did, m.Topic(), err)
 		return
 	}
 	lp.HandleAlarm(obj.Data)
@@ -158,6 +164,7 @@ func (o *YMLampControllerMgr) handleALARM(lp *YMLampController, m *mqtt.Message)
 func (o *YMLampControllerMgr) handleACK(m *mqtt.Message) {
 	var obj protocol.Pack_Ack
 	if err := obj.DeCode(m.PayloadString()); err != nil {
+		logrus.Errorf("YMLampControllerMgr.handleACK:DeCode失败,topic=%s,err=%v", m.Topic(), err)
 		return
 	}
 	oo := models.DeviceCmdRecord{ID: obj.Seq, State: 1, Resp: obj.Data.Error}

+ 1 - 2
cloud/mediasvr/main.go

@@ -3,7 +3,6 @@ package main
 import (
 	"encoding/base64"
 	"encoding/json"
-	"io/ioutil"
 	"net/http"
 	"os"
 	"path/filepath"
@@ -52,7 +51,7 @@ func getFile(streams, date string) []string {
 	var fileList []string
 	for _, dir := range config.DvrDir {
 		path := string(filepath.Separator) + dir + string(filepath.Separator) + streams + string(filepath.Separator) + date
-		if rd, err := ioutil.ReadDir(config.Rootdir + path); err == nil {
+		if rd, err := os.ReadDir(config.Rootdir + path); err == nil {
 			for _, fi := range rd {
 				//过滤掉路径和非mp4文件
 				if !fi.IsDir() && strings.HasSuffix(fi.Name(), ".mp4") {

+ 0 - 128
cloud/mqttforward/mqttclient.go

@@ -1,128 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"sync"
-	"time"
-
-	"lc/common/mqtt"
-
-	"github.com/sirupsen/logrus"
-)
-
-type BaseMqttOnline interface {
-	GetOnlineMsg() (string, string)
-	GetWillMsg() (string, string)
-}
-
-type EmptyMqttOnline struct {
-}
-
-func (o *EmptyMqttOnline) GetOnlineMsg() (string, string) {
-	return "", ""
-}
-func (o *EmptyMqttOnline) GetWillMsg() (string, string) {
-	return "", ""
-}
-
-type MqttClient struct {
-	mqtt       *mqtt.Client        //
-	mu         sync.Mutex          //保护mapTopics
-	mapTopics  map[string]mqtt.QOS //订阅的主题
-	timeout    uint                //超时时间,毫秒为单位
-	MqttOnline BaseMqttOnline      //是否发布上线消息&遗嘱消息
-}
-
-func NewMqttClient(server, clientid, user, password string, timeout uint, mqttOnline BaseMqttOnline) *MqttClient {
-	o := MqttClient{
-		mapTopics:  make(map[string]mqtt.QOS),
-		timeout:    timeout,
-		MqttOnline: mqttOnline,
-	}
-	client, err := mqtt.NewClient(mqtt.ClientOptions{
-		Servers:       []string{server},
-		ClientID:      clientid,
-		Username:      user,
-		Password:      password,
-		AutoReconnect: true,
-	}, &o)
-	if err != nil {
-		panic(fmt.Sprintf("MQTT错误:", err.Error()))
-		return nil
-	}
-	o.mqtt = client
-	err = client.Connect(o.Ctx())
-	return &o
-}
-
-func (o *MqttClient) ConnectionLostHandler(err error) {
-	logrus.Errorln("MqttClient.ConnectionLostHandler:MQTT连接已经断开,原因:", err)
-}
-
-func (o *MqttClient) OnConnectHandler() {
-	logrus.Infoln("MqttClient.OnConnectHandler:MQTT连接成功")
-	//连接成功则订阅主题
-	for k, v := range o.mapTopics {
-		err := o.Subscribe(k, v)
-		if err != nil {
-			return
-		}
-	}
-	topic, str := o.MqttOnline.GetOnlineMsg()
-	if topic != "" {
-		err := o.PublishString(topic, str, 0)
-		if err != nil {
-			return
-		}
-	}
-}
-
-func (o *MqttClient) GetWill() (topic string, payload string) {
-	return o.MqttOnline.GetWillMsg()
-}
-
-func (o *MqttClient) Connect() error {
-	return o.mqtt.Connect(o.Ctx())
-}
-
-func (o *MqttClient) IsConnected() bool {
-	return o.mqtt.IsConnected()
-}
-
-func (o *MqttClient) Publish(topic string, payload []byte, qos mqtt.QOS) error {
-	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishString(topic string, payload string, qos mqtt.QOS) error {
-	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishJSON(topic string, payload interface{}, qos mqtt.QOS) error {
-	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
-}
-
-func (o *MqttClient) Subscribe(topic string, qos mqtt.QOS) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; !ok {
-		o.mapTopics[topic] = qos
-	}
-	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
-}
-
-func (o *MqttClient) Unsubscribe(topic string) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; ok {
-		delete(o.mapTopics, topic)
-	}
-	return o.mqtt.Unsubscribe(o.Ctx(), topic)
-}
-
-func (o *MqttClient) Handle(topic string, handler mqtt.MessageHandler) mqtt.Route {
-	return o.mqtt.Handle(topic, handler)
-}
-
-func (o *MqttClient) Ctx() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.timeout))
-	return ctx
-}

+ 6 - 6
cloud/mqttforward/mqttmgr.go

@@ -16,22 +16,22 @@ func GetMQTTMgr() *MQTTMgr {
 }
 
 type MQTTMgr struct {
-	FromCloud *MqttClient
-	ToCloud   *MqttClient
+	FromCloud *mqtt.MqttClient
+	ToCloud   *mqtt.MqttClient
 }
 
 func _newMQTTMgr() *MQTTMgr {
 	return &MQTTMgr{
-		FromCloud: NewMqttClient(FromMQTTConfig.Mqtt_Server,
+		FromCloud: mqtt.NewMqttClient(FromMQTTConfig.Mqtt_Server,
 			FromMQTTConfig.Mqtt_Id,
 			FromMQTTConfig.Mqtt_User,
 			FromMQTTConfig.Mqtt_Password,
-			3000, &EmptyMqttOnline{}),
-		ToCloud: NewMqttClient(ToMQTTConfig.Mqtt_Server,
+			3000, &mqtt.EmptyMqttOnline{}),
+		ToCloud: mqtt.NewMqttClient(ToMQTTConfig.Mqtt_Server,
 			ToMQTTConfig.Mqtt_Id,
 			ToMQTTConfig.Mqtt_User,
 			ToMQTTConfig.Mqtt_Password,
-			3000, &EmptyMqttOnline{}),
+			3000, &mqtt.EmptyMqttOnline{}),
 	}
 }
 

+ 0 - 1
cloud/websvr/controllers/cbase.go

@@ -94,7 +94,6 @@ const (
 func Krand(size int, kind int) []byte {
 	ikind, kinds, result := kind, [][]int{[]int{10, 48}, []int{26, 97}, []int{26, 65}}, make([]byte, size)
 	isAll := kind > 2 || kind < 0
-	rand.Seed(time.Now().UnixNano())
 	for i := 0; i < size; i++ {
 		if isAll {
 			ikind = rand.Intn(3)

+ 9 - 9
cloud/websvr/controllers/ccamera.go

@@ -2,7 +2,7 @@ package controllers
 
 import (
 	"fmt"
-	"io/ioutil"
+	"io"
 	"os"
 	"strconv"
 	"strings"
@@ -437,7 +437,7 @@ func (o *CameraController) Snapshot() {
 	}
 	defer f.Close()
 	f.Seek(0, 0)
-	content, err := ioutil.ReadAll(f)
+	content, err := io.ReadAll(f)
 	if err != nil {
 		return
 	}
@@ -458,7 +458,7 @@ func (o *CameraController) Snapshot() {
 	filepath = filepath + strDate + string(os.PathSeparator)
 	os.MkdirAll(filepath, os.ModePerm)
 	filepath = filepath + fh.Filename
-	err = ioutil.WriteFile(filepath, content, os.ModePerm)
+	err = os.WriteFile(filepath, content, os.ModePerm)
 	if err != nil {
 		beego.Error("Snapshot存储抓图发生错误:", err)
 	}
@@ -474,14 +474,14 @@ func (o *CameraController) PresetPicture() {
 	}
 	defer f.Close()
 	f.Seek(0, 0)
-	content, err := ioutil.ReadAll(f)
+	content, err := io.ReadAll(f)
 	if err != nil {
 		return
 	}
 	filepath := "file" + string(os.PathSeparator) + "preset" + string(os.PathSeparator)
 	os.MkdirAll(filepath, os.ModePerm)
 	filepath = filepath + fh.Filename
-	err = ioutil.WriteFile(filepath, content, os.ModePerm)
+	err = os.WriteFile(filepath, content, os.ModePerm)
 	if err != nil {
 		beego.Error("Snapshot存储抓图发生错误:", err)
 	}
@@ -497,7 +497,7 @@ func (o *CameraController) FLV() {
 	}
 	defer f.Close()
 	f.Seek(0, 0)
-	content, err := ioutil.ReadAll(f)
+	content, err := io.ReadAll(f)
 	if err != nil {
 		return
 	}
@@ -518,7 +518,7 @@ func (o *CameraController) FLV() {
 	filepath = filepath + strDate + string(os.PathSeparator)
 	os.MkdirAll(filepath, os.ModePerm)
 	filepath = filepath + fh.Filename
-	err = ioutil.WriteFile(filepath, content, os.ModePerm)
+	err = os.WriteFile(filepath, content, os.ModePerm)
 	if err != nil {
 		beego.Error("MP4存储发生错误:", err)
 	}
@@ -541,7 +541,7 @@ func (o *CameraController) GetSnapshot() {
 	//dir := "file/snapshot/" + util.MlNow().Format("2006-01-02") + "/"
 	filepath := "file" + string(os.PathSeparator) + "snapshot" + string(os.PathSeparator) +
 		code + string(os.PathSeparator) + date + string(os.PathSeparator)
-	fis, err := ioutil.ReadDir(filepath)
+	fis, err := os.ReadDir(filepath)
 	if err != nil {
 		beego.Error(fmt.Sprintf("未找到该路径[%s],请先抓图", err.Error()))
 		o.Response(Failure, "未找到该路径,请先抓图", nil)
@@ -576,7 +576,7 @@ func (o *CameraController) GetFLV() {
 	//dir := "file/snapshot/" + util.MlNow().Format("2006-01-02") + "/"
 	filepath := "file" + string(os.PathSeparator) + "flv" + string(os.PathSeparator) +
 		code + string(os.PathSeparator) + date + string(os.PathSeparator)
-	fis, err := ioutil.ReadDir(filepath)
+	fis, err := os.ReadDir(filepath)
 	if err != nil {
 		beego.Error(fmt.Sprintf("未找到该路径[%s],请先录制mp4", err.Error()))
 		o.Response(Failure, "未找到该路径,请先录制mp4", nil)

+ 862 - 0
cloud/websvr/controllers/cgateway.go

@@ -1,12 +1,21 @@
 package controllers
 
 import (
+	"crypto/md5"
+	"encoding/base64"
 	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strconv"
 	"strings"
+	"time"
 
 	"github.com/astaxie/beego"
 
 	"lc/common/models"
+	"lc/common/mqtt"
+	"lc/common/protocol"
 )
 
 type GatewayController struct {
@@ -151,3 +160,856 @@ func (o *GatewayController) ImportGateway() {
 	}
 	o.Response(Success, "成功", resp)
 }
+
+// PushSerialConfig @Title 下发串口配置到网关
+// @Description 下发 serial.json 配置到指定网关
+// @Param   body controllers.ReqGatewayConfig true "数据"
+// @Success 0 {int} BaseResponse.Code "成功"
+// @Failure 1 {int} BaseResponse.Code "失败"
+// @router /v1/serial/push [post]
+func (o *GatewayController) PushSerialConfig() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	if gid == "" || tenant == "" {
+		o.Response(Failure, "gid和tenant不能为空", nil)
+		return
+	}
+
+	body := o.Ctx.Input.RequestBody
+	if len(body) == 0 {
+		o.Response(Failure, "请求body不能为空,请在表单中导入或添加串口配置后再下发", nil)
+		return
+	}
+	sc := protocol.SerialConfig{Serial: make(map[uint8]*protocol.SerialPort)}
+	if err := json.Unmarshal(body, &sc); err != nil || len(sc.Serial) == 0 {
+		o.Response(Failure, "串口配置JSON解析失败或为空", nil)
+		return
+	}
+	content := string(body)
+
+	seq := GetNextUint64()
+	var obj protocol.Pack_SeqFileObject
+	obj.Data.File = "serial.json"
+	obj.Data.Content = content
+	str, err := obj.EnCode(gid, seq)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("编码失败:%s", err.Error()), nil)
+		return
+	}
+
+	topic := GetTopic(tenant, protocol.DT_GATEWAY, gid, protocol.TP_GW_SET_SERIAL)
+	if err := GetMqttHandler().PublishString(topic, str, mqtt.AtLeastOnce); err != nil {
+		o.Response(Failure, fmt.Sprintf("MQTT发布失败:%s", err.Error()), nil)
+		return
+	}
+
+	// 下发成功后,将配置存档到DB
+	pushedCodes := make(map[int]bool)
+	for _, v := range sc.Serial {
+		models.G_db.Save(&models.GatewaySerial{
+			ID: gid, ComID: int(v.Code),
+			Interface: v.Interface, Address: v.Address,
+			BaudRate: v.BaudRate, DataBits: int(v.DataBits),
+			StopBits: int(v.StopBits), Parity: v.Parity,
+			Timeout: int(v.Timeout), ProtocolType: int(v.ProtocolType),
+		})
+		pushedCodes[int(v.Code)] = true
+	}
+	// 标记不在此次推送中的旧串口为删除
+	var oldSerials []models.GatewaySerial
+	models.G_db.Where("id = ?", gid).Find(&oldSerials)
+	for _, s := range oldSerials {
+		if !pushedCodes[s.ComID] {
+			models.G_db.Delete(&s)
+		}
+	}
+
+	// 记录指令
+	dcr := models.DeviceCmdRecord{ID: seq, GID: gid, DID: gid, Topic: topic, Message: str, State: 0}
+	if err := models.G_db.Create(&dcr).Error; err != nil {
+		beego.Error("PushSerialConfig:指令入库失败:", err.Error())
+	}
+
+	o.Response(Success, "串口配置已下发并保存", nil)
+}
+
+// PushDevConfig @Title 下发设备配置到网关
+// @Description 下发 dev/{code}.json 配置到指定网关
+// @Param   body controllers.ReqDevConfig true "数据"
+// @Success 0 {int} BaseResponse.Code "成功"
+// @Failure 1 {int} BaseResponse.Code "失败"
+// @router /v1/dev/push [post]
+func (o *GatewayController) PushDevConfig() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	codeStr := strings.Trim(o.GetString("code"), " ")
+	if gid == "" || tenant == "" || codeStr == "" {
+		o.Response(Failure, "gid、tenant、code不能为空", nil)
+		return
+	}
+	code, err := strconv.Atoi(codeStr)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("code格式错误:%s", err.Error()), nil)
+		return
+	}
+
+	var mdc protocol.MapDevConfig
+	// 优先从请求body解析(前端表单直接提交的配置)
+	body := o.Ctx.Input.RequestBody
+	if len(body) == 0 {
+		o.Response(Failure, "请求body不能为空,请在表单中导入或添加设备配置后再下发", nil)
+		return
+	}
+	if err := json.Unmarshal(body, &mdc); err != nil || len(mdc.Rtu) == 0 {
+		o.Response(Failure, "设备配置JSON解析失败或为空", nil)
+		return
+	}
+	content := string(body)
+
+	seq := GetNextUint64()
+	var obj protocol.Pack_SeqFileObject
+	obj.Data.File = codeStr + ".json"
+	obj.Data.Content = content
+	str, err := obj.EnCode(gid, seq)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("编码失败:%s", err.Error()), nil)
+		return
+	}
+
+	topic := GetTopic(tenant, protocol.DT_GATEWAY, gid, protocol.TP_GW_SET_RTU)
+	if err := GetMqttHandler().PublishString(topic, str, mqtt.AtLeastOnce); err != nil {
+		o.Response(Failure, fmt.Sprintf("MQTT发布失败:%s", err.Error()), nil)
+		return
+	}
+
+	// 下发成功后,将配置存档到DB
+	pushedDevCodes := make(map[string]bool)
+	for _, v := range mdc.Rtu {
+		models.G_db.Save(&models.GatewayDevice{
+			ID: v.DevCode, Name: v.Name, GID: gid,
+			ComID: int(v.Code), RtuID: int(v.DevID), TID: int(v.TID),
+			SendCloud: v.SendCloud, WaitTime: int(v.WaitTime),
+			ProtocolType: int(v.ProtocolType), DevType: int(v.DevType),
+			Tenant: tenant, State: 1,
+		})
+		pushedDevCodes[v.DevCode] = true
+	}
+	// 标记不在此次推送中且同串口下的旧设备为删除
+	var oldDevices []models.GatewayDevice
+	models.G_db.Where("g_id = ? AND com_id = ? AND state = 1", gid, code).Find(&oldDevices)
+	for _, d := range oldDevices {
+		if !pushedDevCodes[d.ID] {
+			models.G_db.Model(&d).Update("state", 0)
+		}
+	}
+
+	// 记录指令
+	dcr := models.DeviceCmdRecord{ID: seq, GID: gid, DID: gid, Topic: topic, Message: str, State: 0}
+	if err := models.G_db.Create(&dcr).Error; err != nil {
+		beego.Error("PushDevConfig:指令入库失败:", err.Error())
+	}
+
+	o.Response(Success, "设备配置已下发并保存", nil)
+}
+
+// PushModelConfig @Title 下发物模型配置到网关
+// @Description 下发 model/{tid}.json 配置到指定网关
+// @Param   body controllers.ReqModelConfig true "数据"
+// @Success 0 {int} BaseResponse.Code "成功"
+// @Failure 1 {int} BaseResponse.Code "失败"
+// @router /v1/model/push [post]
+func (o *GatewayController) PushModelConfig() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	tidStr := strings.Trim(o.GetString("tid"), " ")
+	if gid == "" || tenant == "" || tidStr == "" {
+		o.Response(Failure, "gid、tenant、tid不能为空", nil)
+		return
+	}
+	tid, err := strconv.Atoi(tidStr)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("tid格式错误:%s", err.Error()), nil)
+		return
+	}
+
+	// 必须从请求body取模型JSON(不下发时以表单数据为准)
+	body := o.Ctx.Input.RequestBody
+	if len(body) == 0 {
+		o.Response(Failure, "请求body不能为空,请在表单中选择JSON文件后再下发", nil)
+		return
+	}
+	fileContent := string(body)
+	var iot protocol.IotModel
+	if err := json.Unmarshal(body, &iot); err != nil {
+		o.Response(Failure, fmt.Sprintf("物模型JSON解析失败:%s", err.Error()), nil)
+		return
+	}
+	device := iot.Device
+	modelName := iot.Model
+	protocolName := iot.Protocol
+
+	seq := GetNextUint64()
+	var obj protocol.Pack_SeqFileObject
+	obj.Data.File = tidStr + ".json"
+	obj.Data.Content = fileContent
+	str, err := obj.EnCode(gid, seq)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("编码失败:%s", err.Error()), nil)
+		return
+	}
+
+	topic := GetTopic(tenant, protocol.DT_GATEWAY, gid, protocol.TP_GW_SET_MODEL)
+	if err := GetMqttHandler().PublishString(topic, str, mqtt.AtLeastOnce); err != nil {
+		o.Response(Failure, fmt.Sprintf("MQTT发布失败:%s", err.Error()), nil)
+		return
+	}
+
+	// 记录指令
+	dcr := models.DeviceCmdRecord{ID: seq, GID: gid, DID: gid, Topic: topic, Message: str, State: 0}
+	if err := models.G_db.Create(&dcr).Error; err != nil {
+		beego.Error("PushModelConfig:指令入库失败:", err.Error())
+	}
+
+	// 下发成功后存档到 t_gateway_model
+	models.G_db.Save(&models.GatewayModel{
+		GID: gid, TID: uint16(tid),
+		Device: device, Model: modelName, Protocol: protocolName,
+		File: fileContent, PushedAt: time.Now(),
+	})
+
+	o.Response(Success, "物模型配置已下发并保存", nil)
+}
+
+// SerialConfigList @Title 查询网关串口列表
+// @Description 查询指定网关的所有串口配置
+// @Param   gid query string true "网关ID"
+// @Success 0 {int} BaseResponse.Code "成功"
+// @router /v1/serial/list [get]
+func (o *GatewayController) SerialConfigList() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	if gid == "" {
+		o.Response(Failure, "gid不能为空", nil)
+		return
+	}
+	var serials []models.GatewaySerial
+	if err := models.G_db.Where("id = ?", gid).Find(&serials).Error; err != nil {
+		o.Response(Failure, fmt.Sprintf("查询失败:%s", err.Error()), nil)
+		return
+	}
+	o.Response(Success, "成功", serials)
+}
+
+// SerialConfigSave @Title 保存串口配置
+// @Description 新增或更新单条串口配置
+// @Param   gid query string true "网关ID"
+// @Param   comid query int true "串口编号"
+// @Success 0 {int} BaseResponse.Code "成功"
+// @router /v1/serial/save [post]
+func (o *GatewayController) SerialConfigSave() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	comIDStr := strings.Trim(o.GetString("comid"), " ")
+	if gid == "" || comIDStr == "" {
+		o.Response(Failure, "gid和comid不能为空", nil)
+		return
+	}
+	comID, err := strconv.Atoi(comIDStr)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("comid格式错误:%s", err.Error()), nil)
+		return
+	}
+	baudRate, _ := strconv.Atoi(strings.Trim(o.GetString("baudrate"), " "))
+	dataBits, _ := strconv.Atoi(strings.Trim(o.GetString("databits"), " "))
+	stopBits, _ := strconv.Atoi(strings.Trim(o.GetString("stopbits"), " "))
+	timeout, _ := strconv.Atoi(strings.Trim(o.GetString("timeout"), " "))
+	protocolType, _ := strconv.Atoi(strings.Trim(o.GetString("protocoltype"), " "))
+
+	s := models.GatewaySerial{
+		ID:           gid,
+		ComID:        comID,
+		Interface:    strings.Trim(o.GetString("interface"), " "),
+		Address:      strings.Trim(o.GetString("address"), " "),
+		BaudRate:     baudRate,
+		DataBits:     dataBits,
+		StopBits:     stopBits,
+		Parity:       strings.Trim(o.GetString("parity"), " "),
+		Timeout:      timeout,
+		ProtocolType: protocolType,
+	}
+	if err := models.G_db.Save(&s).Error; err != nil {
+		o.Response(Failure, fmt.Sprintf("保存失败:%s", err.Error()), nil)
+		return
+	}
+	o.Response(Success, "保存成功", nil)
+}
+
+// SerialConfigDelete @Title 删除串口配置
+// @Description 删除单条串口配置
+// @Param   gid query string true "网关ID"
+// @Param   comid query int true "串口编号"
+// @router /v1/serial/delete [post]
+func (o *GatewayController) SerialConfigDelete() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	comIDStr := strings.Trim(o.GetString("comid"), " ")
+	if gid == "" || comIDStr == "" {
+		o.Response(Failure, "gid和comid不能为空", nil)
+		return
+	}
+	comID, err := strconv.Atoi(comIDStr)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("comid格式错误:%s", err.Error()), nil)
+		return
+	}
+	if err := models.G_db.Where("id = ? AND com_id = ?", gid, comID).Delete(&models.GatewaySerial{}).Error; err != nil {
+		o.Response(Failure, fmt.Sprintf("删除失败:%s", err.Error()), nil)
+		return
+	}
+	o.Response(Success, "删除成功", nil)
+}
+
+// DevConfigList @Title 查询网关设备列表
+// @Description 查询指定网关某串口下的设备配置
+// @Param   gid query string true "网关ID"
+// @Param   code query int false "串口编号,0查全部"
+// @router /v1/dev/list [get]
+func (o *GatewayController) DevConfigList() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	if gid == "" {
+		o.Response(Failure, "gid不能为空", nil)
+		return
+	}
+	codeStr := strings.Trim(o.GetString("code"), " ")
+	var devices []models.GatewayDevice
+	db := models.G_db.Where("g_id = ? AND state = 1", gid)
+	if codeStr != "" {
+		code, _ := strconv.Atoi(codeStr)
+		db = db.Where("com_id = ?", code)
+	}
+	if err := db.Find(&devices).Error; err != nil {
+		o.Response(Failure, fmt.Sprintf("查询失败:%s", err.Error()), nil)
+		return
+	}
+	o.Response(Success, "成功", devices)
+}
+
+// DevConfigSave @Title 保存设备配置
+// @Description 新增或更新单条设备配置
+// @router /v1/dev/save [post]
+func (o *GatewayController) DevConfigSave() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	devCode := strings.Trim(o.GetString("devcode"), " ")
+	if gid == "" || devCode == "" {
+		o.Response(Failure, "gid和devcode不能为空", nil)
+		return
+	}
+	comID, _ := strconv.Atoi(strings.Trim(o.GetString("comid"), " "))
+	rtuID, _ := strconv.Atoi(strings.Trim(o.GetString("rtuid"), " "))
+	tid, _ := strconv.Atoi(strings.Trim(o.GetString("tid"), " "))
+	protocolType, _ := strconv.Atoi(strings.Trim(o.GetString("protocoltype"), " "))
+	devType, _ := strconv.Atoi(strings.Trim(o.GetString("devtype"), " "))
+	sendCloud, _ := strconv.Atoi(strings.Trim(o.GetString("sendcloud"), " "))
+	waitTime, _ := strconv.Atoi(strings.Trim(o.GetString("waittime"), " "))
+
+	d := models.GatewayDevice{
+		ID:           devCode,
+		Name:         strings.Trim(o.GetString("name"), " "),
+		GID:          gid,
+		ComID:        comID,
+		RtuID:        rtuID,
+		TID:          tid,
+		SendCloud:    sendCloud,
+		WaitTime:     waitTime,
+		ProtocolType: protocolType,
+		DevType:      devType,
+		Tenant:       tenant,
+		State:        1,
+	}
+	if err := models.G_db.Save(&d).Error; err != nil {
+		o.Response(Failure, fmt.Sprintf("保存失败:%s", err.Error()), nil)
+		return
+	}
+	o.Response(Success, "保存成功", nil)
+}
+
+// DevConfigDelete @Title 删除设备配置
+// @Description 删除单条设备配置(软删除)
+// @Param   devcode query string true "设备编码"
+// @router /v1/dev/delete [post]
+func (o *GatewayController) DevConfigDelete() {
+	devCode := strings.Trim(o.GetString("devcode"), " ")
+	if devCode == "" {
+		o.Response(Failure, "devcode不能为空", nil)
+		return
+	}
+	if err := models.G_db.Model(&models.GatewayDevice{}).Where("id = ?", devCode).Update("state", 0).Error; err != nil {
+		o.Response(Failure, fmt.Sprintf("删除失败:%s", err.Error()), nil)
+		return
+	}
+	o.Response(Success, "删除成功", nil)
+}
+
+// ModelList @Title 查询物模型列表
+// @Description 查询网关已下发的物模型(默认查所有,传gid查指定网关)
+// @router /v1/model/list [get]
+func (o *GatewayController) ModelList() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	type ModelSummary struct {
+		TID      uint16 `json:"tid"`
+		Device   string `json:"device"`
+		Model    string `json:"model"`
+		Protocol string `json:"protocol"`
+		File     string `json:"file,omitempty"`
+	}
+	var list []ModelSummary
+	if gid != "" {
+		var gms []models.GatewayModel
+		if err := models.G_db.Where("g_id = ?", gid).Find(&gms).Error; err != nil {
+			o.Response(Failure, fmt.Sprintf("查询失败:%s", err.Error()), nil)
+			return
+		}
+		for _, v := range gms {
+			list = append(list, ModelSummary{TID: v.TID, Device: v.Device, Model: v.Model, Protocol: v.Protocol, File: v.File})
+		}
+	} else {
+		var gms []models.GatewayModel
+		if err := models.G_db.Find(&gms).Error; err != nil {
+			o.Response(Failure, fmt.Sprintf("查询失败:%s", err.Error()), nil)
+			return
+		}
+		for _, v := range gms {
+			list = append(list, ModelSummary{TID: v.TID, Device: v.Device, Model: v.Model, Protocol: v.Protocol, File: v.File})
+		}
+	}
+	o.Response(Success, "成功", list)
+}
+
+// ModelSave @Title 保存物模型
+// @Description 新增或更新物模型到 t_gateway_model
+// @router /v1/model/save [post]
+func (o *GatewayController) ModelSave() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tidStr := strings.Trim(o.GetString("tid"), " ")
+	if gid == "" || tidStr == "" {
+		o.Response(Failure, "gid和tid不能为空", nil)
+		return
+	}
+	tid, err := strconv.Atoi(tidStr)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("tid格式错误:%s", err.Error()), nil)
+		return
+	}
+	device := strings.Trim(o.GetString("device"), " ")
+	modelName := strings.Trim(o.GetString("model"), " ")
+	protocol := strings.Trim(o.GetString("protocol"), " ")
+	fileContent := strings.Trim(o.GetString("file"), " ")
+	if fileContent == "" {
+		// 尝试从上传的请求body中读取文件内容
+		fileContent = string(o.Ctx.Input.RequestBody)
+	}
+	m := models.GatewayModel{
+		GID: gid, TID: uint16(tid),
+		Device: device, Model: modelName, Protocol: protocol,
+		File: fileContent, PushedAt: time.Now(),
+	}
+	if err := models.G_db.Save(&m).Error; err != nil {
+		o.Response(Failure, fmt.Sprintf("保存失败:%s", err.Error()), nil)
+		return
+	}
+	o.Response(Success, "保存成功", nil)
+}
+
+// ModelDelete @Title 删除物模型
+// @Description 从 t_gateway_model 删除指定物模型
+// @router /v1/model/delete [post]
+func (o *GatewayController) ModelDelete() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tidStr := strings.Trim(o.GetString("tid"), " ")
+	if gid == "" || tidStr == "" {
+		o.Response(Failure, "gid和tid不能为空", nil)
+		return
+	}
+	tid, err := strconv.Atoi(tidStr)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("tid格式错误:%s", err.Error()), nil)
+		return
+	}
+	if err := models.G_db.Where("g_id = ? AND t_id = ?", gid, tid).Delete(&models.GatewayModel{}).Error; err != nil {
+		o.Response(Failure, fmt.Sprintf("删除失败:%s", err.Error()), nil)
+		return
+	}
+	o.Response(Success, "删除成功", nil)
+}
+
+// LogQuery @Title 查询边缘日志
+// @Description 触发边缘上传日志文件
+// @router /v1/log/query [post]
+func (o *GatewayController) LogQuery() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	if gid == "" || tenant == "" {
+		o.Response(Failure, "gid和tenant不能为空", nil)
+		return
+	}
+	seq := GetNextUint64()
+	var obj protocol.Pack_IDObject
+	str, err := obj.EnCode(gid, seq, 0)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("编码失败:%s", err.Error()), nil)
+		return
+	}
+	topic := GetTopic(tenant, protocol.DT_GATEWAY, gid, protocol.TP_GW_LOG)
+	beego.Info("LogQuery:发布,topic=", topic, ",payload=", str)
+	if err := GetMqttHandler().PublishString(topic, str, mqtt.AtLeastOnce); err != nil {
+		beego.Error("LogQuery:MQTT发布失败,topic=", topic, ",err=", err)
+		o.Response(Failure, fmt.Sprintf("MQTT发布失败:%s", err.Error()), nil)
+		return
+	}
+	beego.Info("LogQuery:发布成功,topic=", topic)
+	o.Response(Success, "日志查询指令已发送,请稍后刷新查看", map[string]interface{}{"seq": seq})
+}
+
+// LogList @Title 列出已保存的日志文件
+// @Description 列出网关已上传的日志文件
+// @router /v1/log/list [get]
+func (o *GatewayController) LogList() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	if gid == "" || tenant == "" {
+		o.Response(Failure, "gid和tenant不能为空", nil)
+		return
+	}
+	dir := "/opt/ipole_data/gateway_logs/" + tenant + "/" + gid + "/log/"
+	entries, err := os.ReadDir(dir)
+	if err != nil {
+		o.Response(Failure, "该网关暂无日志文件,请先点击「查询日志」", nil)
+		return
+	}
+	type LogFile struct {
+		Name string `json:"name"`
+		Size int64  `json:"size"`
+		Time string `json:"time"`
+	}
+	var files []LogFile
+	for _, e := range entries {
+		if !e.IsDir() {
+			info, _ := e.Info()
+			f := LogFile{Name: e.Name(), Size: info.Size(), Time: info.ModTime().Format("2006-01-02 15:04:05")}
+			files = append(files, f)
+		}
+	}
+	o.Response(Success, "成功", files)
+}
+
+// LogDelete @Title 删除边缘日志
+// @Description 远程删除边缘网关的所有日志文件
+// @Param   gid query string true "网关ID"
+// @Param   tenant query string true "租户ID"
+// @router /v1/log/delete [post]
+func (o *GatewayController) LogDelete() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	if gid == "" || tenant == "" {
+		o.Response(Failure, "gid和tenant不能为空", nil)
+		return
+	}
+	seq := GetNextUint64()
+	var obj protocol.Pack_IDObject
+	str, err := obj.EnCode(gid, seq, 0)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("编码失败:%s", err.Error()), nil)
+		return
+	}
+	topic := GetTopic(tenant, protocol.DT_GATEWAY, gid, protocol.TP_GW_REMOVE_LOG)
+	beego.Info("LogDelete:发布,topic=", topic)
+	if err := GetMqttHandler().PublishString(topic, str, mqtt.AtLeastOnce); err != nil {
+		beego.Error("LogDelete:MQTT发布失败,topic=", topic, ",err=", err)
+		o.Response(Failure, fmt.Sprintf("MQTT发布失败:%s", err.Error()), nil)
+		return
+	}
+	beego.Info("LogDelete:发布成功,topic=", topic)
+	o.Response(Success, "日志删除指令已发送", nil)
+}
+
+// LogCfg @Title 设置边缘日志等级
+// @Description 云端下发日志等级配置到边缘网关
+// @Param   gid query string true "网关ID"
+// @Param   tenant query string true "租户ID"
+// @Success 0 {int} BaseResponse.Code "成功"
+// @Failure 1 {int} BaseResponse.Code "失败"
+// @router /v1/log/cfg [post]
+func (o *GatewayController) LogCfg() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	if gid == "" || tenant == "" {
+		o.Response(Failure, "gid和tenant不能为空", nil)
+		return
+	}
+
+	body := o.Ctx.Input.RequestBody
+	if len(body) == 0 {
+		o.Response(Failure, "请求body为空,请提供日志等级配置", nil)
+		return
+	}
+
+	topic := GetTopic(tenant, protocol.DT_GATEWAY, gid, protocol.TP_GW_LOG_CFG)
+	beego.Info("LogCfg:发布,topic=", topic)
+	if err := GetMqttHandler().PublishString(topic, string(body), mqtt.AtLeastOnce); err != nil {
+		beego.Error("LogCfg:MQTT发布失败,topic=", topic, ",err=", err)
+		o.Response(Failure, fmt.Sprintf("MQTT发布失败:%s", err.Error()), nil)
+		return
+	}
+	beego.Info("LogCfg:发布成功,topic=", topic)
+	o.Response(Success, "日志等级配置已下发", nil)
+}
+
+// LogRead @Title 读取日志内容
+// @Description 读取指定日志文件内容
+// @router /v1/log/read [get]
+func (o *GatewayController) LogRead() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	fname := strings.Trim(o.GetString("file"), " ")
+	if gid == "" || tenant == "" || fname == "" {
+		o.Response(Failure, "gid、tenant、file不能为空", nil)
+		return
+	}
+	// 防止路径遍历
+	if strings.Contains(fname, "..") || strings.Contains(fname, "/") || strings.Contains(fname, "\\") {
+		o.Response(Failure, "非法文件名", nil)
+		return
+	}
+	path := "/opt/ipole_data/gateway_logs/" + tenant + "/" + gid + "/log/" + fname
+	buf, err := os.ReadFile(path)
+	if err != nil {
+		o.Response(Failure, fmt.Sprintf("读取失败:%s", err.Error()), nil)
+		return
+	}
+	// 限制返回最近100KB,避免超大文件
+	content := string(buf)
+	if len(content) > 100*1024 {
+		content = content[len(content)-100*1024:]
+	}
+	o.Response(Success, "成功", content)
+}
+
+// DeployUpload @Title 上传部署文件
+// @Description 上传网关部署文件
+// @router /v1/deploy/upload [post]
+func (o *GatewayController) DeployUpload() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+
+	file, header, err := o.Ctx.Request.FormFile("file")
+	if err != nil {
+		o.Response(Failure, "读取上传文件失败: "+err.Error(), nil)
+		return
+	}
+	defer file.Close()
+
+	data, err := io.ReadAll(file)
+	if err != nil {
+		o.Response(Failure, "读取文件内容失败: "+err.Error(), nil)
+		return
+	}
+
+	md5Hash := fmt.Sprintf("%x", md5.Sum(data))
+
+	// 保存到临时目录
+	tmpDir := filepath.Join(os.TempDir(), "ipole_deploy")
+	os.MkdirAll(tmpDir, os.ModePerm)
+	tmpFile := filepath.Join(tmpDir, md5Hash)
+	if err := os.WriteFile(tmpFile, data, os.ModePerm); err != nil {
+		o.Response(Failure, "保存文件失败: "+err.Error(), nil)
+		return
+	}
+
+	// 创建部署记录(原始SQL绕过GORM零值问题)
+	now := time.Now()
+	if err := models.G_db.Exec("INSERT INTO t_gateway_deploy (gid, tenant, from_version, to_version, md5, status, phase1_result, phase2_result, error_msg, create_time, update_time) VALUES (?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?)", gid, tenant, "", "", md5Hash, "", "", "", now, now).Error; err != nil {
+		o.Response(Failure, "创建部署记录失败: "+err.Error(), nil)
+		return
+	}
+
+	o.Response(Success, "文件上传成功", map[string]interface{}{
+		"md5":      md5Hash,
+		"size":     len(data),
+		"path":     tmpFile,
+		"fileName": header.Filename,
+	})
+}
+
+// DeployPush @Title 下发部署指令
+// @Description 下发部署指令到网关
+// @router /v1/deploy/push [post]
+func (o *GatewayController) DeployPush() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	version := strings.Trim(o.GetString("version"), " ")
+	fileName := strings.Trim(o.GetString("fileName"), " ")
+	filePath := strings.Trim(o.GetString("filePath"), " ")
+	md5Hash := strings.Trim(o.GetString("md5"), " ")
+
+	if gid == "" || tenant == "" || filePath == "" {
+		o.Response(Failure, "参数不完整", nil)
+		return
+	}
+	if fileName == "" {
+		fileName = "ipole"
+	}
+	if version == "" {
+		version = fmt.Sprintf("v%s", time.Now().Format("20060102-150405"))
+	}
+
+	data, err := os.ReadFile(filePath)
+	if err != nil {
+		o.Response(Failure, "读取部署文件失败: "+err.Error(), nil)
+		return
+	}
+
+	const chunkSize = 64 * 1024 // 64KB
+	totalChunks := (len(data) + chunkSize - 1) / chunkSize
+
+	// 1. 发送部署指令
+	var cmd protocol.Pack_DeployCmd
+	seq := GetNextUint64()
+	cmdStr, err := cmd.EnCodeCmd(gid, gid, seq, version, fileName, md5Hash, totalChunks, chunkSize)
+	if err != nil {
+		o.Response(Failure, "编码部署指令失败: "+err.Error(), nil)
+		return
+	}
+	topic := GetTopic(tenant, protocol.DT_GATEWAY, gid, protocol.TP_GW_DEPLOY)
+	GetMqttHandler().PublishString(topic, cmdStr, mqtt.AtLeastOnce)
+
+	// 2. 逐片发送
+	for i := 0; i < totalChunks; i++ {
+		start := i * chunkSize
+		end := start + chunkSize
+		if end > len(data) {
+			end = len(data)
+		}
+		chunkData := base64.StdEncoding.EncodeToString(data[start:end])
+
+		seq := GetNextUint64()
+		chunkStr, err := cmd.EnCodeChunk(gid, gid, seq, version, i, chunkData)
+		if err != nil {
+			o.Response(Failure, fmt.Sprintf("编码分片%d失败: %s", i, err.Error()), nil)
+			return
+		}
+		GetMqttHandler().PublishString(topic, chunkStr, mqtt.AtLeastOnce)
+
+		if i%20 == 0 && i > 0 {
+			time.Sleep(50 * time.Millisecond)
+		}
+	}
+
+	models.G_db.Model(&models.GatewayDeploy{}).
+		Where("gid = ? AND status = 0", gid).
+		Updates(map[string]interface{}{
+			"to_version":   version,
+			"from_version": "",
+			"md5":          md5Hash,
+			"update_time":  time.Now(),
+		})
+
+	o.Response(Success, "部署指令已下发", map[string]interface{}{
+		"totalChunks": totalChunks,
+		"chunkSize":   chunkSize,
+		"fileSize":    len(data),
+	})
+}
+
+// DeployStatus @Title 查询部署状态
+// @Description 查询网关部署状态
+// @router /v1/deploy/status [get]
+func (o *GatewayController) DeployStatus() {
+	gid := strings.Trim(o.GetString("gid"), " ")
+
+	var deploy models.GatewayDeploy
+	err := models.G_db.Where("gid = ?", gid).Order("create_time DESC").First(&deploy).Error
+	if err != nil {
+		// record not found 返回空状态,方便前端轮询等待
+		o.Response(Success, "暂无部署记录", map[string]interface{}{
+			"status": -1,
+		})
+		return
+	}
+
+	statusText := map[uint8]string{0: "进行中", 1: "成功", 2: "失败", 3: "已回滚"}
+
+	o.Response(Success, "查询成功", map[string]interface{}{
+		"id":           deploy.ID,
+		"fromVersion":  deploy.FromVersion,
+		"toVersion":    deploy.ToVersion,
+		"status":       deploy.Status,
+		"statusText":   statusText[deploy.Status],
+		"errorMsg":     deploy.ErrorMsg,
+		"phase1Result": deploy.Phase1Result,
+		"phase2Result": deploy.Phase2Result,
+		"createTime":   deploy.CreateTime.Format("2006-01-02 15:04:05"),
+	})
+}
+
+// DeployBatchStatus @Title 批量查询部署状态
+// @Description 一次查询多个网关的最近一次部署状态
+// @router /v1/deploy/batch-status [get]
+func (o *GatewayController) DeployBatchStatus() {
+	tenant := strings.Trim(o.GetString("tenant"), " ")
+	gidsStr := strings.Trim(o.GetString("gids"), " ")
+
+	if tenant == "" {
+		o.Response(Failure, "tenant不能为空", nil)
+		return
+	}
+
+	// 两步查询:先取每个 gid 最新 id,再取完整记录
+	query := models.G_db.Table("t_gateway_deploy").
+		Where("tenant = ?", tenant)
+
+	if gidsStr != "" {
+		gids := strings.Split(gidsStr, ",")
+		for i := range gids {
+			gids[i] = strings.Trim(gids[i], " ")
+		}
+		query = query.Where("gid IN (?)", gids)
+	}
+
+	var maxIds []int64
+	dbResult := query.Select("MAX(id)").Group("gid").Pluck("MAX(id)", &maxIds)
+	if dbResult.Error != nil {
+		o.Response(Failure, "查询部署记录失败: "+dbResult.Error.Error(), nil)
+		return
+	}
+
+	var deploys []models.GatewayDeploy
+	if len(maxIds) > 0 {
+		models.G_db.Where("id IN (?)", maxIds).Find(&deploys)
+	}
+
+	result := make(map[string]interface{})
+	for _, deploy := range deploys {
+		result[deploy.GID] = map[string]interface{}{
+			"id":           deploy.ID,
+			"status":       deploy.Status,
+			"toVersion":    deploy.ToVersion,
+			"phase1Result": deploy.Phase1Result,
+			"phase2Result": deploy.Phase2Result,
+			"errorMsg":     deploy.ErrorMsg,
+			"createTime":   deploy.CreateTime.Format("2006-01-02 15:04:05"),
+		}
+	}
+
+	// 补充 gids 列表中有但无部署记录的网关(value = null)
+	if gidsStr != "" {
+		for _, gid := range strings.Split(gidsStr, ",") {
+			gid = strings.Trim(gid, " ")
+			if gid == "" {
+				continue
+			}
+			if _, ok := result[gid]; !ok {
+				result[gid] = nil
+			}
+		}
+	}
+
+	o.Response(Success, "查询成功", result)
+}

+ 2 - 2
cloud/websvr/controllers/ciotmodel.go

@@ -2,7 +2,7 @@ package controllers
 
 import (
 	"fmt"
-	"io/ioutil"
+	"io"
 
 	"github.com/astaxie/beego"
 
@@ -44,7 +44,7 @@ func (o *IotModelController) ModelUpload() {
 	defer f.Close()
 
 	f.Seek(0, 0)
-	content, err := ioutil.ReadAll(f)
+	content, err := io.ReadAll(f)
 	if err != nil {
 		o.Response(Failure, fmt.Sprintf("读文件错误:%s", err.Error()), nil)
 		return

+ 8 - 5
cloud/websvr/controllers/clampcontroller.go

@@ -714,6 +714,8 @@ func (o *LampController) Switch() {
 				continue
 			}
 			topic := GetTopic(obj.Tenant, protocol.DT_LAMPCONTROLLER, v.ID, protocol.TP_YM_SET_SWITCH)
+			beego.Info(fmt.Sprintf("YM Switch 发布: topic=%s, payload=%s, SwitchOn=%d, Brightness=%d",
+				topic, str, obj.SwitchOn, obj.Brightness))
 			err = GetMqttHandler().PublishString(topic, str, mqtt.AtLeastOnce)
 			if err != nil {
 				beego.Error(fmt.Sprintf("LampController.Switch:消息发布错误:%s", err.Error()))
@@ -839,16 +841,17 @@ func (o *LampController) Switch() {
 }
 
 // GetTopic 下发的控制命令
-//topic格式:{tenant}/{GID}/{did}/{TP}
-//func GetCtrlTopic(tenant, gid, did, tp string) string {
-//	return tenant + "/" + gid + "/" + did + "/" + tp
-//}
+// topic格式:{tenant}/{GID}/{did}/{TP}
+//
+//	func GetCtrlTopic(tenant, gid, did, tp string) string {
+//		return tenant + "/" + gid + "/" + did + "/" + tp
+//	}
 func GetTopic(tenant, devtype, id, topic string) string {
 	return tenant + "/" + devtype + "/" + id + "/" + topic
 }
 
 // GethltopicDown 下发的控制命令先固定,再从配置文件读
-//topic格式:hllk/113/lamp/longchi/down
+// topic格式:hllk/113/lamp/longchi/down
 func GethltopicDown(tenant, devtype, id, topic string) string {
 	return "hllk/113/lamp/longchi/down"
 }

+ 1 - 0
cloud/websvr/controllers/eventmgr.go

@@ -53,6 +53,7 @@ func (o *EventMgr) Update(code string) {
 func (o *EventMgr) Handler() {
 	defer func() {
 		if err := recover(); err != nil {
+			time.Sleep(5 * time.Second)
 			go o.Handler()
 			beego.Error(fmt.Sprintf("EventMgr.Handler发生异常:%v", err))
 			beego.Error("EventMgr.Handler发生异常:%s", string(debug.Stack()))

+ 23 - 10
cloud/websvr/controllers/mqtthandler.go

@@ -46,7 +46,9 @@ func NewMqttHandler(server, user, password string, timeout uint) *MqttHandler {
 		return nil
 	}
 	o.mqtt = client
-	err = client.Connect(o.Ctx())
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	err = client.Connect(ctx)
 	return &o
 }
 
@@ -68,7 +70,9 @@ func (o *MqttHandler) GetWill() (string, string) {
 
 func (o *MqttHandler) Connect() error {
 	if !o.mqtt.IsConnected() {
-		return o.mqtt.Connect(o.Ctx())
+		ctx, cancel := o.Ctx()
+		defer cancel()
+		return o.mqtt.Connect(ctx)
 	}
 	return nil
 }
@@ -78,15 +82,21 @@ func (o *MqttHandler) IsConnected() bool {
 }
 
 func (o *MqttHandler) Publish(topic string, payload []byte, qos mqtt.QOS) error {
-	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Publish(ctx, topic, payload, qos)
 }
 
 func (o *MqttHandler) PublishString(topic string, payload string, qos mqtt.QOS) error {
-	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.PublishString(ctx, topic, payload, qos)
 }
 
 func (o *MqttHandler) PublishJSON(topic string, payload interface{}, qos mqtt.QOS) error {
-	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.PublishJSON(ctx, topic, payload, qos)
 }
 
 func (o *MqttHandler) Subscribe(topic string, qos mqtt.QOS) error {
@@ -95,7 +105,9 @@ func (o *MqttHandler) Subscribe(topic string, qos mqtt.QOS) error {
 	if _, ok := o.mapTopics[topic]; !ok {
 		o.mapTopics[topic] = qos
 	}
-	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Subscribe(ctx, topic, qos)
 }
 
 func (o *MqttHandler) Unsubscribe(ctx context.Context, topic string) error {
@@ -104,14 +116,15 @@ func (o *MqttHandler) Unsubscribe(ctx context.Context, topic string) error {
 	if _, ok := o.mapTopics[topic]; ok {
 		delete(o.mapTopics, topic)
 	}
-	return o.mqtt.Unsubscribe(o.Ctx(), topic)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Unsubscribe(ctx, topic)
 }
 
 func (o *MqttHandler) Handle(topic string, handler mqtt.MessageHandler) mqtt.Route {
 	return o.mqtt.Handle(topic, handler)
 }
 
-func (o *MqttHandler) Ctx() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.Timeout))
-	return ctx
+func (o *MqttHandler) Ctx() (context.Context, context.CancelFunc) {
+	return context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.Timeout))
 }

+ 23 - 10
cloud/websvr/controllers/mqtthandlerHL.go

@@ -47,7 +47,9 @@ func NewMqttHandlerhl(server, user, password string, timeout uint) *MqttHandlerh
 		return nil
 	}
 	o.mqtt = client
-	err = client.Connect(o.Ctx())
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	err = client.Connect(ctx)
 	return &o
 }
 
@@ -69,7 +71,9 @@ func (o *MqttHandlerhl) GetWill() (string, string) {
 
 func (o *MqttHandlerhl) Connect() error {
 	if !o.mqtt.IsConnected() {
-		return o.mqtt.Connect(o.Ctx())
+		ctx, cancel := o.Ctx()
+		defer cancel()
+		return o.mqtt.Connect(ctx)
 	}
 	return nil
 }
@@ -79,15 +83,21 @@ func (o *MqttHandlerhl) IsConnected() bool {
 }
 
 func (o *MqttHandlerhl) Publish(topic string, payload []byte, qos mqtt.QOS) error {
-	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Publish(ctx, topic, payload, qos)
 }
 
 func (o *MqttHandlerhl) PublishString(topic string, payload string, qos mqtt.QOS) error {
-	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.PublishString(ctx, topic, payload, qos)
 }
 
 func (o *MqttHandlerhl) PublishJSON(topic string, payload interface{}, qos mqtt.QOS) error {
-	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.PublishJSON(ctx, topic, payload, qos)
 }
 
 func (o *MqttHandlerhl) Subscribe(topic string, qos mqtt.QOS) error {
@@ -96,7 +106,9 @@ func (o *MqttHandlerhl) Subscribe(topic string, qos mqtt.QOS) error {
 	if _, ok := o.mapTopics[topic]; !ok {
 		o.mapTopics[topic] = qos
 	}
-	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Subscribe(ctx, topic, qos)
 }
 
 func (o *MqttHandlerhl) Unsubscribe(ctx context.Context, topic string) error {
@@ -105,14 +117,15 @@ func (o *MqttHandlerhl) Unsubscribe(ctx context.Context, topic string) error {
 	if _, ok := o.mapTopics[topic]; ok {
 		delete(o.mapTopics, topic)
 	}
-	return o.mqtt.Unsubscribe(o.Ctx(), topic)
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Unsubscribe(ctx, topic)
 }
 
 func (o *MqttHandlerhl) Handle(topic string, handler mqtt.MessageHandler) mqtt.Route {
 	return o.mqtt.Handle(topic, handler)
 }
 
-func (o *MqttHandlerhl) Ctx() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.Timeout))
-	return ctx
+func (o *MqttHandlerhl) Ctx() (context.Context, context.CancelFunc) {
+	return context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.Timeout))
 }

+ 189 - 0
cloud/websvr/routers/commentsRouter_.go

@@ -457,6 +457,195 @@ func init() {
 			Filters:          nil,
 			Params:           nil})
 
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "PushSerialConfig",
+			Router:           `/v1/serial/push`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "PushDevConfig",
+			Router:           `/v1/dev/push`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "PushModelConfig",
+			Router:           `/v1/model/push`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "SerialConfigList",
+			Router:           `/v1/serial/list`,
+			AllowHTTPMethods: []string{"get"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "SerialConfigSave",
+			Router:           `/v1/serial/save`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "SerialConfigDelete",
+			Router:           `/v1/serial/delete`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "DevConfigList",
+			Router:           `/v1/dev/list`,
+			AllowHTTPMethods: []string{"get"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "DevConfigSave",
+			Router:           `/v1/dev/save`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "DevConfigDelete",
+			Router:           `/v1/dev/delete`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "ModelList",
+			Router:           `/v1/model/list`,
+			AllowHTTPMethods: []string{"get"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "ModelSave",
+			Router:           `/v1/model/save`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "ModelDelete",
+			Router:           `/v1/model/delete`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "LogQuery",
+			Router:           `/v1/log/query`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "LogList",
+			Router:           `/v1/log/list`,
+			AllowHTTPMethods: []string{"get"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "LogRead",
+			Router:           `/v1/log/read`,
+			AllowHTTPMethods: []string{"get"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "LogDelete",
+			Router:           `/v1/log/delete`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "LogCfg",
+			Router:           `/v1/log/cfg`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "DeployUpload",
+			Router:           `/v1/deploy/upload`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "DeployPush",
+			Router:           `/v1/deploy/push`,
+			AllowHTTPMethods: []string{"post"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "DeployStatus",
+			Router:           `/v1/deploy/status`,
+			AllowHTTPMethods: []string{"get"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
+	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:GatewayController"],
+		beego.ControllerComments{
+			Method:           "DeployBatchStatus",
+			Router:           `/v1/deploy/batch-status`,
+			AllowHTTPMethods: []string{"get"},
+			MethodParams:     param.Make(),
+			Filters:          nil,
+			Params:           nil})
+
 	beego.GlobalControllerRouter["lc/cloud/websvr/controllers:IotModelController"] = append(beego.GlobalControllerRouter["lc/cloud/websvr/controllers:IotModelController"],
 		beego.ControllerComments{
 			Method:           "ModelUpload",

+ 1 - 2
common/configor/utils.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
 	"path"
 	"reflect"
@@ -101,7 +100,7 @@ func (configor *Configor) getConfigurationFiles(watchMode bool, files ...string)
 }
 
 func processFile(config interface{}, file string, errorOnUnmatchedKeys bool) error {
-	data, err := ioutil.ReadFile(file)
+	data, err := os.ReadFile(file)
 	if err != nil {
 		return err
 	}

+ 23 - 0
common/models/gateway_deploy.go

@@ -0,0 +1,23 @@
+package models
+
+import "time"
+
+// GatewayDeploy 网关部署记录
+type GatewayDeploy struct {
+	ID           int64     `gorm:"column:id;type:bigint(20);primary_key;AUTO_INCREMENT"`
+	GID          string    `gorm:"column:gid;type:varchar(64);NOT NULL;index:idx_gid"`
+	Tenant       string    `gorm:"column:tenant;type:varchar(64);NOT NULL;index:idx_tenant_time"`
+	FromVersion  string    `gorm:"column:from_version;type:varchar(32)"`
+	ToVersion    string    `gorm:"column:to_version;type:varchar(32);NOT NULL"`
+	MD5          string    `gorm:"column:md5;type:varchar(64)"`
+	Status       uint8     `gorm:"column:status;type:tinyint(4);default:0"` // 0进行中 1成功 2失败 3回滚
+	Phase1Result string    `gorm:"column:phase1_result;type:text"`          // Phase 1 健康检测结果 JSON
+	Phase2Result string    `gorm:"column:phase2_result;type:text"`          // Phase 2 健康检测结果 JSON
+	ErrorMsg     string    `gorm:"column:error_msg;type:text"`              // 失败原因
+	CreateTime   time.Time `gorm:"column:create_time;type:datetime"`
+	UpdateTime   time.Time `gorm:"column:update_time;type:datetime"`
+}
+
+func (GatewayDeploy) TableName() string {
+	return "t_gateway_deploy"
+}

+ 19 - 0
common/models/gatewaymodel.go

@@ -0,0 +1,19 @@
+package models
+
+import "time"
+
+// GatewayModel 网关已下发的物模型快照
+type GatewayModel struct {
+	GID       string    `gorm:"type:varchar(32);primary_key"` // 网关ID
+	TID       uint16    `gorm:"type:int;primary_key"`         // 物模型ID
+	Device    string    `gorm:"type:varchar(40)"`             // 设备名称
+	Model     string    `gorm:"type:varchar(18)"`             // 设备型号
+	Protocol  string    `gorm:"type:varchar(18)"`             // 协议类型
+	File      string    `gorm:"type:text"`                    // 模型文件内容快照
+	PushedAt  time.Time `gorm:"type:datetime"`                // 下发时间
+	CreatedAt time.Time
+}
+
+func (GatewayModel) TableName() string {
+	return "t_gateway_model"
+}

+ 6 - 0
common/models/init.go

@@ -188,6 +188,12 @@ func CreateTable() {
 	if !G_db.HasTable(&CableGuardianStatus{}) {
 		G_db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&CableGuardianStatus{})
 	}
+	if !G_db.HasTable(&GatewayDeploy{}) {
+		G_db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&GatewayDeploy{})
+	}
+	if !G_db.HasTable(&GatewayModel{}) {
+		G_db.Set("gorm:table_options", "ENGINE=InnoDB").CreateTable(&GatewayModel{})
+	}
 }
 
 func InitDB(conf *util.MySQLConfig) {

+ 145 - 0
common/mqtt/client_wrapper.go

@@ -0,0 +1,145 @@
+package mqtt
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	"github.com/sirupsen/logrus"
+)
+
+// BaseMqttOnline 上线消息和遗嘱消息接口
+type BaseMqttOnline interface {
+	GetOnlineMsg() (string, string)
+	GetWillMsg() (string, string)
+}
+
+// EmptyMqttOnline 空实现:不上线、无遗嘱
+type EmptyMqttOnline struct{}
+
+func (o *EmptyMqttOnline) GetOnlineMsg() (string, string) { return "", "" }
+func (o *EmptyMqttOnline) GetWillMsg() (string, string)   { return "", "" }
+
+// MqttClient 封装 common/mqtt.Client,提供超时控制、主题记忆、断线自动重订阅
+type MqttClient struct {
+	mqtt       *Client
+	mu         sync.Mutex
+	mapTopics  map[string]QOS
+	timeout    uint
+	MqttOnline BaseMqttOnline
+}
+
+// NewMqttClient 创建并连接 MQTT 客户端
+func NewMqttClient(server, clientID, user, password string, timeout uint, online BaseMqttOnline) *MqttClient {
+	o := &MqttClient{
+		mapTopics:  make(map[string]QOS),
+		timeout:    timeout,
+		MqttOnline: online,
+	}
+	client, err := NewClient(ClientOptions{
+		Servers:       []string{server},
+		ClientID:      clientID,
+		Username:      user,
+		Password:      password,
+		AutoReconnect: true,
+	}, o)
+	if err != nil {
+		panic(fmt.Errorf("MQTT错误: %w", err))
+	}
+	o.mqtt = client
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	_ = client.Connect(ctx)
+	return o
+}
+
+// ---- 实现 ConnHandler 接口 ----
+
+func (o *MqttClient) ConnectionLostHandler(err error) {
+	logrus.Errorln("MqttClient.ConnectionLostHandler:MQTT连接已经断开,原因:", err)
+}
+
+func (o *MqttClient) OnConnectHandler() {
+	logrus.Infoln("MqttClient.OnConnectHandler:MQTT连接成功")
+	for k, v := range o.mapTopics {
+		if err := o.Subscribe(k, v); err != nil {
+			logrus.Errorf("重订阅主题 %s 失败: %v", k, err)
+		}
+	}
+	topic, str := o.MqttOnline.GetOnlineMsg()
+	if topic != "" {
+		if err := o.PublishString(topic, str, 1); err != nil {
+			logrus.Errorf("发布上线消息失败: topic=%s, err=%v", topic, err)
+		} else {
+			logrus.Infof("发布上线消息成功: topic=%s", topic)
+		}
+	} else {
+		logrus.Warnln("发布上线消息跳过: GetOnlineMsg返回空topic,请检查appConfig.GID是否已加载")
+	}
+}
+
+func (o *MqttClient) GetWill() (string, string) {
+	return o.MqttOnline.GetWillMsg()
+}
+
+// ---- 操作代理 ----
+
+func (o *MqttClient) Connect() error {
+	if !o.mqtt.IsConnected() {
+		ctx, cancel := o.Ctx()
+		defer cancel()
+		return o.mqtt.Connect(ctx)
+	}
+	return nil
+}
+
+func (o *MqttClient) IsConnected() bool { return o.mqtt.IsConnected() }
+
+func (o *MqttClient) Publish(topic string, payload []byte, qos QOS) error {
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Publish(ctx, topic, payload, qos)
+}
+
+func (o *MqttClient) PublishString(topic string, payload string, qos QOS) error {
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.PublishString(ctx, topic, payload, qos)
+}
+
+func (o *MqttClient) PublishJSON(topic string, payload interface{}, qos QOS) error {
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.PublishJSON(ctx, topic, payload, qos)
+}
+
+func (o *MqttClient) Subscribe(topic string, qos QOS) error {
+	o.mu.Lock()
+	if _, ok := o.mapTopics[topic]; !ok {
+		o.mapTopics[topic] = qos
+	}
+	o.mu.Unlock()
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Subscribe(ctx, topic, qos)
+}
+
+func (o *MqttClient) Unsubscribe(topic string) error {
+	o.mu.Lock()
+	if _, ok := o.mapTopics[topic]; ok {
+		delete(o.mapTopics, topic)
+	}
+	o.mu.Unlock()
+	ctx, cancel := o.Ctx()
+	defer cancel()
+	return o.mqtt.Unsubscribe(ctx, topic)
+}
+
+func (o *MqttClient) Handle(topic string, handler MessageHandler) Route {
+	return o.mqtt.Handle(topic, handler)
+}
+
+func (o *MqttClient) Ctx() (context.Context, context.CancelFunc) {
+	return context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.timeout))
+}

+ 216 - 0
common/mqtt/mgr.go

@@ -0,0 +1,216 @@
+package mqtt
+
+import (
+	"runtime/debug"
+	"time"
+
+	"github.com/sirupsen/logrus"
+
+	"lc/common/util"
+)
+
+// OptType 消息发布/订阅方向
+type OptType uint8
+
+const (
+	ToAll   OptType = 0 // 同时发布到云端和边缘端
+	ToCloud OptType = 1 // 仅云端
+	ToEdge  OptType = 2 // 仅边缘端
+)
+
+// MQTTMessage 队列中的消息
+type MQTTMessage struct {
+	topic   string
+	payload string
+	qos     QOS
+	tp      OptType
+}
+
+// RestartFunc 协程 panic 后重建回调
+type RestartFunc func(fn func(args ...interface{}) interface{}, args ...interface{})
+
+// MQTTMgr 管理 Cloud/Edge 双客户端,支持异步队列发布
+type MQTTMgr struct {
+	Cloud     *MqttClient
+	Edge      *MqttClient
+	Queue     *util.MlQueue
+	restartFn RestartFunc
+}
+
+// MQTTMgrConfig 管理器构造参数
+type MQTTMgrConfig struct {
+	CloudServer   string
+	CloudClientID string
+	CloudUser     string
+	CloudPassword string
+	CloudTimeout  uint
+	CloudOnline   BaseMqttOnline // nil 则使用 EmptyMqttOnline
+
+	EdgeServer   string
+	EdgeClientID string
+	EdgeUser     string
+	EdgePassword string
+	EdgeTimeout  uint
+	EdgeOnline   BaseMqttOnline // nil 则使用 EmptyMqttOnline
+}
+
+// NewMQTTMgr 创建 MQTT 管理器
+func NewMQTTMgr(cfg MQTTMgrConfig) *MQTTMgr {
+	mgr := &MQTTMgr{
+		Queue: util.NewQueue(2000),
+	}
+	if cfg.CloudOnline == nil {
+		cfg.CloudOnline = &EmptyMqttOnline{}
+	}
+	if cfg.EdgeOnline == nil {
+		cfg.EdgeOnline = &EmptyMqttOnline{}
+	}
+	if cfg.CloudServer != "" {
+		mgr.Cloud = NewMqttClient(cfg.CloudServer, cfg.CloudClientID,
+			cfg.CloudUser, cfg.CloudPassword, cfg.CloudTimeout, cfg.CloudOnline)
+	}
+	if cfg.EdgeServer != "" {
+		mgr.Edge = NewMqttClient(cfg.EdgeServer, cfg.EdgeClientID,
+			cfg.EdgeUser, cfg.EdgePassword, cfg.EdgeTimeout, cfg.EdgeOnline)
+	}
+	return mgr
+}
+
+// SetRestartFn 设置 panic 恢复后的协程重建回调
+func (o *MQTTMgr) SetRestartFn(fn RestartFunc) {
+	o.restartFn = fn
+}
+
+// Subscribe 订阅主题,支持方向选择
+func (o *MQTTMgr) Subscribe(topic string, qos QOS, handler MessageHandler, tp OptType) {
+	logrus.Infof("MQTTMgr.Subscribe:topic=%s,qos=%d,direction=%d", topic, qos, tp)
+	switch tp {
+	case ToAll:
+		if o.Cloud != nil {
+			o.Cloud.Handle(topic, handler)
+			if err := o.Cloud.Subscribe(topic, qos); err != nil {
+				logrus.Errorf("MQTTMgr.Subscribe:Cloud订阅失败,topic=%s,err=%v", topic, err)
+			}
+		} else {
+			logrus.Warnf("MQTTMgr.Subscribe:Cloud客户端为nil,无法订阅topic=%s", topic)
+		}
+		if o.Edge != nil {
+			o.Edge.Handle(topic, handler)
+			if err := o.Edge.Subscribe(topic, qos); err != nil {
+				logrus.Errorf("MQTTMgr.Subscribe:Edge订阅失败,topic=%s,err=%v", topic, err)
+			}
+		} else {
+			logrus.Warnf("MQTTMgr.Subscribe:Edge客户端为nil,无法订阅topic=%s", topic)
+		}
+	case ToCloud:
+		if o.Cloud != nil {
+			o.Cloud.Handle(topic, handler)
+			if err := o.Cloud.Subscribe(topic, qos); err != nil {
+				logrus.Errorf("MQTTMgr.Subscribe:Cloud订阅失败,topic=%s,err=%v", topic, err)
+			}
+		} else {
+			logrus.Warnf("MQTTMgr.Subscribe:Cloud客户端为nil,无法订阅topic=%s", topic)
+		}
+	case ToEdge:
+		if o.Edge != nil {
+			o.Edge.Handle(topic, handler)
+			if err := o.Edge.Subscribe(topic, qos); err != nil {
+				logrus.Errorf("MQTTMgr.Subscribe:Edge订阅失败,topic=%s,err=%v", topic, err)
+			}
+		} else {
+			logrus.Warnf("MQTTMgr.Subscribe:Edge客户端为nil,无法订阅topic=%s", topic)
+		}
+	}
+}
+
+// UnSubscribe 退订主题
+func (o *MQTTMgr) UnSubscribe(topic string, tp OptType) {
+	switch tp {
+	case ToAll:
+		if o.Cloud != nil {
+			_ = o.Cloud.Unsubscribe(topic)
+		}
+		if o.Edge != nil {
+			_ = o.Edge.Unsubscribe(topic)
+		}
+	case ToCloud:
+		if o.Cloud != nil {
+			_ = o.Cloud.Unsubscribe(topic)
+		}
+	case ToEdge:
+		if o.Edge != nil {
+			_ = o.Edge.Unsubscribe(topic)
+		}
+	}
+}
+
+// Publish 异步发布(入队列)
+func (o *MQTTMgr) Publish(topic, payload string, qos QOS, tp OptType) {
+	o.Queue.Put(&MQTTMessage{topic: topic, payload: payload, qos: qos, tp: tp})
+}
+
+// doPublish 底层同步发布
+func (o *MQTTMgr) doPublish(msg *MQTTMessage) error {
+	var err error
+	switch msg.tp {
+	case ToAll:
+		if o.Cloud != nil {
+			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
+		}
+		if o.Edge != nil {
+			_ = o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
+		}
+	case ToCloud:
+		if o.Cloud != nil {
+			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
+		}
+	case ToEdge:
+		if o.Edge != nil {
+			err = o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
+		}
+	}
+	return err
+}
+
+// MQTTConnectMgr 连接保持协程(每 10 秒重连)
+func (o *MQTTMgr) MQTTConnectMgr(args ...interface{}) interface{} {
+	for {
+		time.Sleep(10 * time.Second)
+		if o.Cloud != nil {
+			o.Cloud.Connect()
+		}
+		if o.Edge != nil {
+			o.Edge.Connect()
+		}
+	}
+}
+
+// MQTTMessageHandle 队列消费协程(带 RETRY)
+func (o *MQTTMgr) MQTTMessageHandle(args ...interface{}) interface{} {
+	defer func() {
+		if err := recover(); err != nil {
+			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常:%v", err)
+			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常,堆栈信息:%s", string(debug.Stack()))
+			time.Sleep(time.Second)
+			if o.restartFn != nil {
+				o.restartFn(o.MQTTMessageHandle, args)
+			}
+		}
+	}()
+	for {
+		if m, ok, _ := o.Queue.Get(); ok {
+			if msg, ok := m.(*MQTTMessage); ok {
+				for {
+					if err := o.doPublish(msg); err != nil {
+						logrus.Errorf("发布主题为%s的消息失败,原因:%s", msg.topic, err.Error())
+						time.Sleep(time.Second)
+						continue
+					}
+					break
+				}
+			}
+		} else {
+			time.Sleep(200 * time.Millisecond)
+		}
+	}
+}

+ 14 - 17
common/mqtt/mqtt.go

@@ -93,17 +93,20 @@ func NewClient(options ClientOptions, connhandler ConnHandler) (*Client, error)
 
 	pahoOptions.SetCleanSession(false)
 
-	var client Client
+	client := &Client{
+		Options:     options,
+		router:      newRouter(),
+		connhandler: connhandler,
+	}
 	pahoOptions.SetConnectionLostHandler(client.ConnectionLostHandler) //断连
 	pahoOptions.SetOnConnectHandler(client.OnConnectHandler)           //连接
 	if t, m := connhandler.GetWill(); t != "" {
-		pahoOptions.SetWill(t, m, 0, false) //遗嘱消息
+		pahoOptions.SetWill(t, m, 1, false) //遗嘱消息,QoS=1确保送达
 	}
 
 	pahoClient := paho.NewClient(pahoOptions)
-	router := newRouter()
 	pahoClient.AddRoute("#", handle(func(message Message) {
-		routes := router.match(&message)
+		routes := client.router.match(&message)
 		for _, route := range routes {
 			m := message
 			m.vars = route.vars(&message)
@@ -112,11 +115,8 @@ func NewClient(options ClientOptions, connhandler ConnHandler) (*Client, error)
 	}))
 
 	client.client = pahoClient
-	client.Options = options
-	client.router = router
-	client.connhandler = connhandler
 
-	return &client, nil
+	return client, nil
 }
 
 // Connect tries to establish a connection with the mqtt servers
@@ -138,21 +138,18 @@ func (c *Client) DisconnectImmediately() {
 }
 
 func tokenWithContext(ctx context.Context, token paho.Token) error {
-	completer := make(chan error)
+	completer := make(chan error, 1)
 
-	// TODO: This go routine will not be removed up if the ctx is cancelled or a the ctx timeout passes
 	go func() {
 		token.Wait()
 		completer <- token.Error()
 	}()
 
-	for {
-		select {
-		case <-ctx.Done():
-			return ctx.Err()
-		case err := <-completer:
-			return err
-		}
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	case err := <-completer:
+		return err
 	}
 }
 func (c *Client) ConnectionLostHandler(client paho.Client, err error) {

+ 0 - 3
common/onvif/examples/discovery/discovery.go

@@ -2,11 +2,8 @@ package main
 
 import (
 	"fmt"
-	"log"
 	"time"
 
-	"github.com/kr/pretty"
-
 	"lc/common/onvif/discovery"
 	"lc/common/onvif/profiles/devicemgmt"
 	"lc/common/onvif/profiles/media"

+ 0 - 3
common/onvif/examples/recording/recording.go

@@ -1,11 +1,8 @@
 package main
 
 import (
-	"log"
 	"time"
 
-	"github.com/kr/pretty"
-
 	"lc/common/onvif/profiles/recording"
 	"lc/common/onvif/soap"
 )

+ 2 - 2
common/onvif/soap/soap.go

@@ -3,6 +3,7 @@ package soap
 import (
 	"bytes"
 	"context"
+	crand "crypto/rand"
 	"crypto/sha1"
 	"crypto/tls"
 	"encoding/base64"
@@ -207,10 +208,9 @@ func NewWSSSecurityHeader(user, pass string) *WSSSecurityHeader {
 
 	// Nonce
 	b := make([]byte, 16)
-	rand.Read(b)
+	crand.Read(b)
 	//nonce := fmt.Sprintf("%x-%x-%x-%x-%x",
 	//	b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
-	rand.Seed(time.Now().UnixNano())
 	nonce := randomString(20)
 
 	hdr.UsernameToken.Nonce.EncodingType = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary"

+ 9 - 7
common/protocol/config.go

@@ -23,13 +23,15 @@ type CloudConfig struct {
 }
 
 type AppConfig struct {
-	Name    string      `json:"name"`    //应用名称
-	Tenant  string      `json:"tenant"`  //租户ID
-	GID     string      `json:"gid"`     //网关ID
-	SN      string      `json:"sn"`      //序列号
-	Upgrade bool        `json:"upgrade"` //需要升级
-	Edge    EdgeConfig  `json:"edge"`    //边缘端配置
-	Cloud   CloudConfig `json:"cloud"`   //云端配置
+	Name      string      `json:"name"`      //应用名称
+	Tenant    string      `json:"tenant"`    //租户ID
+	GID       string      `json:"gid"`       //网关ID
+	SN        string      `json:"sn"`        //序列号
+	Upgrade   bool        `json:"upgrade"`   //需要升级
+	UpdateUrl string      `json:"updateurl"` //更新服务URL
+	NtpServer string      `json:"ntpserver"` //NTP服务器地址
+	Edge      EdgeConfig  `json:"edge"`      //边缘端配置
+	Cloud     CloudConfig `json:"cloud"`     //云端配置
 }
 
 // --------------------------------------------------------

+ 120 - 0
common/protocol/deploy.go

@@ -0,0 +1,120 @@
+package protocol
+
+// ============================================================
+// 部署指令/分片 (Cloud → Edge)
+// ============================================================
+
+// Pack_DeployCmd 部署指令或分片
+type Pack_DeployCmd struct {
+	Header
+	Data DeployCmdData `json:"data"`
+}
+
+type DeployCmdData struct {
+	Version     string `json:"Version"`     // 目标版本号
+	FileName    string `json:"FileName"`    // 文件名, e.g. "ipole"
+	TotalChunks int    `json:"TotalChunks"` // 分片总数(Action="start"时有效)
+	ChunkSize   int    `json:"ChunkSize"`   // 单片大小(Action="start"时有效)
+	ChunkIndex  int    `json:"ChunkIndex"`  // 当前分片索引(Action="chunk"时有效)
+	MD5         string `json:"MD5"`         // 完整文件 MD5(Action="start"时有效)
+	Data        string `json:"Data"`        // base64 编码的分片数据(Action="chunk"时有效)
+	Action      string `json:"Action"`      // "start" | "chunk" | "abort"
+}
+
+func (o *Pack_DeployCmd) EnCodeCmd(id, gid string, seq uint64, version, fileName, md5 string, totalChunks, chunkSize int) (string, error) {
+	o.Header.SetHeaderData(id, gid, seq)
+	o.Data.Version = version
+	o.Data.FileName = fileName
+	o.Data.TotalChunks = totalChunks
+	o.Data.ChunkSize = chunkSize
+	o.Data.MD5 = md5
+	o.Data.Action = "start"
+	return json.MarshalToString(o)
+}
+
+func (o *Pack_DeployCmd) EnCodeChunk(id, gid string, seq uint64, version string, chunkIndex int, data string) (string, error) {
+	o.Header.SetHeaderData(id, gid, seq)
+	o.Data.Version = version
+	o.Data.ChunkIndex = chunkIndex
+	o.Data.Data = data
+	o.Data.Action = "chunk"
+	return json.MarshalToString(o)
+}
+
+func (o *Pack_DeployCmd) EnCodeAbort(id, gid string, seq uint64, version string) (string, error) {
+	o.Header.SetHeaderData(id, gid, seq)
+	o.Data.Version = version
+	o.Data.Action = "abort"
+	return json.MarshalToString(o)
+}
+
+func (o *Pack_DeployCmd) DeCode(message string) error {
+	return json.UnmarshalFromString(message, o)
+}
+
+// ============================================================
+// 部署执行 ACK (Edge → Cloud)
+// ============================================================
+
+// Pack_DeployAck 部署执行结果(文件接收+替换的成功/失败)
+type Pack_DeployAck struct {
+	Header
+	Data DeployAckData `json:"data"`
+}
+
+type DeployAckData struct {
+	Version string `json:"Version"`
+	Success bool   `json:"Success"`
+	Error   string `json:"Error"` // 失败时填写原因
+}
+
+func (o *Pack_DeployAck) EnCode(id, gid string, seq uint64, version string, success bool, errMsg string) (string, error) {
+	o.Header.SetHeaderData(id, gid, seq)
+	o.Data.Version = version
+	o.Data.Success = success
+	o.Data.Error = errMsg
+	return json.MarshalToString(o)
+}
+
+func (o *Pack_DeployAck) DeCode(message string) error {
+	return json.UnmarshalFromString(message, o)
+}
+
+// ============================================================
+// 健康检测结果 (Edge → Cloud)
+// ============================================================
+
+// Pack_HealthCheck 健康检测结果上报
+type Pack_HealthCheck struct {
+	Header
+	Data HealthCheckData `json:"data"`
+}
+
+type HealthCheckData struct {
+	Version   string        `json:"Version"`
+	Phase     int           `json:"Phase"`   // 1 或 2
+	Success   bool          `json:"Success"` // 当前 Phase 是否全部通过
+	Timestamp int64         `json:"Timestamp"`
+	Checks    []HealthCheck `json:"Checks"`
+}
+
+type HealthCheck struct {
+	Name       string `json:"Name"`       // "process" | "mqtt" | "redis" | "serial" | "modbus" | "device_online"
+	Status     string `json:"Status"`     // "ok" | "fail" | "pending" | "timeout"
+	Detail     string `json:"Detail"`     // 人类可读的详情
+	DurationMs int    `json:"DurationMs"` // 检测耗时ms
+}
+
+func (o *Pack_HealthCheck) EnCode(id, gid string, seq uint64, version string, phase int, success bool, checks []HealthCheck) (string, error) {
+	o.Header.SetHeaderData(id, gid, seq)
+	o.Data.Version = version
+	o.Data.Phase = phase
+	o.Data.Success = success
+	o.Data.Timestamp = BJNow().Unix()
+	o.Data.Checks = checks
+	return json.MarshalToString(o)
+}
+
+func (o *Pack_HealthCheck) DeCode(message string) error {
+	return json.UnmarshalFromString(message, o)
+}

+ 91 - 0
common/protocol/deploy_test.go

@@ -0,0 +1,91 @@
+package protocol
+
+import (
+	"encoding/base64"
+	"testing"
+)
+
+func TestPack_DeployCmd_EnCodeCmd_DeCode(t *testing.T) {
+	var obj Pack_DeployCmd
+	str, err := obj.EnCodeCmd("gid1", "gid1", 100, "1.5.0", "ipole", "abc123def", 320, 65536)
+	if err != nil {
+		t.Fatalf("EnCodeCmd failed: %v", err)
+	}
+	if str == "" {
+		t.Fatal("EnCodeCmd returned empty string")
+	}
+
+	var obj2 Pack_DeployCmd
+	if err := obj2.DeCode(str); err != nil {
+		t.Fatalf("DeCode failed: %v", err)
+	}
+	if obj2.Data.Version != "1.5.0" {
+		t.Errorf("Version mismatch: got %s, want 1.5.0", obj2.Data.Version)
+	}
+	if obj2.Data.Action != "start" {
+		t.Errorf("Action mismatch: got %s, want start", obj2.Data.Action)
+	}
+	if obj2.Data.TotalChunks != 320 {
+		t.Errorf("TotalChunks mismatch: got %d, want 320", obj2.Data.TotalChunks)
+	}
+}
+
+func TestPack_DeployCmd_EnCodeChunk_DeCode(t *testing.T) {
+	var obj Pack_DeployCmd
+	testData := base64.StdEncoding.EncodeToString([]byte("hello world test chunk data"))
+	str, err := obj.EnCodeChunk("gid1", "gid1", 101, "1.5.0", 5, testData)
+	if err != nil {
+		t.Fatalf("EnCodeChunk failed: %v", err)
+	}
+
+	var obj2 Pack_DeployCmd
+	if err := obj2.DeCode(str); err != nil {
+		t.Fatalf("DeCode failed: %v", err)
+	}
+	if obj2.Data.ChunkIndex != 5 {
+		t.Errorf("ChunkIndex mismatch: got %d, want 5", obj2.Data.ChunkIndex)
+	}
+	if obj2.Data.Action != "chunk" {
+		t.Errorf("Action mismatch: got %s, want chunk", obj2.Data.Action)
+	}
+}
+
+func TestPack_DeployAck_EnCode_DeCode(t *testing.T) {
+	var obj Pack_DeployAck
+	str, err := obj.EnCode("gid1", "gid1", 200, "1.5.0", true, "")
+	if err != nil {
+		t.Fatalf("EnCode failed: %v", err)
+	}
+
+	var obj2 Pack_DeployAck
+	if err := obj2.DeCode(str); err != nil {
+		t.Fatalf("DeCode failed: %v", err)
+	}
+	if !obj2.Data.Success {
+		t.Error("Expected Success=true")
+	}
+}
+
+func TestPack_HealthCheck_EnCode_DeCode(t *testing.T) {
+	var obj Pack_HealthCheck
+	checks := []HealthCheck{
+		{Name: "mqtt", Status: "ok", Detail: "connected", DurationMs: 150},
+		{Name: "redis", Status: "ok", Detail: "PING ok", DurationMs: 10},
+	}
+
+	str, err := obj.EnCode("gid1", "gid1", 300, "1.5.0", 1, true, checks)
+	if err != nil {
+		t.Fatalf("EnCode failed: %v", err)
+	}
+
+	var obj2 Pack_HealthCheck
+	if err := obj2.DeCode(str); err != nil {
+		t.Fatalf("DeCode failed: %v", err)
+	}
+	if obj2.Data.Phase != 1 {
+		t.Errorf("Phase mismatch: got %d, want 1", obj2.Data.Phase)
+	}
+	if len(obj2.Data.Checks) != 2 {
+		t.Errorf("Checks count: got %d, want 2", len(obj2.Data.Checks))
+	}
+}

+ 7 - 2
common/protocol/topic.go

@@ -39,14 +39,19 @@ var (
 	TP_GW_LOG_ACK        string = "log/ack"
 	TP_GW_REMOVE_LOG     string = "rlog"
 	TP_GW_REMOVE_LOG_ACK string = "rlog/ack"
+	TP_GW_LOG_CFG        string = "logcfg"
+	TP_GW_LOG_CFG_ACK    string = "logcfg/ack"
 	TP_GW_ONLINE         string = "online"
 	TP_GW_WILL           string = "will"
 	TP_GW_SYS            string = "sys"
 	TP_GW_SYS_ACK        string = "sys/ack"
 	TP_GW_ITS            string = "its"
 	TP_GW_ITS_ACK        string = "its/ack"
-	TP_GW_ONVIFDEV       string = "onvifdev"     //海康一键告警或摄像头设备上报
-	TP_GW_ONVIFDEV_ACK   string = "onvifdev/ack" //海康一键告警或摄像头设备上报
+	TP_GW_ONVIFDEV       string = "onvifdev"      //海康一键告警或摄像头设备上报
+	TP_GW_ONVIFDEV_ACK   string = "onvifdev/ack"  //海康一键告警或摄像头设备上报
+	TP_GW_DEPLOY         string = "deploy"        //一键部署: 下发部署命令+二进制分片
+	TP_GW_DEPLOY_ACK     string = "deploy/ack"    //一键部署: 部署结果上报
+	TP_GW_DEPLOY_HEALTH  string = "deploy/health" //一键部署: 健康检查结果上报
 )
 
 // modbus协议设备通用

+ 2 - 2
common/util/gzip.go

@@ -3,7 +3,7 @@ package util
 import (
 	"bytes"
 	"compress/gzip"
-	"io/ioutil"
+	"io"
 )
 
 func GzipEncode(in []byte) ([]byte, error) {
@@ -34,5 +34,5 @@ func GzipDecode(in []byte) ([]byte, error) {
 	}
 	defer reader.Close()
 
-	return ioutil.ReadAll(reader)
+	return io.ReadAll(reader)
 }

+ 233 - 0
common/util/taglog.go

@@ -0,0 +1,233 @@
+package util
+
+import (
+	"encoding/json"
+	"os"
+	"sync"
+
+	"github.com/sirupsen/logrus"
+)
+
+// TagLogger 带设备标签的日志包装器,支持运行时级别控制
+type TagLogger struct {
+	mu          sync.RWMutex
+	globalLevel logrus.Level
+	tagLevels   map[string]logrus.Level
+	persistPath string
+}
+
+// LogLevelConfig 持久化配置结构
+type LogLevelConfig struct {
+	Level string            `json:"level"`
+	Tags  map[string]string `json:"tags"`
+}
+
+var _tagLog *TagLogger
+
+// InitTagLog 初始化全局 TagLogger
+func InitTagLog(globalLevel logrus.Level) {
+	_tagLog = &TagLogger{
+		globalLevel: globalLevel,
+		tagLevels:   make(map[string]logrus.Level),
+		persistPath: "conf/logcfg.json",
+	}
+}
+
+// GetTagLog 获取全局 TagLogger 实例
+func GetTagLog() *TagLogger {
+	if _tagLog == nil {
+		InitTagLog(logrus.InfoLevel)
+	}
+	return _tagLog
+}
+
+// SetPersistPath 设置持久化文件路径
+func (o *TagLogger) SetPersistPath(path string) {
+	o.persistPath = path
+}
+
+// SetLevel 设置全局默认级别(线程安全)
+func (o *TagLogger) SetLevel(level logrus.Level) {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+	o.globalLevel = level
+}
+
+// SetTagLevel 设置单个 tag 的覆盖级别
+func (o *TagLogger) SetTagLevel(tag string, level logrus.Level) {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+	o.tagLevels[tag] = level
+}
+
+// RemoveTagLevel 移除 tag 的级别覆盖(恢复跟随全局)
+func (o *TagLogger) RemoveTagLevel(tag string) {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+	delete(o.tagLevels, tag)
+}
+
+// GetLevel 获取指定 tag 的有效级别
+func (o *TagLogger) GetLevel(tag string) logrus.Level {
+	o.mu.RLock()
+	defer o.mu.RUnlock()
+	if lv, ok := o.tagLevels[tag]; ok {
+		return lv
+	}
+	return o.globalLevel
+}
+
+// GetGlobalLevel 获取全局默认级别
+func (o *TagLogger) GetGlobalLevel() logrus.Level {
+	o.mu.RLock()
+	defer o.mu.RUnlock()
+	return o.globalLevel
+}
+
+// GetAllTagLevels 获取所有 tag 级别(用于前端展示)
+func (o *TagLogger) GetAllTagLevels() map[string]string {
+	o.mu.RLock()
+	defer o.mu.RUnlock()
+	result := make(map[string]string)
+	for k, v := range o.tagLevels {
+		result[k] = v.String()
+	}
+	return result
+}
+
+// shouldLog 判断指定 tag 和 level 是否应输出
+func (o *TagLogger) shouldLog(tag string, level logrus.Level) bool {
+	o.mu.RLock()
+	defer o.mu.RUnlock()
+	effectiveLevel := o.globalLevel
+	if lv, ok := o.tagLevels[tag]; ok {
+		effectiveLevel = lv
+	}
+	return level <= effectiveLevel
+}
+
+// Log 返回带 tag 字段的 logrus.Entry。注意:此方法不做级别过滤,
+// 需要级别过滤的调用方应使用 Error/Info/Debug 等带级方法或 LogAt。
+func (o *TagLogger) Log(tag string) *logrus.Entry {
+	return logrus.WithField("tag", tag)
+}
+
+// LogAt 指定级别,若级别不足返回 nil
+func (o *TagLogger) LogAt(tag string, level logrus.Level) *logrus.Entry {
+	if !o.shouldLog(tag, level) {
+		return nil
+	}
+	entry := logrus.WithField("tag", tag)
+	return entry
+}
+
+// Debug ...
+func (o *TagLogger) Debug(tag string, args ...interface{}) {
+	if e := o.LogAt(tag, logrus.DebugLevel); e != nil {
+		e.Debug(args...)
+	}
+}
+
+// Info ...
+func (o *TagLogger) Info(tag string, args ...interface{}) {
+	if e := o.LogAt(tag, logrus.InfoLevel); e != nil {
+		e.Info(args...)
+	}
+}
+
+// Warn ...
+func (o *TagLogger) Warn(tag string, args ...interface{}) {
+	if e := o.LogAt(tag, logrus.WarnLevel); e != nil {
+		e.Warn(args...)
+	}
+}
+
+// Error ...
+func (o *TagLogger) Error(tag string, args ...interface{}) {
+	if e := o.LogAt(tag, logrus.ErrorLevel); e != nil {
+		e.Error(args...)
+	}
+}
+
+// Debugf ...
+func (o *TagLogger) Debugf(tag string, format string, args ...interface{}) {
+	if e := o.LogAt(tag, logrus.DebugLevel); e != nil {
+		e.Debugf(format, args...)
+	}
+}
+
+// Infof ...
+func (o *TagLogger) Infof(tag string, format string, args ...interface{}) {
+	if e := o.LogAt(tag, logrus.InfoLevel); e != nil {
+		e.Infof(format, args...)
+	}
+}
+
+// Warnf ...
+func (o *TagLogger) Warnf(tag string, format string, args ...interface{}) {
+	if e := o.LogAt(tag, logrus.WarnLevel); e != nil {
+		e.Warnf(format, args...)
+	}
+}
+
+// Errorf ...
+func (o *TagLogger) Errorf(tag string, format string, args ...interface{}) {
+	if e := o.LogAt(tag, logrus.ErrorLevel); e != nil {
+		e.Errorf(format, args...)
+	}
+}
+
+// SaveToFile 持久化当前配置到文件
+func (o *TagLogger) SaveToFile() error {
+	o.mu.RLock()
+	cfg := LogLevelConfig{
+		Level: o.globalLevel.String(),
+		Tags:  make(map[string]string, len(o.tagLevels)),
+	}
+	for k, v := range o.tagLevels {
+		cfg.Tags[k] = v.String()
+	}
+	o.mu.RUnlock()
+
+	f, err := os.Create(o.persistPath)
+	if err != nil {
+		return err
+	}
+	defer f.Close()
+
+	data, err := json.MarshalIndent(cfg, "", "  ")
+	if err != nil {
+		return err
+	}
+	_, err = f.Write(data)
+	return err
+}
+
+// LoadFromFile 从文件加载配置
+func (o *TagLogger) LoadFromFile() error {
+	data, err := os.ReadFile(o.persistPath)
+	if err != nil {
+		return err
+	}
+	var cfg LogLevelConfig
+	if err := json.Unmarshal(data, &cfg); err != nil {
+		return err
+	}
+
+	o.mu.Lock()
+	defer o.mu.Unlock()
+
+	if lv, err := logrus.ParseLevel(cfg.Level); err == nil {
+		o.globalLevel = lv
+	} else if cfg.Level != "" {
+		logrus.Warnf("TagLogger.LoadFromFile:无效的全局级别'%s',使用默认值", cfg.Level)
+	}
+	for k, v := range cfg.Tags {
+		if lv, err := logrus.ParseLevel(v); err == nil {
+			o.tagLevels[k] = lv
+		} else {
+			logrus.Warnf("TagLogger.LoadFromFile:无效的tag级别'%s=%s',已忽略", k, v)
+		}
+	}
+	return nil
+}

+ 1 - 0
edge/camera/ipc_device.go

@@ -259,6 +259,7 @@ func (o *LcDevice) Handle() {
 		if err := recover(); err != nil {
 			logrus.Errorf("LcDevice.Handle发生异常:%v", err)
 			logrus.Errorf("LcDevice.Handle发生异常,堆栈信息:%s", string(debug.Stack()))
+			time.Sleep(5 * time.Second)
 			go o.Handle()
 		}
 	}()

+ 2 - 3
edge/camera/ipc_media.go

@@ -2,7 +2,6 @@ package main
 
 import (
 	"errors"
-	"io/ioutil"
 	"net/http"
 	"os"
 	"strconv"
@@ -220,7 +219,7 @@ func (o *LcDevice) Snapshot(token string) {
 			fileName, err = o.SnapshotFromRtsp(token)
 		} else {
 			fileName = util.GetPath(4) + o.onvifDev.Code + "_" + strconv.FormatInt(util.MlNow().Unix(), 10) + ".jpg"
-			if err = ioutil.WriteFile(fileName, resp.Body(), os.ModePerm); err != nil {
+			if err = os.WriteFile(fileName, resp.Body(), os.ModePerm); err != nil {
 				logrus.Errorf("存储图片失败:%s", err.Error())
 				return
 			}
@@ -257,7 +256,7 @@ func (o *LcDevice) Snapshot2(file string) error {
 		return errors.New("调用截图接口发生错误")
 	}
 	fileName = util.GetPath(4) + file
-	err = ioutil.WriteFile(fileName, resp.Body(), os.ModePerm)
+	err = os.WriteFile(fileName, resp.Body(), os.ModePerm)
 	if err != nil {
 		return err
 	}

+ 3 - 0
edge/camera/main.go

@@ -58,6 +58,9 @@ func main() {
 		return
 	}
 	gopool = util.NewPool(5)
+	GetMQTTMgr().SetRestartFn(func(fn func(args ...interface{}) interface{}, args ...interface{}) {
+		gopool.Add(fn, args)
+	})
 	gopool.Add(DiscoveryCamera, 1)                //发现摄像头
 	gopool.Add(GetMQTTMgr().MQTTMessageHandle, 2) //发布进程
 	gopool.Add(GetMQTTMgr().MQTTConnectMgr, 3)    //保持连接

+ 37 - 0
edge/camera/mqtt_init.go

@@ -0,0 +1,37 @@
+package main
+
+import (
+	"sync"
+
+	"lc/common/mqtt"
+)
+
+type OptType = mqtt.OptType
+type MQTTMessage = mqtt.MQTTMessage
+
+const (
+	ToAll   = mqtt.ToAll
+	ToCloud = mqtt.ToCloud
+	ToEdge  = mqtt.ToEdge
+)
+
+var _mqttMgrOnce sync.Once
+var _mqttMgr *mqtt.MQTTMgr
+
+func GetMQTTMgr() *mqtt.MQTTMgr {
+	_mqttMgrOnce.Do(func() {
+		_mqttMgr = mqtt.NewMQTTMgr(mqtt.MQTTMgrConfig{
+			CloudServer:   appConfig.Cloud.Mqtt.Server,
+			CloudClientID: appConfig.GID + "@" + appname + appversion,
+			CloudUser:     appConfig.Cloud.Mqtt.User,
+			CloudPassword: appConfig.Cloud.Mqtt.Password,
+			CloudTimeout:  appConfig.Cloud.Mqtt.Timeout,
+			EdgeServer:    appConfig.Edge.Mqtt.Server,
+			EdgeClientID:  appConfig.GID + "@" + appname + appversion,
+			EdgeUser:      appConfig.Edge.Mqtt.User,
+			EdgePassword:  appConfig.Edge.Mqtt.Password,
+			EdgeTimeout:   appConfig.Edge.Mqtt.Timeout,
+		})
+	})
+	return _mqttMgr
+}

+ 0 - 125
edge/camera/mqttclient.go

@@ -1,125 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-)
-
-type BaseMqttOnline interface {
-	GetOnlineMsg() (string, string)
-	GetWillMsg() (string, string)
-}
-
-type EmptyMqttOnline struct {
-}
-
-func (o *EmptyMqttOnline) GetOnlineMsg() (string, string) {
-	return "", ""
-}
-func (o *EmptyMqttOnline) GetWillMsg() (string, string) {
-	return "", ""
-}
-
-type MqttClient struct {
-	mqtt       *mqtt.Client        //
-	mu         sync.Mutex          //保护mapTopics
-	mapTopics  map[string]mqtt.QOS //订阅的主题
-	timeout    uint                //超时时间,毫秒为单位
-	MqttOnline BaseMqttOnline      //是否发布上线消息&遗嘱消息
-}
-
-func NewMqttClient(server, clientid, user, password string, timeout uint, mqttOnline BaseMqttOnline) *MqttClient {
-	o := MqttClient{
-		mapTopics:  make(map[string]mqtt.QOS),
-		timeout:    timeout,
-		MqttOnline: mqttOnline,
-	}
-	client, err := mqtt.NewClient(mqtt.ClientOptions{
-		Servers:       []string{server},
-		ClientID:      clientid,
-		Username:      user,
-		Password:      password,
-		AutoReconnect: true,
-	}, &o)
-	if err != nil {
-		panic(fmt.Sprintf("MQTT错误:", err.Error()))
-		return nil
-	}
-	o.mqtt = client
-	err = client.Connect(o.Ctx())
-	return &o
-}
-
-func (o *MqttClient) ConnectionLostHandler(err error) {
-	logrus.Errorln("MqttClient.ConnectionLostHandler:MQTT连接已经断开,原因:", err)
-}
-
-func (o *MqttClient) OnConnectHandler() {
-	logrus.Infoln("MqttClient.OnConnectHandler:MQTT连接成功")
-	//连接成功则订阅主题
-	for k, v := range o.mapTopics {
-		o.Subscribe(k, v)
-	}
-	topic, str := o.MqttOnline.GetOnlineMsg()
-	if topic != "" {
-		o.PublishString(topic, str, 0)
-	}
-}
-
-func (o *MqttClient) GetWill() (topic string, payload string) {
-	return o.MqttOnline.GetWillMsg()
-}
-
-func (o *MqttClient) Connect() error {
-	if !o.mqtt.IsConnected() {
-		return o.mqtt.Connect(o.Ctx())
-	}
-	return nil
-}
-
-func (o *MqttClient) IsConnected() bool {
-	return o.mqtt.IsConnected()
-}
-
-func (o *MqttClient) Publish(topic string, payload []byte, qos mqtt.QOS) error {
-	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishString(topic string, payload string, qos mqtt.QOS) error {
-	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishJSON(topic string, payload interface{}, qos mqtt.QOS) error {
-	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
-}
-
-func (o *MqttClient) Subscribe(topic string, qos mqtt.QOS) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; !ok {
-		o.mapTopics[topic] = qos
-	}
-	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
-}
-
-func (o *MqttClient) Unsubscribe(topic string) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; ok {
-		delete(o.mapTopics, topic)
-	}
-	return o.mqtt.Unsubscribe(o.Ctx(), topic)
-}
-
-func (o *MqttClient) Handle(topic string, handler mqtt.MessageHandler) mqtt.Route {
-	return o.mqtt.Handle(topic, handler)
-}
-
-func (o *MqttClient) Ctx() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.timeout))
-	return ctx
-}

+ 0 - 187
edge/camera/mqttmgr.go

@@ -1,187 +0,0 @@
-package main
-
-import (
-	"runtime/debug"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-	"lc/common/util"
-)
-
-type OptType uint8
-
-const (
-	ToAll   OptType = 0 //发布和订阅边缘端与云端的消息
-	ToCloud OptType = 1 //发布和订阅云端的消息
-	ToEdge  OptType = 2 //发布和订阅边缘端的消息
-)
-
-var _mqttMgronce sync.Once
-var _mqttMgrsingle *MQTTMgr
-
-// GetMQTTMgr 单态
-func GetMQTTMgr() *MQTTMgr {
-	_mqttMgronce.Do(func() {
-		_mqttMgrsingle = _newMQTTMgr()
-	})
-	return _mqttMgrsingle
-}
-
-type MQTTMgr struct {
-	Cloud *MqttClient
-	Edge  *MqttClient
-	Queue *util.MlQueue
-}
-
-//建两个client
-func _newMQTTMgr() *MQTTMgr {
-	mgr := &MQTTMgr{
-		Queue: util.NewQueue(2000),
-	}
-	if appConfig.Edge.Mqtt.Server != "" {
-		mgr.Edge = NewMqttClient(appConfig.Edge.Mqtt.Server,
-			appConfig.GID+"@"+appname+appversion,
-			appConfig.Edge.Mqtt.User,
-			appConfig.Edge.Mqtt.Password,
-			appConfig.Edge.Mqtt.Timeout,
-			&EmptyMqttOnline{})
-	}
-	if appConfig.Cloud.Mqtt.Server != "" {
-		mgr.Cloud = NewMqttClient(appConfig.Cloud.Mqtt.Server,
-			appConfig.GID+"@"+appname+appversion,
-			appConfig.Cloud.Mqtt.User,
-			appConfig.Cloud.Mqtt.Password,
-			appConfig.Cloud.Mqtt.Timeout,
-			&EmptyMqttOnline{})
-	}
-	return mgr
-}
-
-// Subscribe 定阅
-func (o *MQTTMgr) Subscribe(topic string, qos mqtt.QOS, handler mqtt.MessageHandler, tp OptType) {
-	switch tp {
-	case ToAll:
-		if o.Cloud != nil {
-			o.Cloud.Handle(topic, handler)
-			o.Cloud.Subscribe(topic, qos)
-		}
-		if o.Edge != nil {
-			o.Edge.Handle(topic, handler)
-			o.Edge.Subscribe(topic, qos)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			o.Cloud.Handle(topic, handler)
-			o.Cloud.Subscribe(topic, qos)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.Handle(topic, handler)
-			o.Edge.Subscribe(topic, qos)
-		}
-	}
-}
-
-// UnSubscribe 退定
-func (o *MQTTMgr) UnSubscribe(topic string, tp OptType) {
-	switch tp {
-	case ToAll:
-		if o.Cloud != nil {
-			o.Cloud.Unsubscribe(topic)
-		}
-		if o.Edge != nil {
-			o.Edge.Unsubscribe(topic)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			o.Cloud.Unsubscribe(topic)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.Unsubscribe(topic)
-		}
-	}
-}
-
-// Publish 发布进队列
-func (o *MQTTMgr) Publish(topic string, payload string, qos mqtt.QOS, tp OptType) {
-	msg := MQTTMessage{
-		topic:   topic,
-		payload: payload,
-		qos:     qos,
-		tp:      tp,
-	}
-	o.Queue.Put(&msg)
-}
-
-//发布低
-func (o *MQTTMgr) _publish(msg *MQTTMessage) error {
-	var err error
-	switch msg.tp {
-	case ToAll:
-		if o.Cloud != nil {
-			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-		if o.Edge != nil {
-			o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	}
-	return err
-}
-
-// MQTTConnectMgr 连接保持
-func (o *MQTTMgr) MQTTConnectMgr(args ...interface{}) interface{} {
-	for {
-		time.Sleep(10 * time.Second)
-		if o.Cloud != nil {
-			o.Cloud.Connect()
-		}
-		if o.Edge != nil {
-			o.Edge.Connect()
-		}
-	}
-}
-
-func (o *MQTTMgr) MQTTMessageHandle(args ...interface{}) interface{} {
-	defer func() {
-		if err := recover(); err != nil {
-			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常:%v", err)
-			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常,堆栈信息:%s", string(debug.Stack()))
-			gopool.Add(o.MQTTMessageHandle, args)
-		}
-	}()
-	var err error
-	for { //队列中所有发布
-		if m, ok, _ := o.Queue.Get(); ok {
-			if msg, ok := m.(*MQTTMessage); ok {
-			RETRY:
-				err = o._publish(msg)
-				if err != nil {
-					logrus.Errorf("发布主题为%s的消息失败,原因:%s", msg.topic, err.Error())
-					time.Sleep(time.Second)
-					goto RETRY
-				}
-			}
-		} else {
-			time.Sleep(200 * time.Millisecond)
-		}
-	}
-}
-
-type MQTTMessage struct {
-	topic   string
-	payload string
-	qos     mqtt.QOS
-	tp      OptType
-}

+ 12 - 13
edge/ipole/auto_reload.go

@@ -7,7 +7,6 @@ import (
 	"time"
 
 	"github.com/radovskyb/watcher"
-	"github.com/sirupsen/logrus"
 
 	"lc/common/util"
 )
@@ -29,7 +28,7 @@ func WatchDevConfig(args ...interface{}) interface{} {
 						case watcher.Create, watcher.Write:
 							rtuArr, err := LoadDev(uint8(code))
 							if err != nil {
-								logrus.Errorf("加载RTU文件[%s]失败:%s", strconv.Itoa(code)+".json", err.Error())
+								util.GetTagLog().Errorf("sys", "加载RTU文件[%s]失败:%s", strconv.Itoa(code)+".json", err.Error())
 							} else {
 								GetDeviceMgr().UpdateDevices(uint8(code), rtuArr)
 							}
@@ -40,7 +39,7 @@ func WatchDevConfig(args ...interface{}) interface{} {
 					}
 				}
 			case err := <-w.Error:
-				logrus.Errorf("WatchDevConfig:发生错误:%s", err.Error())
+				util.GetTagLog().Errorf("sys", "WatchDevConfig:发生错误:%s", err.Error())
 			case <-w.Closed:
 				return
 			}
@@ -48,10 +47,10 @@ func WatchDevConfig(args ...interface{}) interface{} {
 	}()
 
 	if err := w.Add(util.GetPath(1)); err != nil {
-		logrus.Errorf("WatchDevConfig:Add(conf)发生错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "WatchDevConfig:Add(conf)发生错误:%s", err.Error())
 	}
 	if err := w.Start(time.Second); err != nil {
-		logrus.Errorf("WatchDevConfig:Start发生错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "WatchDevConfig:Start发生错误:%s", err.Error())
 	}
 	return 0
 }
@@ -81,17 +80,17 @@ func WatchModelConfig(args ...interface{}) interface{} {
 					}
 				}
 			case err := <-w.Error:
-				logrus.Errorf("WatchModelConfig:发生错误:%s", err.Error())
+				util.GetTagLog().Errorf("sys", "WatchModelConfig:发生错误:%s", err.Error())
 			case <-w.Closed:
 				return
 			}
 		}
 	}()
 	if err := w.Add(util.GetPath(2)); err != nil {
-		logrus.Errorf("WatchModelConfig:Add(conf)发生错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "WatchModelConfig:Add(conf)发生错误:%s", err.Error())
 	}
 	if err := w.Start(time.Second); err != nil {
-		logrus.Errorf("WatchModelConfig:Start发生错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "WatchModelConfig:Start发生错误:%s", err.Error())
 	}
 	return 0
 }
@@ -117,31 +116,31 @@ func WatchConfConfig(args ...interface{}) interface{} {
 					}
 				}
 			case err := <-w.Error:
-				logrus.Errorf("WatchConfConfig:发生错误:%s", err.Error())
+				util.GetTagLog().Errorf("sys", "WatchConfConfig:发生错误:%s", err.Error())
 			case <-w.Closed:
 				return
 			}
 		}
 	}()
 	if err := w.Add(util.GetPath(0)); err != nil {
-		logrus.Errorf("WatchConfConfig:Add(conf)发生错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "WatchConfConfig:Add(conf)发生错误:%s", err.Error())
 	}
 	if err := w.Start(time.Second); err != nil {
-		logrus.Errorf("WatchConfConfig:Start发生错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "WatchConfConfig:Start发生错误:%s", err.Error())
 	}
 	return 0
 }
 
 func UpdateAppConfig() {
 	if err := loadAppConfig(); err != nil {
-		logrus.Errorf("UpdateAppConfig:重新加载app.json文件失败:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "UpdateAppConfig:重新加载app.json文件失败:%s", err.Error())
 		return
 	}
 }
 
 func UpdateSerialConfig() {
 	if err := loadSerialConfig(); err != nil {
-		logrus.Errorf("UpdateSerialConfig:重新加载serial.json文件失败:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "UpdateSerialConfig:重新加载serial.json文件失败:%s", err.Error())
 		return
 	}
 	GetSerialMgr().AddSerialPorts(serialConfig.Serial)

+ 114 - 0
edge/ipole/cleanlog.go

@@ -0,0 +1,114 @@
+package main
+
+import (
+	"os"
+	"path/filepath"
+	"sort"
+	"strings"
+	"time"
+
+	"lc/common/util"
+)
+
+const (
+	logRetentionDays = 7
+	logMaxTotalSize  = 100 * 1024 * 1024 // 100MB
+)
+
+type logFileInfo struct {
+	name    string
+	size    int64
+	modTime time.Time
+}
+
+// removeLogFile 删除单个日志文件并返回释放的字节数
+func removeLogFile(dir string, f logFileInfo) (bool, int64) {
+	path := filepath.Join(dir, f.name)
+	if err := os.Remove(path); err != nil {
+		util.GetTagLog().Warnf("sys", "CleanLogs:删除失败:%s,err=%v", f.name, err)
+		return false, 0
+	}
+	return true, f.size
+}
+
+// CleanLogs 定时清理日志文件:保留7天,总大小不超过100MB
+func CleanLogs(args ...interface{}) interface{} {
+	for {
+		time.Sleep(1 * time.Hour)
+
+		dir := util.GetPath(3) // {workdir}/log/
+		entries, err := os.ReadDir(dir)
+		if err != nil {
+			util.GetTagLog().Warnf("sys", "CleanLogs:读取日志目录失败:%s", err)
+			continue
+		}
+
+		var logFiles []logFileInfo
+		var totalSize int64
+		now := time.Now()
+
+		for _, e := range entries {
+			if e.IsDir() || !strings.HasSuffix(e.Name(), ".log") {
+				continue
+			}
+			info, err := e.Info()
+			if err != nil {
+				continue
+			}
+			lf := logFileInfo{
+				name:    e.Name(),
+				size:    info.Size(),
+				modTime: info.ModTime(),
+			}
+			logFiles = append(logFiles, lf)
+			totalSize += lf.size
+		}
+
+		if len(logFiles) == 0 {
+			continue
+		}
+
+		// 按修改时间升序排列(最旧的在前)
+		sort.Slice(logFiles, func(i, j int) bool {
+			return logFiles[i].modTime.Before(logFiles[j].modTime)
+		})
+
+		var deleted int
+		var freed int64
+
+		// Step 1: 删除超过7天的文件
+		cutoff := now.AddDate(0, 0, -logRetentionDays)
+		remaining := make([]logFileInfo, 0, len(logFiles))
+		for _, f := range logFiles {
+			if f.modTime.Before(cutoff) {
+				if ok, sz := removeLogFile(dir, f); ok {
+					deleted++
+					freed += sz
+					totalSize -= sz
+				}
+			} else {
+				remaining = append(remaining, f)
+			}
+		}
+
+		// Step 2: 若剩余总大小超过100MB,从最旧的文件开始删除(索引遍历避免切片复制)
+		idx := 0
+		for totalSize > logMaxTotalSize && idx < len(remaining) {
+			if ok, sz := removeLogFile(dir, remaining[idx]); ok {
+				deleted++
+				freed += sz
+				totalSize -= sz
+			}
+			idx++
+		}
+
+		if deleted > 0 {
+			kept := len(remaining) - idx
+			if kept < 0 {
+				kept = 0
+			}
+			util.GetTagLog().Infof("sys", "CleanLogs:清理完成,删除%d个文件,释放%.1fKB,剩余%d个文件,总大小%.1fKB",
+				deleted, float64(freed)/1024, kept, float64(totalSize)/1024)
+		}
+	}
+}

+ 44 - 44
edge/ipole/concentrator.go

@@ -11,7 +11,6 @@ import (
 	"time"
 
 	"github.com/go-redis/redis/v7"
-	"github.com/sirupsen/logrus"
 	"github.com/valyala/bytebufferpool"
 
 	"lc/common/mqtt"
@@ -185,20 +184,20 @@ func (o *Concentrator) UpdateModel2(mi *ModelInfo) {
 		return
 	}
 	if mi.Flag == 0 {
-		logrus.Errorf("Concentrator.UpdateModel2:设备[%s]的物模型[tid=%d]模型文件被删除,下次启动即将生效。", o.devinfo.DevCode, mi.TID)
+		util.GetTagLog().Errorf("concentrator", "Concentrator.UpdateModel2:设备[%s]的物模型[tid=%d]模型文件被删除,下次启动即将生效。", o.devinfo.DevCode, mi.TID)
 		return
 	}
-	logrus.Debugf("Concentrator.UpdateModel2:更新设备[%s]的物模型[%d]", o.devinfo.DevCode, mi.TID)
+	util.GetTagLog().Debugf("concentrator", "Concentrator.UpdateModel2:更新设备[%s]的物模型[%d]", o.devinfo.DevCode, mi.TID)
 	iot, err := loadModel(mi.TID)
 	if err != nil {
-		logrus.Errorf("Concentrator.UpdateModel2:加载模型[%d]文件错误:%s", mi.TID, err.Error())
+		util.GetTagLog().Errorf("concentrator", "Concentrator.UpdateModel2:加载模型[%d]文件错误:%s", mi.TID, err.Error())
 		return
 	}
 	if iot.Protocol == ConcentratorProtocol { //合法的物模型
 		o.model = iot
-		logrus.Infof("Concentrator.UpdateModel2:更新设备[%s]的物模型[%d]成功", o.devinfo.DevCode, mi.TID)
+		util.GetTagLog().Infof("concentrator", "Concentrator.UpdateModel2:更新设备[%s]的物模型[%d]成功", o.devinfo.DevCode, mi.TID)
 	} else {
-		logrus.Error("Concentrator.UpdateModel2:物模型错误,TID和文件名tid不一致或协议非ModbusRTU协议")
+		util.GetTagLog().Error("concentrator", "Concentrator.UpdateModel2:物模型错误,TID和文件名tid不一致或协议非ModbusRTU协议")
 	}
 }
 
@@ -208,7 +207,7 @@ func (o *Concentrator) ReloadOOTFromRedis() error {
 		if err == redis.Nil {
 			return nil
 		}
-		logrus.Errorf("Concentrator.ReloadOOTFromRedis设备[%s]从redis加载时间策略失败:%s", o.devinfo.DevCode, err.Error())
+		util.GetTagLog().Errorf("concentrator", "Concentrator.ReloadOOTFromRedis设备[%s]从redis加载时间策略失败:%s", o.devinfo.DevCode, err.Error())
 		return err
 	}
 	for k, v := range mapdata {
@@ -227,7 +226,7 @@ func (o *Concentrator) ReloadSwitchOOTFromRedis() error {
 		if err == redis.Nil {
 			return nil
 		}
-		logrus.Errorf("Concentrator.ReloadSwitchOOTFromRedis设备[%s]从redis加载时间策略失败:%s", o.devinfo.DevCode, err.Error())
+		util.GetTagLog().Errorf("concentrator", "Concentrator.ReloadSwitchOOTFromRedis设备[%s]从redis加载时间策略失败:%s", o.devinfo.DevCode, err.Error())
 		return err
 	}
 	for k, v := range mapdata {
@@ -246,7 +245,7 @@ func (o *Concentrator) ReloadBroadCastFromRedis() error {
 		if err == redis.Nil {
 			return nil
 		}
-		logrus.Errorf("Concentrator.ReloadBroadCastFromRedis设备[%s]从redis加载广播恢复截止时间失败:%s", o.devinfo.DevCode, err.Error())
+		util.GetTagLog().Errorf("concentrator", "Concentrator.ReloadBroadCastFromRedis设备[%s]从redis加载广播恢复截止时间失败:%s", o.devinfo.DevCode, err.Error())
 		return err
 	}
 	if t, err := util.MlParseTime(strTime); err == nil {
@@ -261,7 +260,7 @@ func (o *Concentrator) ReloadLampAlarmFromRedis() error {
 		if err == redis.Nil {
 			return nil
 		}
-		logrus.Errorf("Concentrator.ReloadLampAlarmFromRedis设备[%s]从redis加载广播恢复截止时间失败:%s", o.devinfo.DevCode, err.Error())
+		util.GetTagLog().Errorf("concentrator", "Concentrator.ReloadLampAlarmFromRedis设备[%s]从redis加载广播恢复截止时间失败:%s", o.devinfo.DevCode, err.Error())
 		return err
 	}
 	for k, v := range mapAlarm {
@@ -269,7 +268,7 @@ func (o *Concentrator) ReloadLampAlarmFromRedis() error {
 		if err := json.UnmarshalFromString(v, &lai); err == nil {
 			o.mapLampAlarm[k] = &lai
 		} else {
-			logrus.Errorf("从redis获取的告警信息还原失败,原内容:%s,失败原因:%s", v, err.Error())
+			util.GetTagLog().Errorf("concentrator", "从redis获取的告警信息还原失败,原内容:%s,失败原因:%s", v, err.Error())
 		}
 	}
 	return nil
@@ -278,8 +277,9 @@ func (o *Concentrator) ReloadLampAlarmFromRedis() error {
 func (o *Concentrator) Handle() {
 	defer func() {
 		if err := recover(); err != nil {
-			logrus.Errorf("Concentrator.Handle发生异常:%v", err)
-			logrus.Errorf("Concentrator.Handle发生异常,堆栈信息:%s", string(debug.Stack()))
+			util.GetTagLog().Errorf("concentrator", "Concentrator.Handle发生异常:%v", err)
+			util.GetTagLog().Errorf("concentrator", "Concentrator.Handle发生异常,堆栈信息:%s", string(debug.Stack()))
+			time.Sleep(5 * time.Second)
 			go o.Handle()
 		}
 	}()
@@ -294,7 +294,7 @@ func (o *Concentrator) Handle() {
 	for {
 		select {
 		case <-o.ctx.Done():
-			logrus.Errorf("设备[%s]的HandlePole退出,原因:%v", o.devinfo.DevCode, o.ctx.Err())
+			util.GetTagLog().Errorf("concentrator", "设备[%s]的HandlePole退出,原因:%v", o.devinfo.DevCode, o.ctx.Err())
 			exit = true
 		case devinfo_ := <-o.chanDevInfo:
 			o.devinfo = devinfo_
@@ -307,7 +307,7 @@ func (o *Concentrator) Handle() {
 					if fn, ok := o.mapTopicHandle[mm.Topic()]; ok {
 						fn(mm)
 					} else {
-						logrus.Errorf("Concentrator.Handle:不支持的主题:%s", mm.Topic())
+						util.GetTagLog().Errorf("concentrator", "Concentrator.Handle:不支持的主题:%s", mm.Topic())
 					}
 				}
 			} else {
@@ -349,14 +349,14 @@ func (o *Concentrator) CheckRecoveryAuto(force bool) {
 			delete(o.mapTempLampsOOT, k)
 		}
 		if err := o.BroadcastAuto(); err != nil {
-			logrus.Errorf("广播恢复时控失败:%s", err.Error())
+			util.GetTagLog().Errorf("concentrator", "广播恢复时控失败:%s", err.Error())
 		} else {
 			o.broadcastAutoTime = time.Time{}
 			//删除redis中所有临时开关灯记录
 			if err := redisEdgeData.Del(LampSwitchPrefix + o.devinfo.DevCode).Err(); err != nil {
-				logrus.Errorf("手动广播恢复时控,更新redis失败:%s", err.Error())
+				util.GetTagLog().Errorf("concentrator", "手动广播恢复时控,更新redis失败:%s", err.Error())
 			}
-			logrus.Info("广播恢复时控成功")
+			util.GetTagLog().Info("concentrator", "广播恢复时控成功")
 		}
 	} else {
 		//如果广播控模式未过期,则判断单灯是否手动控过期,过期则恢复
@@ -364,9 +364,9 @@ func (o *Concentrator) CheckRecoveryAuto(force bool) {
 		for k, v := range o.mapTempLampsOOT {
 			if time.Time(v.End).Before(util.MlNow()) {
 				if err := o.SetPoleAuto(k); err != nil {
-					logrus.Errorf("单灯[%d]恢复时控失败:%s", k, err.Error())
+					util.GetTagLog().Errorf("concentrator", "单灯[%d]恢复时控失败:%s", k, err.Error())
 				} else {
-					logrus.Infof("单灯[%d]恢复时控成功", k)
+					util.GetTagLog().Infof("concentrator", "单灯[%d]恢复时控成功", k)
 					strList = append(strList, strconv.Itoa(int(k)))
 					delete(o.mapTempLampsOOT, k)
 				}
@@ -374,7 +374,7 @@ func (o *Concentrator) CheckRecoveryAuto(force bool) {
 		}
 		if len(strList) > 0 {
 			if err := redisEdgeData.HDel(LampSwitchPrefix+o.devinfo.DevCode, strList...).Err(); err != nil {
-				logrus.Errorf("手动恢复灯控[%v]时控模式,更新redis失败:%s", strList, err.Error())
+				util.GetTagLog().Errorf("concentrator", "手动恢复灯控[%v]时控模式,更新redis失败:%s", strList, err.Error())
 			}
 		}
 	}
@@ -471,7 +471,7 @@ func (o *Concentrator) UploadLampAlarm() {
 			mapRedis := make(map[string]interface{})
 			mapRedis[k] = strAlarm
 			if err := redisEdgeData.HSet(LampAlarmPrefix+o.devinfo.DevCode, mapRedis).Err(); err != nil {
-				logrus.Errorf("告警信息缓存入redis失败:%s", err.Error())
+				util.GetTagLog().Errorf("concentrator", "告警信息缓存入redis失败:%s", err.Error())
 			}
 
 		} else if v.Alarm.EndTime != "" { //告警结束上报
@@ -485,7 +485,7 @@ func (o *Concentrator) UploadLampAlarm() {
 	}
 	if len(toDelete) > 0 {
 		if err := redisEdgeData.HDel(LampAlarmPrefix+o.devinfo.DevCode, toDelete...).Err(); err != nil {
-			logrus.Errorf("告警信息从redis删除失败:%s", err.Error())
+			util.GetTagLog().Errorf("concentrator", "告警信息从redis删除失败:%s", err.Error())
 		}
 	}
 }
@@ -543,14 +543,14 @@ func (o *Concentrator) CheckLampAlarm(lnd LampNumberDID, b1, b2 uint8) {
 		if except == protocol.LE_OK { //告警结束
 			a.Alarm.EndTime = now.Format("2006-01-02 15:04:05")
 			a.Alarm.Brightness = b1
-			logrus.Debugf("灯控[%s]状态[%s]", lnd.DID, ConvertExcept(except))
+			util.GetTagLog().Debugf("concentrator", "灯控[%s]状态[%s]", lnd.DID, ConvertExcept(except))
 		}
 	} else {
 		if except != protocol.LE_OK { //告警开始
 			a := protocol.LampAlarm{DID: lnd.DID, AlarmType: except, AlarmBrightness: b1, StartTime: now.Format("2006-01-02 15:04:05")}
 			lai := LampAlarmInfo{Alarm: &a, Send: false}
 			o.mapLampAlarm[lnd.DID] = &lai
-			logrus.Debugf("灯控[%s]状态[%s]", lnd.DID, ConvertExcept(except))
+			util.GetTagLog().Debugf("concentrator", "灯控[%s]状态[%s]", lnd.DID, ConvertExcept(except))
 		}
 	}
 }
@@ -702,7 +702,7 @@ func (o *Concentrator) GetOnOffTime(PoleID uint32, Cmd uint8) ([]zigbee.OnOffTim
 		}
 		return ret, nil
 	} else {
-		logrus.Errorf("读取开关灯时间返回的内容错误:%s", hex.EncodeToString(recvdata))
+		util.GetTagLog().Errorf("concentrator", "读取开关灯时间返回的内容错误:%s", hex.EncodeToString(recvdata))
 	}
 	return nil, errors.New("读取开关灯时间返回的内容错误")
 }
@@ -731,7 +731,7 @@ func (o *Concentrator) ReadPoleTime(PoleID uint32) (uint8, uint8, uint8, error)
 	if len(pgfcresp.Data) >= 5 {
 		return pgfcresp.Data[2], pgfcresp.Data[3], pgfcresp.Data[4], nil
 	} else {
-		logrus.Errorf("读取单灯时间返回的内容错误:%s", hex.EncodeToString(recvdata))
+		util.GetTagLog().Errorf("concentrator", "读取单灯时间返回的内容错误:%s", hex.EncodeToString(recvdata))
 	}
 	return 0, 0, 0, errors.New("读取单灯时间返回的内容错误")
 }
@@ -773,7 +773,7 @@ func (o *Concentrator) ReadElectricalPara(PoleID uint32) (*ElecPara, error) {
 		ep.Degree[1] = binary.BigEndian.Uint16(pgfcresp.Data[11:13])
 		return &ep, nil
 	} else {
-		logrus.Errorf("读取电流电压电度返回的内容错误:%s", hex.EncodeToString(recvdata))
+		util.GetTagLog().Errorf("concentrator", "读取电流电压电度返回的内容错误:%s", hex.EncodeToString(recvdata))
 	}
 	return nil, errors.New("读取电流电压电度返回错误")
 }
@@ -818,7 +818,7 @@ func (o *Concentrator) GetBrightness(PoleID uint32) (uint8, uint8, error) {
 	if pgfcresp.Cmd == zigbee.CmdReadBrightness && len(pgfcresp.Data) >= 4 {
 		return pgfcresp.Data[2], pgfcresp.Data[3], nil
 	} else {
-		logrus.Errorf("查询亮度返回的内容错误:%s", hex.EncodeToString(recvdata))
+		util.GetTagLog().Errorf("concentrator", "查询亮度返回的内容错误:%s", hex.EncodeToString(recvdata))
 	}
 	return 0, 0, errors.New("查询亮度返回错误")
 }
@@ -860,7 +860,7 @@ func (o *Concentrator) HandleTpChzbSetBroadcasttime(m mqtt.Message) {
 	var ret protocol.Pack_Ack
 	var err error
 	if err = obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("concentrator", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	err = o.BroadcastTime()
@@ -874,7 +874,7 @@ func (o *Concentrator) HandleTpChzbSetWaittime(m mqtt.Message) {
 	var ret protocol.Pack_Ack
 	var err error
 	if err = obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("concentrator", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	if obj.Data.Waittime < 1000 || obj.Data.Waittime > 15000 {
@@ -893,7 +893,7 @@ func (o *Concentrator) HandleTpChzbSetSwitch(m mqtt.Message) {
 	var err error
 	mapIpole := make(map[uint32]*protocol.StateError)
 	if err = obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("concentrator", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	if obj.Id != o.devinfo.DevCode {
@@ -930,7 +930,7 @@ func (o *Concentrator) HandleTpChzbSetSwitch(m mqtt.Message) {
 		}
 	}
 	if err := redisEdgeData.HSet(LampSwitchPrefix+o.devinfo.DevCode, mapRedisTempLampsOOT).Err(); err != nil {
-		logrus.Errorf("手动开关灯时间设置[内容:%v]缓存到redis失败:%s", mapRedisTempLampsOOT, err.Error())
+		util.GetTagLog().Errorf("concentrator", "手动开关灯时间设置[内容:%v]缓存到redis失败:%s", mapRedisTempLampsOOT, err.Error())
 	}
 	if str, err := ret.EnCode(o.devinfo.DevCode, appConfig.GID, obj.Seq, mapIpole); err == nil {
 		GetMQTTMgr().Publish(GetTopic(protocol.DT_CONCENTRATOR, o.devinfo.DevCode, protocol.TP_CHZB_SET_SWITCH_ACK), str, mqtt.AtMostOnce, ToAll)
@@ -942,7 +942,7 @@ func (o *Concentrator) HandleTpChzbSetRecoveryAuto(m mqtt.Message) {
 	var err error
 	mapIpole := make(map[uint32]*protocol.StateError)
 	if err = obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("concentrator", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	if obj.Id != o.devinfo.DevCode {
@@ -958,12 +958,12 @@ func (o *Concentrator) HandleTpChzbSetRecoveryAuto(m mqtt.Message) {
 				delete(o.mapTempLampsOOT, v)
 				strList = append(strList, strconv.Itoa(int(v)))
 			} else {
-				logrus.Errorf("手动单灯[%d]恢复时控失败:%s", v, err.Error())
+				util.GetTagLog().Errorf("concentrator", "手动单灯[%d]恢复时控失败:%s", v, err.Error())
 			}
 			mapIpole[v] = protocol.NewStateError(err)
 		}
 		if err := redisEdgeData.HDel(LampSwitchPrefix+o.devinfo.DevCode, strList...).Err(); err != nil {
-			logrus.Errorf("手动恢复灯控[%v]时控模式,更新redis失败:%s", strList, err.Error())
+			util.GetTagLog().Errorf("concentrator", "手动恢复灯控[%v]时控模式,更新redis失败:%s", strList, err.Error())
 		}
 	}
 
@@ -978,14 +978,14 @@ func (o *Concentrator) HandleTpChzbSetOnofftime(m mqtt.Message) {
 	var err error
 	mapIpole := make(map[uint32]*protocol.StateError)
 	if err = obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("concentrator", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	if obj.Id != o.devinfo.DevCode {
 		return
 	}
 	if len(obj.Data.LampIDs) == 0 || len(obj.Data.OnOffTime) == 0 {
-		logrus.Errorf("Handle_TP_CHZB_SET_ONOFFTIME:错误,灯控编号[%v],时间段个数:%v",
+		util.GetTagLog().Errorf("concentrator", "Handle_TP_CHZB_SET_ONOFFTIME:错误,灯控编号[%v],时间段个数:%v",
 			obj.Data.LampIDs, obj.Data.OnOffTime)
 		return
 	}
@@ -1007,9 +1007,9 @@ func (o *Concentrator) HandleTpChzbSetOnofftime(m mqtt.Message) {
 			continue
 		}
 		if err = o.SetOnOffTime(v, zigbee.CmdSetOnofftime, data); err != nil {
-			logrus.Errorf("单灯[%d]设置开关灯时间失败:%s", v, err.Error())
+			util.GetTagLog().Errorf("concentrator", "单灯[%d]设置开关灯时间失败:%s", v, err.Error())
 		} else {
-			logrus.Infof("单灯[%d]设置开关灯时间成功", v)
+			util.GetTagLog().Infof("concentrator", "单灯[%d]设置开关灯时间成功", v)
 		}
 		mapIpole[v] = protocol.NewStateError(err)
 		mapRedisOOT[strconv.Itoa(int(v))] = datastr //缓存到redis
@@ -1017,7 +1017,7 @@ func (o *Concentrator) HandleTpChzbSetOnofftime(m mqtt.Message) {
 	}
 	//持久缓存到redis,以便于重启后读取进内存中
 	if err := redisEdgeData.HSet(LampOotPrefix+o.devinfo.DevCode, mapRedisOOT).Err(); err != nil {
-		logrus.Errorf("灯控时间设置[内容:%v]缓存到redis失败:%s", mapRedisOOT, err.Error())
+		util.GetTagLog().Errorf("concentrator", "灯控时间设置[内容:%v]缓存到redis失败:%s", mapRedisOOT, err.Error())
 	}
 	if str, err := ret.EnCode(o.devinfo.DevCode, appConfig.GID, obj.Seq, mapIpole); err == nil {
 		GetMQTTMgr().Publish(GetTopic(protocol.DT_CONCENTRATOR, o.devinfo.DevCode, protocol.TP_CHZB_SET_ONOFFTIME_ACK), str, mqtt.AtMostOnce, ToCloud)
@@ -1029,7 +1029,7 @@ func (o *Concentrator) HandleTpChzbQueryOnofftime(m mqtt.Message) {
 	var oot []zigbee.OnOffTime
 	var err error
 	if err = obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("concentrator", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	if obj.Id != o.devinfo.DevCode {
@@ -1060,7 +1060,7 @@ func (o *Concentrator) HandleTpChzbSetUpdateLamp(m mqtt.Message) {
 	var ret protocol.Pack_Ack
 	var err error
 	if err = obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("concentrator", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	if obj.Id != o.devinfo.DevCode {
@@ -1085,7 +1085,7 @@ func (o *Concentrator) HandleTpChzbQueryTime(m mqtt.Message) {
 	var obj protocol.Pack_CHZB_QueryTime
 	var ret protocol.Pack_CHZB_QueryTimeAck
 	if err := obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("concentrator", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	var pt *protocol.CHZB_LampTime = nil

+ 2 - 0
edge/ipole/conf/app.json

@@ -4,6 +4,8 @@
   "GID":"T184136721",
   "SN":"202012302040",
   "Upgrade":true,
+  "UpdateUrl":"http://106.52.134.22:8180/",
+  "NtpServer":"106.52.134.22",
   "Edge":{
     "mqtt":{
       "server":"tcp://127.0.0.1:1883",

+ 2 - 3
edge/ipole/config.go

@@ -2,7 +2,6 @@ package main
 
 import (
 	"errors"
-	"github.com/sirupsen/logrus"
 	"strconv"
 
 	"lc/common/configor"
@@ -35,7 +34,7 @@ func loadSerialConfig() error {
 	return err
 }
 
-//加载模型文件
+// 加载模型文件
 func loadModel(tid uint16) (*protocol.IotModel, error) {
 	var o protocol.IotModel
 	var fname = util.GetPath(2) + strconv.Itoa(int(tid)) + ".json"
@@ -54,7 +53,7 @@ func LoadDev(code uint8) ([]protocol.DevInfo, error) {
 	var arrDev protocol.MapDevConfig
 	err := configor.Load(&arrDev, util.GetPath(1)+strconv.Itoa(int(code))+".json")
 	if err != nil {
-		logrus.Errorf("LoadDev err = :%s", err.Error())
+		util.GetTagLog().Errorf("sys", "LoadDev err = :%s", err.Error())
 		return nil, err
 
 	}

+ 239 - 0
edge/ipole/deploy.html

@@ -0,0 +1,239 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>网关部署 - IPole</title>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#f5f7fa;color:#333;min-height:100vh}
+.header{background:#1a73e8;color:#fff;padding:16px 24px;font-size:18px;font-weight:600}
+.container{max-width:640px;margin:24px auto;padding:0 16px}
+.card{background:#fff;border-radius:8px;padding:24px;margin-bottom:16px;box-shadow:0 1px 3px rgba(0,0,0,.1)}
+.card h2{font-size:16px;margin-bottom:16px}
+.info-row{display:flex;justify-content:space-between;padding:8px 0;border-bottom:1px solid #eee;font-size:14px}
+.info-row:last-child{border-bottom:none}
+.info-label{color:#888}
+.upload-area{border:2px dashed #ccc;border-radius:8px;padding:40px;text-align:center;cursor:pointer;transition:border-color .2s}
+.upload-area:hover,.upload-area.dragover{border-color:#1a73e8}
+.upload-area p{color:#888;margin-top:8px;font-size:13px}
+.progress-bar{height:8px;background:#e8eaed;border-radius:4px;overflow:hidden;margin-top:12px}
+.progress-fill{height:100%;background:#1a73e8;transition:width .3s;width:0}
+.btn{display:inline-block;padding:10px 24px;background:#1a73e8;color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:14px}
+.btn:disabled{opacity:.5;cursor:not-allowed}
+.check-item{display:flex;align-items:center;padding:8px 0;border-bottom:1px solid #eee;font-size:14px}
+.check-item:last-child{border-bottom:none}
+.check-icon{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;margin-right:12px;font-size:12px;flex-shrink:0}
+.check-ok{background:#e6f4ea;color:#1e8e3e}
+.check-fail{background:#fce8e6;color:#d93025}
+.check-pending{background:#e8eaed;color:#888}
+.check-detail{color:#888;font-size:12px;margin-left:auto}
+.status-msg{padding:12px;border-radius:4px;text-align:center;font-size:14px;margin-top:12px}
+.status-ok{background:#e6f4ea;color:#1e8e3e}
+.status-fail{background:#fce8e6;color:#d93025}
+.status-progress{background:#e8f0fe;color:#1a73e8}
+.hidden{display:none}
+.error-msg{color:#d93025;font-size:13px;margin-top:4px}
+</style>
+</head>
+<body>
+<div class="header">网关部署工具</div>
+<div class="container">
+  <div class="card" id="infoCard">
+    <h2>系统信息</h2>
+    <div class="info-row"><span class="info-label">当前版本</span><span id="currentVersion">--</span></div>
+    <div class="info-row"><span class="info-label">上次部署</span><span id="lastDeploy">--</span></div>
+    <div class="info-row"><span class="info-label">上次结果</span><span id="lastResult">--</span></div>
+  </div>
+
+  <div class="card" id="uploadCard">
+    <h2>上传新版本</h2>
+    <div class="upload-area" id="dropZone">
+      <div style="font-size:36px">+</div>
+      <p>拖拽或点击上传 ipole 可执行文件</p>
+      <input type="file" id="fileInput" style="display:none">
+    </div>
+    <div id="fileInfo" class="hidden" style="margin-top:12px">
+      <div class="info-row"><span class="info-label">文件名</span><span id="fileName">--</span></div>
+      <div class="info-row"><span class="info-label">大小</span><span id="fileSize">--</span></div>
+    </div>
+    <div id="uploadProgress" class="hidden">
+      <div class="progress-bar"><div class="progress-fill" id="progressFill"></div></div>
+      <p style="font-size:12px;color:#888;margin-top:4px" id="progressText">0%</p>
+    </div>
+    <button class="btn" id="deployBtn" style="margin-top:12px;width:100%" disabled>开始部署</button>
+    <p id="uploadError" class="error-msg hidden"></p>
+  </div>
+
+  <div class="card hidden" id="statusCard">
+    <h2 id="statusTitle">部署状态</h2>
+    <div id="statusMsg" class="status-msg status-progress">正在部署...</div>
+    <div id="checkList"></div>
+  </div>
+
+  <div class="card hidden" id="reconnectCard">
+    <div class="status-msg status-progress">等待网关重启...<br><span style="font-size:12px">正在自动重连(每2秒)</span></div>
+  </div>
+</div>
+
+<script>
+var selectedFile = null;
+var dropZone = document.getElementById('dropZone');
+var fileInput = document.getElementById('fileInput');
+var checkNames = {mqtt:'MQTT连接', redis:'Redis', serial:'串口', modbus:'设备采集', device_online:'设备在线'};
+
+// 加载系统信息
+fetch('/deploy/result').then(function(r){return r.json()}).then(function(data){
+  if (data.version) {
+    document.getElementById('currentVersion').textContent = data.version;
+    var d = new Date(data.timestamp * 1000);
+    document.getElementById('lastDeploy').textContent = d.toLocaleString();
+    var ok = data.phase1 && data.phase1.success;
+    document.getElementById('lastResult').textContent = ok ? '成功' : '失败';
+    if (ok) document.getElementById('lastResult').style.color = '#1e8e3e';
+    else document.getElementById('lastResult').style.color = '#d93025';
+  }
+}).catch(function(){});
+
+// 文件选择
+dropZone.addEventListener('click', function(){ fileInput.click(); });
+dropZone.addEventListener('dragover', function(e){ e.preventDefault(); dropZone.classList.add('dragover'); });
+dropZone.addEventListener('dragleave', function(){ dropZone.classList.remove('dragover'); });
+dropZone.addEventListener('drop', function(e){
+  e.preventDefault();
+  dropZone.classList.remove('dragover');
+  handleFile(e.dataTransfer.files[0]);
+});
+fileInput.addEventListener('change', function(){ handleFile(fileInput.files[0]); });
+
+function handleFile(file) {
+  if (!file) return;
+  selectedFile = file;
+  document.getElementById('fileName').textContent = file.name;
+  document.getElementById('fileSize').textContent = formatSize(file.size);
+  document.getElementById('fileInfo').classList.remove('hidden');
+  document.getElementById('deployBtn').disabled = false;
+  document.getElementById('uploadError').classList.add('hidden');
+}
+
+function formatSize(bytes) {
+  if (bytes < 1024) return bytes + ' B';
+  if (bytes < 1048576) return (bytes/1024).toFixed(1) + ' KB';
+  return (bytes/1048576).toFixed(1) + ' MB';
+}
+
+document.getElementById('deployBtn').addEventListener('click', function(){
+  if (!selectedFile) return;
+  var btn = this;
+  btn.disabled = true;
+  btn.textContent = '上传中...';
+
+  var formData = new FormData();
+  formData.append('file', selectedFile);
+
+  var xhr = new XMLHttpRequest();
+  xhr.open('POST', '/deploy/upload');
+
+  xhr.upload.onprogress = function(e){
+    if (e.lengthComputable){
+      var pct = Math.round(e.loaded / e.total * 100);
+      document.getElementById('uploadProgress').classList.remove('hidden');
+      document.getElementById('progressFill').style.width = pct + '%';
+      document.getElementById('progressText').textContent = pct + '%';
+    }
+  };
+
+  xhr.onload = function(){
+    try {
+      var resp = JSON.parse(xhr.responseText);
+      if (resp.success) {
+        document.getElementById('uploadCard').classList.add('hidden');
+        showReconnect();
+      } else {
+        showError(resp.error || '部署失败');
+      }
+    } catch(e) {
+      showError('解析响应失败');
+    }
+    btn.disabled = false;
+    btn.textContent = '开始部署';
+  };
+
+  xhr.onerror = function(){
+    document.getElementById('uploadCard').classList.add('hidden');
+    showReconnect();
+  };
+
+  xhr.send(formData);
+});
+
+function showReconnect(){
+  document.getElementById('reconnectCard').classList.remove('hidden');
+  pollReconnect();
+}
+
+function pollReconnect(){
+  var attempts = 0;
+  var interval = setInterval(function(){
+    attempts++;
+    fetch('/deploy/result')
+      .then(function(r){ return r.json(); })
+      .then(function(data){
+        clearInterval(interval);
+        document.getElementById('reconnectCard').classList.add('hidden');
+        showHealthResult(data);
+      })
+      .catch(function(){
+        if (attempts > 30){
+          clearInterval(interval);
+          document.getElementById('reconnectCard').innerHTML =
+            '<div class="status-msg status-fail">连接超时,请检查网关是否正常启动</div>';
+        }
+      });
+  }, 2000);
+}
+
+function showHealthResult(data){
+  var card = document.getElementById('statusCard');
+  card.classList.remove('hidden');
+
+  var checks = (data.phase1 && data.phase1.checks) || [];
+  var allOk = data.phase1 && data.phase1.success;
+
+  document.getElementById('statusTitle').textContent = allOk ? '部署成功' : '部署失败';
+  document.getElementById('statusMsg').className = 'status-msg ' + (allOk ? 'status-ok' : 'status-fail');
+  document.getElementById('statusMsg').textContent = allOk ? 'Phase 1 健康检测全部通过' : 'Phase 1 健康检测失败';
+
+  var list = document.getElementById('checkList');
+  list.innerHTML = '';
+  checks.forEach(function(c){
+    var cls = c.Status === 'ok' ? 'check-ok' : 'check-fail';
+    var icon = c.Status === 'ok' ? 'OK' : '!!';
+    list.innerHTML += '<div class="check-item">' +
+      '<span class="check-icon ' + cls + '">' + icon + '</span>' +
+      '<span>' + (checkNames[c.Name] || c.Name) + '</span>' +
+      '<span class="check-detail">' + c.Detail + '</span></div>';
+  });
+
+  if (data.phase2){
+    var p2 = data.phase2;
+    list.innerHTML += '<div style="margin-top:12px;padding-top:12px;border-top:2px solid #eee">' +
+      '<p style="font-size:13px;color:#888;margin-bottom:8px">Phase 2 业务恢复确认</p></div>';
+    (data.phase2.checks||[]).forEach(function(c){
+      var cls = c.Status === 'ok' ? 'check-ok' : 'check-fail';
+      list.innerHTML += '<div class="check-item">' +
+        '<span class="check-icon ' + cls + '">' + (c.Status === 'ok' ? 'OK' : '!!') + '</span>' +
+        '<span>' + (checkNames[c.Name] || c.Name) + '</span>' +
+        '<span class="check-detail">' + c.Detail + '</span></div>';
+    });
+  }
+}
+
+function showError(msg){
+  var el = document.getElementById('uploadError');
+  el.textContent = msg;
+  el.classList.remove('hidden');
+}
+</script>
+</body>
+</html>

+ 6 - 0
edge/ipole/deployhtml.go

@@ -0,0 +1,6 @@
+package main
+
+import _ "embed"
+
+//go:embed deploy.html
+var deployHTML string

+ 217 - 0
edge/ipole/deployhttp.go

@@ -0,0 +1,217 @@
+package main
+
+import (
+	stdjson "encoding/json" // 标准库,避免 json-iterator nil map panic
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"path/filepath"
+	"runtime/debug"
+	"time"
+
+	"lc/common/util"
+)
+
+// StartDeployHTTP 启动本地部署 HTTP 服务(端口 9998)
+func StartDeployHTTP() {
+	mux := http.NewServeMux()
+
+	// 简易健康探针:无依赖直接返回 OK,用于验证 HTTP 服务是否正常
+	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+		w.Write([]byte("OK"))
+	})
+
+	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+		if r.URL.Path == "/favicon.ico" {
+			w.WriteHeader(204)
+			return
+		}
+		if r.URL.Path != "/" {
+			http.NotFound(w, r)
+			return
+		}
+		util.GetTagLog().Infof("sys", "DeployHTTP: GET / from %s", r.RemoteAddr)
+		w.Header().Set("Content-Type", "text/html; charset=utf-8")
+		w.Write([]byte(deployHTML))
+	})
+
+	mux.HandleFunc("/deploy/upload", withRecover(handleDeployUpload))
+	mux.HandleFunc("/deploy/status", handleDeployStatusSSE)
+	mux.HandleFunc("/deploy/result", withRecover(handleDeployResult))
+
+	server := &http.Server{
+		Addr:         ":9998",
+		Handler:      withLogging(mux),
+		ReadTimeout:  30 * time.Second,
+		WriteTimeout: 60 * time.Second,
+		IdleTimeout:  120 * time.Second,
+	}
+
+	util.GetTagLog().Infof("sys", "本地部署服务启动在 :9998")
+	go func() {
+		if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+			util.GetTagLog().Errorf("sys", "本地部署服务启动失败:%s", err.Error())
+		}
+	}()
+}
+
+// withLogging 请求日志中间件
+func withLogging(next http.Handler) http.Handler {
+	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		util.GetTagLog().Infof("sys", "DeployHTTP: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr)
+		next.ServeHTTP(w, r)
+	})
+}
+
+// withRecover panic 恢复包装
+func withRecover(fn http.HandlerFunc) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		defer func() {
+			if err := recover(); err != nil {
+				util.GetTagLog().Errorf("sys", "DeployHTTP: panic in %s %s: %v\n%s", r.Method, r.URL.Path, err, string(debug.Stack()))
+				http.Error(w, "Internal Server Error", 500)
+			}
+		}()
+		fn(w, r)
+	}
+}
+
+func handleDeployUpload(w http.ResponseWriter, r *http.Request) {
+	if r.Method != http.MethodPost {
+		writeJSON(w, 405, map[string]interface{}{"success": false, "error": "Method not allowed"})
+		return
+	}
+
+	r.Body = http.MaxBytesReader(w, r.Body, 50<<20)
+
+	file, header, err := r.FormFile("file")
+	if err != nil {
+		writeJSON(w, 400, map[string]interface{}{"success": false, "error": "读取文件失败: " + err.Error()})
+		return
+	}
+	defer file.Close()
+
+	tmpDir := filepath.Join(util.GetPath(4), deployTmpDir)
+	os.MkdirAll(tmpDir, os.ModePerm)
+	tmpFile := filepath.Join(tmpDir, header.Filename)
+	out, err := os.Create(tmpFile)
+	if err != nil {
+		writeJSON(w, 500, map[string]interface{}{"success": false, "error": "创建临时文件失败: " + err.Error()})
+		return
+	}
+	defer out.Close()
+
+	if _, err := io.Copy(out, file); err != nil {
+		writeJSON(w, 500, map[string]interface{}{"success": false, "error": "写入文件失败: " + err.Error()})
+		return
+	}
+
+	if !isELF(tmpFile) {
+		os.Remove(tmpFile)
+		writeJSON(w, 400, map[string]interface{}{"success": false, "error": "文件格式错误:非Linux可执行文件"})
+		return
+	}
+
+	md5Hash, _ := fileMD5(tmpFile)
+
+	cwd, _ := os.Getwd()
+	targetPath := filepath.Join(cwd, appname)
+	backupPath := targetPath + ".bak"
+
+	version := fmt.Sprintf("local-%s", time.Now().Format("20060102-150405"))
+
+	os.Remove(backupPath)
+	if _, err := os.Stat(targetPath); err == nil {
+		if err := os.Rename(targetPath, backupPath); err != nil {
+			writeJSON(w, 500, map[string]interface{}{"success": false, "error": "备份旧版失败: " + err.Error()})
+			return
+		}
+	}
+
+	if err := os.Rename(tmpFile, targetPath); err != nil {
+		os.Rename(backupPath, targetPath)
+		writeJSON(w, 500, map[string]interface{}{"success": false, "error": "替换文件失败: " + err.Error()})
+		return
+	}
+	os.Chmod(targetPath, 0755)
+
+	marker := DeployMarker{Version: version, Timestamp: time.Now().Unix(), Action: "deploy"}
+	markerContent, _ := json.MarshalToString(marker)
+	os.WriteFile(filepath.Join(util.GetPath(0), "deploy_marker.json"), []byte(markerContent), os.ModePerm)
+
+	util.GetTagLog().Infof("sys", "DeployHTTP: 本地上传部署完成 version=%s md5=%s", version, md5Hash)
+
+	writeJSON(w, 200, map[string]interface{}{
+		"success": true,
+		"version": version,
+		"md5":     md5Hash,
+		"message": "文件已部署,即将重启...",
+	})
+
+	go func() {
+		time.Sleep(500 * time.Millisecond)
+		os.Exit(0)
+	}()
+}
+
+func handleDeployStatusSSE(w http.ResponseWriter, r *http.Request) {
+	util.GetTagLog().Infof("sys", "DeployHTTP: SSE连接 from %s", r.RemoteAddr)
+
+	w.Header().Set("Content-Type", "text/event-stream")
+	w.Header().Set("Cache-Control", "no-cache")
+	w.Header().Set("Connection", "keep-alive")
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+
+	flusher, ok := w.(http.Flusher)
+	if !ok {
+		http.Error(w, "SSE not supported", 500)
+		return
+	}
+
+	result := DeployResultGet()
+	if result != nil {
+		data, _ := json.MarshalToString(result)
+		fmt.Fprintf(w, "data: %s\n\n", data)
+		flusher.Flush()
+	} else {
+		fmt.Fprintf(w, "data: {\"status\":\"waiting\"}\n\n")
+		flusher.Flush()
+	}
+
+	ticker := time.NewTicker(15 * time.Second)
+	defer ticker.Stop()
+
+	for {
+		select {
+		case <-r.Context().Done():
+			util.GetTagLog().Infof("sys", "DeployHTTP: SSE断开 from %s", r.RemoteAddr)
+			return
+		case <-ticker.C:
+			result := DeployResultGet()
+			if result != nil {
+				data, _ := json.MarshalToString(result)
+				fmt.Fprintf(w, "data: %s\n\n", data)
+				flusher.Flush()
+			}
+		}
+	}
+}
+
+func handleDeployResult(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Access-Control-Allow-Origin", "*")
+	result := DeployResultGet()
+	if result == nil {
+		writeJSON(w, 200, map[string]interface{}{"status": "no_deployment"})
+		return
+	}
+	writeJSON(w, 200, result)
+}
+
+func writeJSON(w http.ResponseWriter, code int, data interface{}) {
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	w.WriteHeader(code)
+	body, _ := stdjson.Marshal(data)
+	w.Write(body)
+}

+ 382 - 0
edge/ipole/deploymgr.go

@@ -0,0 +1,382 @@
+package main
+
+import (
+	"crypto/md5"
+	"encoding/base64"
+	"encoding/hex"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync"
+	"time"
+
+	"lc/common/mqtt"
+	"lc/common/protocol"
+	"lc/common/util"
+)
+
+const (
+	deployChunkTimeout = 10 * time.Minute
+	deployTmpDir       = "deploy"
+)
+
+// DeployMgr 部署管理器
+type DeployMgr struct {
+	mu          sync.Mutex
+	active      bool
+	version     string
+	fileName    string
+	totalChunks int
+	chunkSize   int
+	expectedMD5 string
+	chunkBits   []bool
+	receivedAt  time.Time
+	deployTmp   string
+	cancel      chan struct{}
+}
+
+var _deployMgrOnce sync.Once
+var _deployMgr *DeployMgr
+
+func GetDeployMgr() *DeployMgr {
+	_deployMgrOnce.Do(func() {
+		_deployMgr = &DeployMgr{}
+	})
+	return _deployMgr
+}
+
+// HandleTpDeploy 处理来自 MQTT 的部署指令/分片
+func HandleTpDeploy(m mqtt.Message) {
+	var obj protocol.Pack_DeployCmd
+	if err := obj.DeCode(string(m.Payload())); err != nil {
+		util.GetTagLog().Errorf("sys", "HandleTpDeploy:DeCode失败,err=%v", err)
+		return
+	}
+	mgr := GetDeployMgr()
+
+	switch obj.Data.Action {
+	case "start":
+		mgr.start(&obj)
+	case "chunk":
+		mgr.receiveChunk(&obj)
+	case "abort":
+		mgr.abort(&obj)
+	default:
+		util.GetTagLog().Warnf("sys", "HandleTpDeploy:未知Action=%s", obj.Data.Action)
+	}
+}
+
+func (o *DeployMgr) start(cmd *protocol.Pack_DeployCmd) {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+
+	if o.active {
+		util.GetTagLog().Warnf("sys", "DeployMgr:已有部署进行中,拒绝新部署指令 version=%s", cmd.Data.Version)
+		o.sendAck(cmd.Data.Version, false, "已有部署进行中")
+		return
+	}
+
+	o.active = true
+	o.version = cmd.Data.Version
+	o.fileName = cmd.Data.FileName
+	o.totalChunks = cmd.Data.TotalChunks
+	o.chunkSize = cmd.Data.ChunkSize
+	o.expectedMD5 = cmd.Data.MD5
+	o.chunkBits = make([]bool, cmd.Data.TotalChunks)
+	o.receivedAt = time.Now()
+	o.deployTmp = filepath.Join(util.GetPath(4), deployTmpDir)
+	o.cancel = make(chan struct{}, 1)
+
+	if err := os.MkdirAll(o.deployTmp, os.ModePerm); err != nil {
+		util.GetTagLog().Errorf("sys", "DeployMgr:创建临时目录失败,path=%s,err=%v", o.deployTmp, err)
+		o.cleanup("创建临时目录失败: " + err.Error())
+		return
+	}
+
+	util.GetTagLog().Infof("sys", "DeployMgr:开始部署 version=%s totalChunks=%d chunkSize=%d md5=%s",
+		cmd.Data.Version, cmd.Data.TotalChunks, cmd.Data.ChunkSize, cmd.Data.MD5)
+
+	// 启动超时监控 goroutine(独立于 gopool,避免 worker 饥饿问题)
+	go o.watchTimeout()
+}
+
+func (o *DeployMgr) receiveChunk(cmd *protocol.Pack_DeployCmd) {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+
+	if !o.active {
+		return
+	}
+	if cmd.Data.Version != o.version {
+		util.GetTagLog().Warnf("sys", "DeployMgr:分片版本不匹配,expect=%s,got=%s", o.version, cmd.Data.Version)
+		return
+	}
+
+	idx := cmd.Data.ChunkIndex
+	if idx < 0 || idx >= o.totalChunks {
+		util.GetTagLog().Errorf("sys", "DeployMgr:分片索引越界,idx=%d,total=%d", idx, o.totalChunks)
+		return
+	}
+
+	// base64 decode the chunk data
+	decoded, err := base64.StdEncoding.DecodeString(cmd.Data.Data)
+	if err != nil {
+		util.GetTagLog().Errorf("sys", "DeployMgr:base64解码分片%d失败,err=%v", idx, err)
+		return
+	}
+
+	chunkFile := filepath.Join(o.deployTmp, fmt.Sprintf("chunk_%d", idx))
+	if err := os.WriteFile(chunkFile, decoded, os.ModePerm); err != nil {
+		util.GetTagLog().Errorf("sys", "DeployMgr:写分片失败,idx=%d,err=%v", idx, err)
+		return
+	}
+
+	o.chunkBits[idx] = true
+	o.receivedAt = time.Now()
+
+	allReceived := true
+	for _, b := range o.chunkBits {
+		if !b {
+			allReceived = false
+			break
+		}
+	}
+
+	if allReceived {
+		go o.assembleAndDeploy()
+	}
+}
+
+func (o *DeployMgr) abort(cmd *protocol.Pack_DeployCmd) {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+
+	if !o.active || cmd.Data.Version != o.version {
+		return
+	}
+
+	util.GetTagLog().Infof("sys", "DeployMgr:收到取消指令 version=%s", o.version)
+	if o.cancel != nil {
+		close(o.cancel)
+	}
+	o.cleanup("已取消")
+}
+
+func (o *DeployMgr) assembleAndDeploy() {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+
+	if !o.active {
+		return
+	}
+
+	util.GetTagLog().Infof("sys", "DeployMgr:所有分片已收齐,开始重组 version=%s", o.version)
+
+	// 1. 重组文件
+	assembledFile := filepath.Join(o.deployTmp, o.fileName)
+	outFile, err := os.Create(assembledFile)
+	if err != nil {
+		o.cleanup("创建重组文件失败: " + err.Error())
+		return
+	}
+	defer outFile.Close()
+
+	for i := 0; i < o.totalChunks; i++ {
+		chunkFile := filepath.Join(o.deployTmp, fmt.Sprintf("chunk_%d", i))
+		data, err := os.ReadFile(chunkFile)
+		if err != nil {
+			outFile.Close()
+			o.cleanup(fmt.Sprintf("读取分片%d失败: %s", i, err.Error()))
+			return
+		}
+		if _, err := outFile.Write(data); err != nil {
+			outFile.Close()
+			o.cleanup(fmt.Sprintf("写入重组文件失败: %s", err.Error()))
+			return
+		}
+	}
+
+	// 2. MD5 校验
+	actualMD5, err := fileMD5(assembledFile)
+	if err != nil {
+		o.cleanup("计算MD5失败: " + err.Error())
+		return
+	}
+	if !strings.EqualFold(actualMD5, o.expectedMD5) {
+		o.cleanup(fmt.Sprintf("MD5校验失败:期望%s 实际%s", o.expectedMD5, actualMD5))
+		return
+	}
+	util.GetTagLog().Infof("sys", "DeployMgr:MD5校验通过 md5=%s", actualMD5)
+
+	// 3. ELF 魔数校验
+	if !isELF(assembledFile) {
+		o.cleanup("文件格式错误:非Linux可执行文件")
+		return
+	}
+
+	// 4. 磁盘空间检查(rename 不消耗额外空间,只需 1x 文件大小)
+	cwd, _ := os.Getwd()
+	targetPath := filepath.Join(cwd, o.fileName)
+	fi, _ := os.Stat(assembledFile)
+	needed := fi.Size() + 1*1024*1024 // 文件大小 + 1MB 安全余量
+	free, err := getFreeSpace(cwd)
+	if err == nil && free < needed {
+		o.cleanup(fmt.Sprintf("磁盘空间不足:需要%dMB 可用%dMB", needed/(1024*1024), free/(1024*1024)))
+		return
+	}
+
+	// 5. 备份当前二进制,替换新文件
+	backupPath := targetPath + ".bak"
+
+	os.Remove(backupPath)
+	if _, err := os.Stat(targetPath); err == nil {
+		if err := os.Rename(targetPath, backupPath); err != nil {
+			o.cleanup("备份旧版失败: " + err.Error())
+			return
+		}
+		util.GetTagLog().Infof("sys", "DeployMgr:旧版已备份到 %s", backupPath)
+	}
+
+	if err := os.Rename(assembledFile, targetPath); err != nil {
+		os.Rename(backupPath, targetPath)
+		o.cleanup("替换文件失败: " + err.Error())
+		return
+	}
+	if err := os.Chmod(targetPath, 0755); err != nil {
+		util.GetTagLog().Warnf("sys", "DeployMgr:chmod失败,err=%v", err)
+	}
+
+	util.GetTagLog().Infof("sys", "DeployMgr:文件替换成功 version=%s path=%s", o.version, targetPath)
+
+	// 6. 写部署标记
+	marker := DeployMarker{
+		Version:   o.version,
+		Timestamp: time.Now().Unix(),
+		Action:    "deploy",
+	}
+	markerPath := filepath.Join(util.GetPath(0), "deploy_marker.json")
+	markerContent, _ := json.MarshalToString(marker)
+	if err := os.WriteFile(markerPath, []byte(markerContent), os.ModePerm); err != nil {
+		util.GetTagLog().Errorf("sys", "DeployMgr:写部署标记失败,err=%v", err)
+	}
+
+	// 7. 发送 ACK
+	o.active = false
+	o.sendAck(o.version, true, "")
+	util.GetTagLog().Infof("sys", "DeployMgr:即将退出进程以完成部署")
+
+	// 8. 退出让 goforever 拉起新版本
+	time.Sleep(500 * time.Millisecond)
+	os.Exit(0)
+}
+
+// watchTimeout 分片接收超时监控(独立 goroutine,不走 gopool 避免 worker 饥饿)
+func (o *DeployMgr) watchTimeout() {
+	ticker := time.NewTicker(30 * time.Second)
+	defer ticker.Stop()
+	for {
+		select {
+		case <-o.cancel:
+			return
+		case <-ticker.C:
+			o.mu.Lock()
+			if !o.active {
+				o.mu.Unlock()
+				return
+			}
+			elapsed := time.Since(o.receivedAt)
+			if elapsed > deployChunkTimeout {
+				received := o.countReceived()
+				o.mu.Unlock()
+				o.cleanup(fmt.Sprintf("分片接收超时:已收 %d/%d", received, o.totalChunks))
+				return
+			}
+			o.mu.Unlock()
+		}
+	}
+}
+
+func (o *DeployMgr) cleanup(errMsg string) {
+	util.GetTagLog().Errorf("sys", "DeployMgr:部署失败,原因=%s", errMsg)
+
+	if o.deployTmp != "" {
+		os.RemoveAll(o.deployTmp)
+	}
+
+	if o.active {
+		o.sendAck(o.version, false, errMsg)
+	}
+
+	o.active = false
+	o.version = ""
+	o.chunkBits = nil
+	o.cancel = nil
+}
+
+func (o *DeployMgr) sendAck(version string, success bool, errMsg string) {
+	var ack protocol.Pack_DeployAck
+	seq := GetNextUint64()
+	if str, err := ack.EnCode(appConfig.GID, appConfig.GID, seq, version, success, errMsg); err == nil {
+		topic := GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_DEPLOY_ACK)
+		GetMQTTMgr().Publish(topic, str, 0, ToCloud)
+		util.GetTagLog().Infof("sys", "DeployMgr:发送ACK version=%s success=%v", version, success)
+	}
+}
+
+func (o *DeployMgr) IsActive() bool {
+	o.mu.Lock()
+	defer o.mu.Unlock()
+	return o.active
+}
+
+func (o *DeployMgr) countReceived() int {
+	n := 0
+	for _, b := range o.chunkBits {
+		if b {
+			n++
+		}
+	}
+	return n
+}
+
+// DeployMarker 部署/回滚标记
+type DeployMarker struct {
+	Version   string `json:"version"`
+	Timestamp int64  `json:"timestamp"`
+	Action    string `json:"action"`
+}
+
+// ---- 工具函数 ----
+
+func fileMD5(path string) (string, error) {
+	f, err := os.Open(path)
+	if err != nil {
+		return "", err
+	}
+	defer f.Close()
+	h := md5.New()
+	if _, err := io.Copy(h, f); err != nil {
+		return "", err
+	}
+	return hex.EncodeToString(h.Sum(nil)), nil
+}
+
+func isELF(path string) bool {
+	f, err := os.Open(path)
+	if err != nil {
+		return false
+	}
+	defer f.Close()
+	header := make([]byte, 4)
+	if _, err := io.ReadFull(f, header); err != nil {
+		return false
+	}
+	return header[0] == 0x7f && header[1] == 'E' && header[2] == 'L' && header[3] == 'F'
+}
+
+// init registers the deploy handler
+func init() {
+	// Deploy handler will be registered in InitCloudMqttSubscribeTopics after appConfig is loaded
+}

+ 23 - 0
edge/ipole/deploymgr_linux.go

@@ -0,0 +1,23 @@
+//go:build linux
+// +build linux
+
+package main
+
+import (
+	"syscall"
+
+	"lc/common/util"
+)
+
+func getFreeSpace(dir string) (int64, error) {
+	var stat syscall.Statfs_t
+	if err := syscall.Statfs(dir, &stat); err != nil {
+		return 0, err
+	}
+	free := int64(stat.Bfree) * int64(stat.Bsize)
+	total := int64(stat.Blocks) * int64(stat.Bsize)
+	util.GetTagLog().Infof("sys", "DeployMgr:磁盘空间检查 dir=%s total=%dMB free=%dMB avail=%dMB bsize=%d blocks=%d bfree=%d bavail=%d",
+		dir, total/(1024*1024), int64(stat.Bfree)*int64(stat.Bsize)/(1024*1024), free/(1024*1024),
+		stat.Bsize, stat.Blocks, stat.Bfree, stat.Bavail)
+	return free, nil
+}

+ 9 - 0
edge/ipole/deploymgr_win.go

@@ -0,0 +1,9 @@
+//go:build !linux
+// +build !linux
+
+package main
+
+// getFreeSpace 非 Linux 平台跳过磁盘空间检查(仅用于开发编译)
+func getFreeSpace(dir string) (int64, error) {
+	return 0, nil
+}

+ 62 - 4
edge/ipole/devmgr.go

@@ -1,11 +1,11 @@
 package main
 
 import (
+	"fmt"
 	"sync"
 
-	"github.com/sirupsen/logrus"
-
 	"lc/common/protocol"
+	"lc/common/util"
 )
 
 type ModelInfo struct {
@@ -31,7 +31,7 @@ func CreateDevice(t uint8, devinfo *protocol.DevInfo) Device {
 	case 2:
 		device = NewYmLampController(devinfo)
 	default:
-		logrus.Errorf("AddDevices:不支持的协议的设备:mode=%d", t)
+		util.GetTagLog().Errorf("sys", "AddDevices:不支持的协议的设备:mode=%d", t)
 	}
 	return device
 }
@@ -51,13 +51,60 @@ type DeviceMgr struct {
 	mapDevice map[string]Device
 }
 
+func validateDevInfos(devinfos []protocol.DevInfo) error {
+	for i := range devinfos {
+		v := &devinfos[i]
+		if v.DevCode == "" {
+			return fmt.Errorf("DevCode 不能为空")
+		}
+		if v.TID == 0 {
+			return fmt.Errorf("设备[%s]的 TID 不能为 0,请指定正确的物模型ID", v.DevCode)
+		}
+		if v.ProtocolType > 2 {
+			return fmt.Errorf("设备[%s]的 ProtocolType=%d 不支持,仅支持 0(ModbusRTU) 1(Zigbee) 2(裕明485)", v.DevCode, v.ProtocolType)
+		}
+		// 同批次内 DevCode 唯一性
+		for j := i + 1; j < len(devinfos); j++ {
+			if v.DevCode == devinfos[j].DevCode {
+				return fmt.Errorf("同一文件中 DevCode=%s 重复", v.DevCode)
+			}
+		}
+		// 同批次内同一 Code 下 DevID 冲突检测
+		for j := i + 1; j < len(devinfos); j++ {
+			if v.Code == devinfos[j].Code && v.DevID == devinfos[j].DevID {
+				return fmt.Errorf("同一串口[Code=%d]下 DevID=%d 冲突(设备 %s 和 %s)", v.Code, v.DevID, v.DevCode, devinfos[j].DevCode)
+			}
+		}
+	}
+	return nil
+}
+
+func (o *DeviceMgr) checkDevIDConflict(v *protocol.DevInfo) error {
+	for _, exist := range o.mapDevice {
+		ei := exist.GetDevInfo()
+		if ei.Code == v.Code && ei.DevID == v.DevID && ei.DevCode != v.DevCode {
+			return fmt.Errorf("串口[Code=%d]下 DevID=%d 已被设备[%s]占用,设备[%s]不能使用相同地址",
+				v.Code, v.DevID, ei.DevCode, v.DevCode)
+		}
+	}
+	return nil
+}
+
 // AddDevices 新增
 func (o *DeviceMgr) AddDevices(devinfos []protocol.DevInfo) {
+	if err := validateDevInfos(devinfos); err != nil {
+		util.GetTagLog().Errorf("sys", "AddDevices: 配置校验失败: %v", err)
+		return
+	}
 	o.mu.Lock()
 	defer o.mu.Unlock()
 	for _, v := range devinfos {
 		if _, ok := o.mapDevice[v.DevCode]; ok {
-			logrus.Errorf("AddDevices:已添加该设备,不可重复添加:devcode=%s", v.DevCode)
+			util.GetTagLog().Errorf("sys", "AddDevices:已添加该设备,不可重复添加:devcode=%s", v.DevCode)
+			continue
+		}
+		if err := o.checkDevIDConflict(&v); err != nil {
+			util.GetTagLog().Errorf("sys", "AddDevices: %v", err)
 			continue
 		}
 		di := v
@@ -80,6 +127,12 @@ func (o *DeviceMgr) RemoveDevice(devcode string) {
 
 // UpdateDevices 新增或修改
 func (o *DeviceMgr) UpdateDevices(code uint8, devinfos []protocol.DevInfo) {
+	if len(devinfos) > 0 {
+		if err := validateDevInfos(devinfos); err != nil {
+			util.GetTagLog().Errorf("sys", "UpdateDevices: 配置校验失败: %v", err)
+			return
+		}
+	}
 	//先遍历devinfos
 	mapDevinfos := make(map[string]*protocol.DevInfo)
 	for _, v := range devinfos {
@@ -123,6 +176,11 @@ func (o *DeviceMgr) UpdateDevices(code uint8, devinfos []protocol.DevInfo) {
 				device.Stop()
 				delete(o.mapDevice, v.DevCode)
 			}
+		} else {
+			if err := o.checkDevIDConflict(&v); err != nil {
+				util.GetTagLog().Errorf("sys", "UpdateDevices: %v", err)
+				continue
+			}
 		}
 		if device := CreateDevice(v.ProtocolType, &v); device != nil {
 			device.Start()

+ 29 - 12
edge/ipole/handlefile.go

@@ -1,7 +1,6 @@
 package main
 
 import (
-	"io/ioutil"
 	"os"
 	"regexp"
 	"strconv"
@@ -20,7 +19,7 @@ func HandleFile(topic string, obj *protocol.Pack_SeqFileObject) error {
 		}
 		return SaveFile(obj.Data.File, obj.Data.Content, 0)
 	case protocol.TP_GW_SET_SERIAL:
-		var sc protocol.SerialConfig
+		sc := protocol.SerialConfig{Serial: make(map[uint8]*protocol.SerialPort)}
 		if err := json.UnmarshalFromString(obj.Data.Content, &sc); err != nil {
 			return err
 		}
@@ -51,35 +50,53 @@ func SaveFile(fname, content string, flag uint8) error {
 	case 2:
 		SavePath = util.GetPath(2)
 	}
+	tmpPath := util.GetPath(4)
+	util.GetTagLog().Infof("sys", "SaveFile:开始,fname=%s,flag=%d,tmpPath=%s,savePath=%s", fname, flag, tmpPath, SavePath)
+
 	//存成临时文件
-	if err := ioutil.WriteFile(util.GetPath(4)+fname, []byte(content), os.ModePerm); err != nil {
+	if err := os.MkdirAll(tmpPath, os.ModePerm); err != nil {
+		util.GetTagLog().Errorf("sys", "SaveFile:MkdirAll失败,path=%s,err=%v", tmpPath, err)
 		return err
 	}
-	//备份原文件
-	if err := os.Rename(SavePath+fname, SavePath+fname+".bak"); err != nil {
+	if err := os.WriteFile(tmpPath+fname, []byte(content), os.ModePerm); err != nil {
+		util.GetTagLog().Errorf("sys", "SaveFile:写tmp文件失败,path=%s,err=%v", tmpPath+fname, err)
 		return err
 	}
+	util.GetTagLog().Infof("sys", "SaveFile:tmp文件写入成功,path=%s,len=%d", tmpPath+fname, len(content))
+
+	//备份原文件(不存在则跳过)
+	if _, err := os.Stat(SavePath + fname); err == nil {
+		if err := os.Rename(SavePath+fname, SavePath+fname+".bak"); err != nil {
+			util.GetTagLog().Errorf("sys", "SaveFile:备份失败,src=%s,err=%v", SavePath+fname, err)
+			return err
+		}
+		util.GetTagLog().Infof("sys", "SaveFile:备份成功,src=%s", SavePath+fname+".bak")
+	} else {
+		util.GetTagLog().Infof("sys", "SaveFile:原文件不存在,跳过备份,path=%s", SavePath+fname)
+	}
 
 	//拷贝新文件
-	if err := os.Rename(util.GetPath(4)+fname, SavePath+fname); err != nil {
+	if err := os.Rename(tmpPath+fname, SavePath+fname); err != nil {
+		util.GetTagLog().Errorf("sys", "SaveFile:替换失败,src=%s,dst=%s,err=%v", tmpPath+fname, SavePath+fname, err)
 		//拷贝失败,则还原文件
 		if err := os.Rename(SavePath+fname+".bak", SavePath+fname); err != nil {
-			return err
+			util.GetTagLog().Errorf("sys", "SaveFile:还原失败,src=%s,err=%v", SavePath+fname+".bak", err)
 		}
 		return err
 	}
+	util.GetTagLog().Infof("sys", "SaveFile:替换成功,path=%s", SavePath+fname)
 	return nil
 }
 
 func ReadFileContent(topic string, o *protocol.Pack_SeqFileObject) {
 	switch topic {
 	case protocol.TP_GW_APP:
-		if buf, err := ioutil.ReadFile(util.GetPath(0) + "app.json"); err == nil {
+		if buf, err := os.ReadFile(util.GetPath(0) + "app.json"); err == nil {
 			o.Data.File = "app.json"
 			o.Data.Content = string(buf)
 		}
 	case protocol.TP_GW_SERIAL:
-		if buf, err := ioutil.ReadFile(util.GetPath(0) + "serial.json"); err == nil {
+		if buf, err := os.ReadFile(util.GetPath(0) + "serial.json"); err == nil {
 			o.Data.File = "serial.json"
 			o.Data.Content = string(buf)
 		}
@@ -104,7 +121,7 @@ func ReadMutilFileContent(topic string, id uint, o *protocol.Pack_MutilFileObjec
 			fname := strconv.Itoa(int(id)) + ".json"
 			AddFileObject(FileDir, fname, o)
 		} else {
-			rd, _ := ioutil.ReadDir(FileDir)
+			rd, _ := os.ReadDir(FileDir)
 			for _, fi := range rd {
 				if !fi.IsDir() {
 					ok, _ := regexp.MatchString(`[1-9].*json`, fi.Name())
@@ -115,7 +132,7 @@ func ReadMutilFileContent(topic string, id uint, o *protocol.Pack_MutilFileObjec
 			}
 		}
 	} else if topic == protocol.TP_GW_LOG { //日志
-		rd, _ := ioutil.ReadDir(FileDir)
+		rd, _ := os.ReadDir(FileDir)
 		for _, fi := range rd {
 			if !fi.IsDir() {
 				ok := strings.HasSuffix(fi.Name(), ".log")
@@ -132,7 +149,7 @@ func ReadMutilFileContent(topic string, id uint, o *protocol.Pack_MutilFileObjec
 }
 
 func AddFileObject(fpath, fname string, o *protocol.Pack_MutilFileObject) {
-	if buf, err := ioutil.ReadFile(fpath + fname); err == nil {
+	if buf, err := os.ReadFile(fpath + fname); err == nil {
 		var fo protocol.FileObject
 		fo.File = fname
 		fo.Content = string(buf)

+ 339 - 0
edge/ipole/healthcheck.go

@@ -0,0 +1,339 @@
+package main
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"sync/atomic"
+	"time"
+
+	"lc/common/protocol"
+	"lc/common/util"
+)
+
+const (
+	phase1Timeout = 30 * time.Second
+	phase2Delay   = 5 * time.Minute
+	phase2Window  = 5 * time.Minute
+)
+
+// HealthCheckRunner 健康检测运行器
+type HealthCheckRunner struct {
+	marker     *DeployMarker
+	running    int32
+	phase2Done int32
+}
+
+var _hcRunner *HealthCheckRunner
+
+// StartHealthCheckIfNeeded 启动时检查部署标记,如有则运行健康检测
+func StartHealthCheckIfNeeded() {
+	markerPath := filepath.Join(util.GetPath(0), "deploy_marker.json")
+	data, err := os.ReadFile(markerPath)
+	if err != nil {
+		// 检查是否有回滚标记
+		rollbackPath := filepath.Join(util.GetPath(0), "rollback_marker.json")
+		if rbData, err := os.ReadFile(rollbackPath); err == nil {
+			var marker DeployMarker
+			json.Unmarshal(rbData, &marker)
+			util.GetTagLog().Infof("sys", "检测到回滚标记,已回滚到版本:%s", marker.Version)
+			reportRollback(marker)
+			os.Remove(rollbackPath)
+		}
+		return
+	}
+
+	var marker DeployMarker
+	if err := json.Unmarshal(data, &marker); err != nil {
+		util.GetTagLog().Errorf("sys", "解析部署标记失败,err=%v", err)
+		return
+	}
+
+	util.GetTagLog().Infof("sys", "检测到部署标记,开始健康检测 version=%s", marker.Version)
+
+	_hcRunner = &HealthCheckRunner{
+		marker: &marker,
+	}
+
+	go func() {
+		time.Sleep(3 * time.Second) // 等待 MQTT 初始化
+		_hcRunner.runPhase1()
+	}()
+}
+
+func (o *HealthCheckRunner) runPhase1() {
+	if !atomic.CompareAndSwapInt32(&o.running, 0, 1) {
+		return
+	}
+	defer atomic.StoreInt32(&o.running, 0)
+
+	util.GetTagLog().Infof("sys", "HealthCheck:Phase 1 开始 version=%s", o.marker.Version)
+	deadline := time.Now().Add(phase1Timeout)
+
+	checks := []protocol.HealthCheck{
+		o.checkMQTT(deadline),
+		o.checkRedis(deadline),
+	}
+
+	allOK := true
+	for _, c := range checks {
+		if c.Status != "ok" {
+			allOK = false
+			break
+		}
+	}
+
+	o.reportPhase(1, allOK, checks)
+
+	if !allOK {
+		util.GetTagLog().Errorf("sys", "HealthCheck:Phase 1 失败,开始回滚")
+		o.rollback(checks)
+		return
+	}
+
+	util.GetTagLog().Infof("sys", "HealthCheck:Phase 1 全部通过,清理部署标记")
+	os.Remove(filepath.Join(util.GetPath(0), "deploy_marker.json"))
+	DeployResultSave(o.marker.Version, 1, allOK, checks)
+
+	go func() {
+		time.Sleep(phase2Delay)
+		o.runPhase2()
+	}()
+}
+
+func (o *HealthCheckRunner) checkMQTT(deadline time.Time) protocol.HealthCheck {
+	start := time.Now()
+	check := protocol.HealthCheck{Name: "mqtt"}
+
+	for time.Now().Before(deadline) {
+		mgr := GetMQTTMgr()
+		if mgr.Cloud != nil && mgr.Cloud.IsConnected() {
+			check.Status = "ok"
+			check.Detail = "MQTT Broker 已连接"
+			check.DurationMs = int(time.Since(start).Milliseconds())
+			util.GetTagLog().Infof("sys", "HealthCheck:MQTT检测通过")
+			return check
+		}
+		time.Sleep(1 * time.Second)
+	}
+
+	check.Status = "timeout"
+	check.Detail = "MQTT Broker 不可达,请检查 Server/User/Password 配置、网络连通性"
+	check.DurationMs = int(time.Since(start).Milliseconds())
+	util.GetTagLog().Errorf("sys", "HealthCheck:MQTT检测失败")
+	return check
+}
+
+func (o *HealthCheckRunner) checkRedis(deadline time.Time) protocol.HealthCheck {
+	start := time.Now()
+	check := protocol.HealthCheck{Name: "redis"}
+
+	for time.Now().Before(deadline) {
+		if redisEdgeData != nil {
+			if _, err := redisEdgeData.Ping().Result(); err == nil {
+				check.Status = "ok"
+				check.Detail = "Redis PING 正常"
+				check.DurationMs = int(time.Since(start).Milliseconds())
+				util.GetTagLog().Infof("sys", "HealthCheck:Redis检测通过")
+				return check
+			}
+		}
+		time.Sleep(2 * time.Second)
+	}
+
+	check.Status = "timeout"
+	check.Detail = "Redis 连接失败,请检查 Redis Server/Password 配置"
+	check.DurationMs = int(time.Since(start).Milliseconds())
+	util.GetTagLog().Errorf("sys", "HealthCheck:Redis检测失败")
+	return check
+}
+
+func (o *HealthCheckRunner) runPhase2() {
+	atomic.StoreInt32(&o.phase2Done, 1)
+	util.GetTagLog().Infof("sys", "HealthCheck:Phase 2 开始 version=%s", o.marker.Version)
+
+	checks := []protocol.HealthCheck{
+		o.checkSerial(),
+		o.checkModbusData(),
+	}
+
+	allOK := true
+	for _, c := range checks {
+		if c.Status != "ok" {
+			allOK = false
+			break
+		}
+	}
+
+	o.reportPhase(2, allOK, checks)
+	DeployResultSave(o.marker.Version, 2, allOK, checks)
+
+	if !allOK {
+		util.GetTagLog().Warnf("sys", "HealthCheck:Phase 2 存在告警,不回滚")
+	} else {
+		util.GetTagLog().Infof("sys", "HealthCheck:Phase 2 全部通过")
+	}
+}
+
+func (o *HealthCheckRunner) checkSerial() protocol.HealthCheck {
+	start := time.Now()
+	check := protocol.HealthCheck{Name: "serial"}
+
+	sc := GetSerialMgr()
+	failedPorts := sc.GetFailedPorts()
+	if len(failedPorts) == 0 {
+		check.Status = "ok"
+		check.Detail = "所有串口打开正常"
+	} else {
+		check.Status = "fail"
+		parts := make([]string, len(failedPorts))
+		for i, p := range failedPorts {
+			parts[i] = fmt.Sprintf("%d", p)
+		}
+		check.Detail = "串口打开失败: " + strings.Join(parts, ", ")
+	}
+	check.DurationMs = int(time.Since(start).Milliseconds())
+	return check
+}
+
+func (o *HealthCheckRunner) checkModbusData() protocol.HealthCheck {
+	start := time.Now()
+	check := protocol.HealthCheck{Name: "modbus"}
+
+	deviceCount := 0
+	hasData := false
+	mapRtuUploadManager.Range(func(key, value interface{}) bool {
+		mgr, ok := value.(*RtuUploadManager)
+		if !ok {
+			return true
+		}
+		deviceCount++
+		mgr.DataLock.Lock()
+		lastDataTime := mgr.Datatime
+		mgr.DataLock.Unlock()
+		if time.Since(lastDataTime) < phase2Window {
+			hasData = true
+			return false
+		}
+		return true
+	})
+
+	if deviceCount == 0 {
+		check.Status = "ok"
+		check.Detail = fmt.Sprintf("无Modbus设备配置,跳过采集检测")
+	} else if hasData {
+		check.Status = "ok"
+		check.Detail = fmt.Sprintf("设备数据采集正常(%d台设备)", deviceCount)
+	} else {
+		check.Status = "fail"
+		check.Detail = fmt.Sprintf("%d台设备5分钟内无数据上报,请检查设备配置和物模型是否正确加载", deviceCount)
+	}
+	check.DurationMs = int(time.Since(start).Milliseconds())
+	return check
+}
+
+func (o *HealthCheckRunner) reportPhase(phase int, success bool, checks []protocol.HealthCheck) {
+	var obj protocol.Pack_HealthCheck
+	seq := GetNextUint64()
+	if str, err := obj.EnCode(appConfig.GID, appConfig.GID, seq, o.marker.Version, phase, success, checks); err == nil {
+		topic := GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_DEPLOY_HEALTH)
+		GetMQTTMgr().Publish(topic, str, 0, ToCloud)
+		util.GetTagLog().Infof("sys", "HealthCheck:Phase %d 结果已上报 success=%v", phase, success)
+	}
+}
+
+func (o *HealthCheckRunner) rollback(checks []protocol.HealthCheck) {
+	cwd, _ := os.Getwd()
+	targetPath := filepath.Join(cwd, appname)
+	backupPath := targetPath + ".bak"
+
+	rbMarker := DeployMarker{
+		Version:   o.marker.Version,
+		Timestamp: time.Now().Unix(),
+		Action:    "rollback",
+	}
+	rbContent, _ := json.MarshalToString(rbMarker)
+	os.WriteFile(filepath.Join(util.GetPath(0), "rollback_marker.json"), []byte(rbContent), os.ModePerm)
+
+	os.Remove(filepath.Join(util.GetPath(0), "deploy_marker.json"))
+
+	DeployResultSave(o.marker.Version, 1, false, checks)
+
+	if _, err := os.Stat(backupPath); os.IsNotExist(err) {
+		util.GetTagLog().Errorf("sys", "HealthCheck:备份文件不存在,无法自动回滚")
+		return
+	}
+
+	if err := os.Rename(backupPath, targetPath); err != nil {
+		util.GetTagLog().Errorf("sys", "HealthCheck:回滚失败,err=%v", err)
+		return
+	}
+	os.Chmod(targetPath, 0755)
+	util.GetTagLog().Infof("sys", "HealthCheck:回滚成功,退出进程")
+
+	time.Sleep(500 * time.Millisecond)
+	os.Exit(2)
+}
+
+func reportRollback(marker DeployMarker) {
+	var obj protocol.Pack_DeployAck
+	seq := GetNextUint64()
+	if str, err := obj.EnCode(appConfig.GID, appConfig.GID, seq, marker.Version, false,
+		"已自动回滚到版本 "+marker.Version); err == nil {
+		topic := GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_DEPLOY_ACK)
+		GetMQTTMgr().Publish(topic, str, 0, ToCloud)
+	}
+}
+
+// ---- 部署结果持久化 ----
+
+type DeployResult struct {
+	Version   string             `json:"version"`
+	Timestamp int64              `json:"timestamp"`
+	Phase1    *DeployPhaseResult `json:"phase1,omitempty"`
+	Phase2    *DeployPhaseResult `json:"phase2,omitempty"`
+}
+
+type DeployPhaseResult struct {
+	Success bool                   `json:"success"`
+	Checks  []protocol.HealthCheck `json:"checks"`
+}
+
+var _deployResult *DeployResult
+
+func DeployResultSave(version string, phase int, success bool, checks []protocol.HealthCheck) {
+	if _deployResult == nil || _deployResult.Version != version {
+		_deployResult = &DeployResult{
+			Version:   version,
+			Timestamp: time.Now().Unix(),
+		}
+	}
+	pr := &DeployPhaseResult{Success: success, Checks: checks}
+	if phase == 1 {
+		_deployResult.Phase1 = pr
+	} else {
+		_deployResult.Phase2 = pr
+	}
+
+	content, _ := json.MarshalToString(_deployResult)
+	resultPath := filepath.Join(util.GetPath(0), "deploy_result.json")
+	os.WriteFile(resultPath, []byte(content), os.ModePerm)
+}
+
+func DeployResultGet() *DeployResult {
+	if _deployResult != nil {
+		return _deployResult
+	}
+	resultPath := filepath.Join(util.GetPath(0), "deploy_result.json")
+	data, err := os.ReadFile(resultPath)
+	if err != nil {
+		return nil
+	}
+	var result DeployResult
+	if err := json.Unmarshal(data, &result); err != nil {
+		return nil
+	}
+	_deployResult = &result
+	return &result
+}

+ 69 - 0
edge/ipole/logcfg.go

@@ -0,0 +1,69 @@
+package main
+
+import (
+	"github.com/sirupsen/logrus"
+
+	"lc/common/mqtt"
+	"lc/common/protocol"
+	"lc/common/util"
+)
+
+// InitLogCfg 初始化日志配置:从持久化文件加载,或使用默认值
+func InitLogCfg() {
+	tl := util.GetTagLog()
+	tl.SetPersistPath("conf/logcfg.json")
+	if err := tl.LoadFromFile(); err != nil {
+		tl.Infof("sys", "InitLogCfg:未找到持久化配置,使用默认debug级别,err=%v", err)
+		tl.SetLevel(logrus.DebugLevel)
+	} else {
+		tl.Infof("sys", "InitLogCfg:已加载持久化配置,global=%s,tags=%v",
+			tl.GetGlobalLevel().String(), tl.GetAllTagLevels())
+	}
+}
+
+// HandleTpLogCfg 处理云端下发的日志等级配置
+func HandleTpLogCfg(m mqtt.Message) {
+	tl := util.GetTagLog()
+	tl.Infof("sys", "HandleTpLogCfg:收到日志配置,topic=%s", m.Topic())
+
+	var cfg util.LogLevelConfig
+	cfg.Tags = make(map[string]string)
+	if err := json.Unmarshal([]byte(m.PayloadString()), &cfg); err != nil {
+		tl.Errorf("sys", "HandleTpLogCfg:解析失败,payload=%s,err=%v", m.PayloadString(), err)
+		return
+	}
+
+	// 设置全局级别
+	if cfg.Level != "" {
+		if lv, err := logrus.ParseLevel(cfg.Level); err == nil {
+			tl.SetLevel(lv)
+			tl.Infof("sys", "HandleTpLogCfg:全局级别已更新为%s", cfg.Level)
+		} else {
+			tl.Warnf("sys", "HandleTpLogCfg:无效的全局级别:%s", cfg.Level)
+		}
+	}
+
+	// 设置 tag 级别
+	for tag, levelStr := range cfg.Tags {
+		if lv, err := logrus.ParseLevel(levelStr); err == nil {
+			tl.SetTagLevel(tag, lv)
+			tl.Infof("sys", "HandleTpLogCfg:tag[%s]级别已更新为%s", tag, levelStr)
+		} else {
+			tl.Warnf("sys", "HandleTpLogCfg:无效的tag级别:%s=%s", tag, levelStr)
+		}
+	}
+
+	// 异步持久化,避免阻塞MQTT消息处理
+	go func() {
+		if err := tl.SaveToFile(); err != nil {
+			tl.Errorf("sys", "HandleTpLogCfg:持久化失败,err=%v", err)
+		}
+	}()
+
+	// 发送ACK
+	var ack protocol.Pack_Ack
+	if str, err := ack.EnCode(appConfig.GID, appConfig.GID, 0, nil); err == nil {
+		topic := GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_LOG_CFG_ACK)
+		GetMQTTMgr().Publish(topic, str, 0, ToCloud)
+	}
+}

+ 39 - 19
edge/ipole/main.go

@@ -12,7 +12,6 @@ import (
 
 	jsoniter "github.com/json-iterator/go"
 	"github.com/sanbornm/go-selfupdate/selfupdate"
-	"github.com/sirupsen/logrus"
 	"github.com/thinkgos/timing/v3"
 
 	"lc/common/util"
@@ -42,7 +41,7 @@ func Stat(args ...interface{}) interface{} {
 func GetNextUint64() uint64 {
 	u64, err := IDGen.NextId()
 	if err != nil {
-		logrus.Errorf("IDGen.NextId发生错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "IDGen.NextId发生错误:%s", err.Error())
 		u64 = util.MlNow().Unix()
 	}
 	return uint64(u64)
@@ -51,9 +50,9 @@ func GetNextUint64() uint64 {
 func EnableUpdate() {
 	var updater = &selfupdate.Updater{
 		CurrentVersion: version,
-		ApiURL:         "http://106.52.134.22:8180/",
-		BinURL:         "http://106.52.134.22:8180/",
-		DiffURL:        "http://106.52.134.22:8180/",
+		ApiURL:         appConfig.UpdateUrl,
+		BinURL:         appConfig.UpdateUrl,
+		DiffURL:        appConfig.UpdateUrl,
 		Dir:            "update/",
 		CmdName:        appname, // app name
 	}
@@ -69,7 +68,7 @@ func WatchGoforever(args ...interface{}) interface{} {
 
 		isRun, err := CheckProRunning("goforever")
 		if err != nil {
-			logrus.Errorf("检查goforever命令失败:%s", err.Error())
+			util.GetTagLog().Errorf("sys", "检查goforever命令失败:%s", err.Error())
 		} else {
 			if !isRun {
 				//这里重启goforever
@@ -77,12 +76,12 @@ func WatchGoforever(args ...interface{}) interface{} {
 				//err := exec.Command("/bin/sh", "-c", "/usr/app/goforever/goforever ./goforever &").Run()
 				err := exec.Command("/bin/sh", "-c", "/usr/app/goforever ./goforever &").Run()
 				if err != nil {
-					logrus.Errorf("重启goforever出错: %s", err.Error())
+					util.GetTagLog().Errorf("sys", "重启goforever出错: %s", err.Error())
 				} else {
-					logrus.Info("重启goforever成功,当前时间:", util.MlNow().String())
+					util.GetTagLog().Info("sys", "重启goforever成功,当前时间:", util.MlNow().String())
 				}
 			} else {
-				logrus.Info("goforever进程正在运行:", util.MlNow().String())
+				util.GetTagLog().Info("sys", "goforever进程正在运行:", util.MlNow().String())
 			}
 		}
 	}
@@ -91,11 +90,15 @@ func WatchGoforever(args ...interface{}) interface{} {
 // SyncTime 每半小时同步一次时间
 func SyncTime(args ...interface{}) interface{} {
 	for {
-		err := exec.Command("ntpclient", "-h", "106.52.134.22", "-s").Run()
+		if appConfig.NtpServer == "" {
+			time.Sleep(30 * time.Minute)
+			continue
+		}
+		err := exec.Command("ntpclient", "-h", appConfig.NtpServer, "-s").Run()
 		if err != nil {
-			logrus.Errorf("时间同步失败:%s", err.Error())
+			util.GetTagLog().Errorf("sys", "时间同步失败:%s", err.Error())
 		} else {
-			logrus.Info("时间同步成功,当前时间:", util.MlNow().String())
+			util.GetTagLog().Info("sys", "时间同步成功,当前时间:", util.MlNow().String())
 		}
 		time.Sleep(30 * time.Minute)
 	}
@@ -106,7 +109,7 @@ func SignalProcess(args ...interface{}) interface{} {
 		ch := make(chan os.Signal)
 		signal.Notify(ch, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL, syscall.SIGQUIT)
 
-		logrus.Info("检测到退出信号:", <-ch)
+		util.GetTagLog().Info("sys", "检测到退出信号:", <-ch)
 
 	}
 	return 0
@@ -127,13 +130,13 @@ func main() {
 	util.InitLogrus("release")
 
 	go func() {
-		logrus.Infoln(http.ListenAndServe(":9999", nil))
+		util.GetTagLog().Info("sys", http.ListenAndServe(":9999", nil))
 	}()
 
-	logrus.Infof("当前程序版本:%s", appname+" "+version)
+	util.GetTagLog().Infof("sys", "当前程序版本:%s", appname+" "+version)
 
 	if err := loadAppConfig(); err != nil {
-		logrus.Errorf("loadAppConfig错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "loadAppConfig错误:%s", err.Error())
 		return
 	}
 	//升级检查
@@ -142,7 +145,7 @@ func main() {
 	}
 
 	if err := loadSerialConfig(); err != nil {
-		logrus.Errorf("loadAppConfig错误:%s", err.Error())
+		util.GetTagLog().Errorf("sys", "loadAppConfig错误:%s", err.Error())
 		return
 	}
 
@@ -155,17 +158,32 @@ func main() {
 
 	InitCloudMqttSubscribeTopics()
 
+	//初始化日志配置(加载持久化级别或使用默认值)
+	InitLogCfg()
+
+	// 启动健康检测(检测部署标记文件)
+	go StartHealthCheckIfNeeded()
+
 	//打开串口
 	GetSerialMgr().AddSerialPorts(serialConfig.Serial)
 
 	//创建设备并启动数据处理流程
 	for k := range serialConfig.Serial {
-		if devinfos, err := LoadDev(k); err == nil {
-			GetDeviceMgr().AddDevices(devinfos)
+		devinfos, err := LoadDev(k)
+		if err != nil {
+			util.GetTagLog().Warnf("sys", "加载串口[%d]的设备配置文件dev/%d.json失败: %v ,该串口无设备将被跳过", k, k, err)
+			continue
 		}
+		GetDeviceMgr().AddDevices(devinfos)
 	}
 
+	// 启动本地部署 HTTP 服务(端口9998)
+	StartDeployHTTP()
+
 	gopool = util.NewPool(10)
+	GetMQTTMgr().SetRestartFn(func(fn func(args ...interface{}) interface{}, args ...interface{}) {
+		gopool.Add(fn, args)
+	})
 	gopool.Add(SyncTime, 0)
 	// gopool.Add(WatchGoforever, 0)
 	// gopool.Add(SignalProcess, 8)
@@ -176,6 +194,8 @@ func main() {
 	gopool.Add(GetMQTTMgr().MQTTConnectMgr, 5)
 	gopool.Add(Stat, 6)
 	gopool.Add(RadarReceive, 7)
+	gopool.Add(CleanLogs, 9)
+	gopool.Add(Heartbeat, 10)
 	gopool.Run()
 	gopool.Wait()
 }

+ 41 - 37
edge/ipole/modbusrtu.go

@@ -9,7 +9,6 @@ import (
 	"runtime/debug"
 	"time"
 
-	"github.com/sirupsen/logrus"
 	"github.com/thinkgos/timing/v3"
 
 	"lc/common/mqtt"
@@ -62,12 +61,12 @@ func NewModbusRtu(info *protocol.DevInfo) Device {
 
 	iot, err := loadModel(info.TID)
 	if err != nil {
-		logrus.Errorf("ReloadModel:加载模型[tid=%d]文件发生错误:%s", info.TID, err.Error())
+		util.GetTagLog().Errorf("modbus", "ReloadModel:加载模型[tid=%d]文件发生错误:%s", info.TID, err.Error())
 	} else {
 		if iot.Protocol == ModbusRtuProtocol {
 			rtu.model = iot
 		} else {
-			logrus.Error("ModbusRtu.UpdateModel:物模型错误,非ModbusRTU协议")
+			util.GetTagLog().Error("modbus", "ModbusRtu.UpdateModel:物模型错误,非ModbusRTU协议")
 		}
 	}
 	mapRtuUploadManager.Store(info.DevCode, NewRtuUploadManager(info))
@@ -115,22 +114,22 @@ func (o *ModbusRtu) UpdateModel2(mi *ModelInfo) {
 		return
 	}
 	if mi.Flag == 0 {
-		logrus.Errorf("设备[%s]的物模型[tid=%d]模型文件被删除,下次启动即将生效。", o.devinfo.DevCode, mi.TID)
+		util.GetTagLog().Errorf("modbus", "设备[%s]的物模型[tid=%d]模型文件被删除,下次启动即将生效。", o.devinfo.DevCode, mi.TID)
 		return
 	}
-	logrus.Debugf("ModbusRtu.UpdateModel2:更新设备[%s]的物模型[%d]", o.devinfo.DevCode, mi.TID)
+	util.GetTagLog().Debugf("modbus", "ModbusRtu.UpdateModel2:更新设备[%s]的物模型[%d]", o.devinfo.DevCode, mi.TID)
 	iot, err := loadModel(mi.TID)
 	if err != nil {
-		logrus.Errorf("ModbusRtu.UpdateModel2:加载模型[%d]文件错误:%s", mi.TID, err.Error())
+		util.GetTagLog().Errorf("modbus", "ModbusRtu.UpdateModel2:加载模型[%d]文件错误:%s", mi.TID, err.Error())
 		return
 	}
 	if iot.Protocol == ModbusRtuProtocol { //合法的物模型
 		o.model = iot
 		o.clearRequest()
 		o.updateRequest()
-		logrus.Infof("ModbusRtu.UpdateModel2:更新设备[%s]的物模型[%d]成功", o.devinfo.DevCode, mi.TID)
+		util.GetTagLog().Infof("modbus", "ModbusRtu.UpdateModel2:更新设备[%s]的物模型[%d]成功", o.devinfo.DevCode, mi.TID)
 	} else {
-		logrus.Error("ModbusRtu.UpdateModel2:物模型错误,TID和文件名tid不一致或协议非ModbusRTU协议")
+		util.GetTagLog().Error("modbus", "ModbusRtu.UpdateModel2:物模型错误,TID和文件名tid不一致或协议非ModbusRTU协议")
 	}
 }
 
@@ -151,8 +150,9 @@ func (o *ModbusRtu) GetDevType() string {
 func (o *ModbusRtu) HandleData() {
 	defer func() {
 		if err := recover(); err != nil {
-			logrus.Error("ModbusRtu.HandleData:panic:", err)
-			logrus.Error("stack:", string(debug.Stack()))
+			util.GetTagLog().Error("modbus", "ModbusRtu.HandleData:panic:", err)
+			util.GetTagLog().Error("modbus", "stack:", string(debug.Stack()))
+			time.Sleep(5 * time.Second)
 			go o.HandleData()
 		}
 	}()
@@ -161,7 +161,7 @@ func (o *ModbusRtu) HandleData() {
 	for {
 		select {
 		case <-o.ctx.Done():
-			logrus.Errorf("设备[%s]的HandleData退出,原因:%v", o.devinfo.DevCode, o.ctx.Err())
+			util.GetTagLog().Errorf("modbus", "设备[%s]的HandleData退出,原因:%v", o.devinfo.DevCode, o.ctx.Err())
 			return
 		case devinfo_ := <-o.chanDevInfo:
 			o.devinfo = devinfo_
@@ -183,12 +183,16 @@ func (o *ModbusRtu) clearRequest() {
 }
 
 func (o *ModbusRtu) updateRequest() {
+	if o.model == nil {
+		util.GetTagLog().Errorf("modbus", "设备[%s]的物模型为nil,无法更新采集请求,请先下发TID=%d的物模型文件", o.devinfo.DevCode, o.devinfo.TID)
+		return
+	}
 	for k, v := range o.model.Packet {
 		r := Request{CID: k, Rtuinfo: o.devinfo, FuncCode: v.Code, Address: v.Addr,
 			Quantity: v.Quantity, ScanRate: time.Duration(v.Cycle) * time.Millisecond,
 		}
 		if err := o.AddGatherJob(&r); err != nil {
-			logrus.Errorf("给串口[%d]添加采集任务[DevCode=%s,SlaveID=%d,TID=%d,CID=%d]失败:%s",
+			util.GetTagLog().Errorf("modbus", "给串口[%d]添加采集任务[DevCode=%s,SlaveID=%d,TID=%d,CID=%d]失败:%s",
 				o.devinfo.Code, o.devinfo.DevCode, o.devinfo.DevID, o.devinfo.TID, k, err.Error())
 		} else {
 			o.reqList = append(o.reqList, &r)
@@ -231,7 +235,7 @@ func (o *ModbusRtu) ProcReadDiscretes(cid uint8, address, quality uint16, valBuf
 }
 
 func (o *ModbusRtu) ProcReadHoldingRegisters(cid uint8, address, quality uint16, valBuf []byte) {
-	logrus.Debugf("收到自寄存器地址%d;开始的数据:%v", address, hex.EncodeToString(valBuf))
+	util.GetTagLog().Debugf("modbus", "收到自寄存器地址%d;开始的数据:%v", address, hex.EncodeToString(valBuf))
 	dataLen := len(valBuf)
 	if o.model.Packet[cid].Resplen != uint(dataLen) {
 		return
@@ -242,7 +246,7 @@ func (o *ModbusRtu) ProcReadHoldingRegisters(cid uint8, address, quality uint16,
 			continue
 		}
 		if int(v.Start+v.Len) > dataLen { //索引超长,忽略该项目
-			logrus.Errorf("物模型[TID=%d]配置的数据项[sid=%d]start加len超过采集响应的长度", o.model.TID, v.SID)
+			util.GetTagLog().Errorf("modbus", "物模型[TID=%d]配置的数据项[sid=%d]start加len超过采集响应的长度", o.model.TID, v.SID)
 			continue
 		}
 		var fVal float64
@@ -277,7 +281,7 @@ func (o *ModbusRtu) ProcReadHoldingRegisters(cid uint8, address, quality uint16,
 		}
 		fVal = fVal - float64(v.Base)
 
-		logrus.Debugf(v.NameZh, ":", fVal)
+		util.GetTagLog().Debugf("modbus", v.NameZh, ":", fVal)
 
 		if v.Type == 0 || v.Type == 1 { //整数
 			dataMap[v.SID] = Precision(fVal, 0, true)
@@ -294,20 +298,20 @@ func (o *ModbusRtu) ProcReadHoldingRegisters(cid uint8, address, quality uint16,
 }
 
 func (o *ModbusRtu) ProcReadInputRegisters(cid uint8, address, quality uint16, valBuf []byte) {
-	logrus.Debugf("收到自寄存器地址%d, 开始的数据:%v, 长度为:%d", address, hex.EncodeToString(valBuf), len(valBuf))
+	util.GetTagLog().Debugf("modbus", "收到自寄存器地址%d, 开始的数据:%v, 长度为:%d", address, hex.EncodeToString(valBuf), len(valBuf))
 	dataLen := len(valBuf)
 	if o.model.Packet[cid].Resplen != uint(dataLen) {
-		logrus.Errorf("ProcReadInputRegisters len no equal")
+		util.GetTagLog().Errorf("modbus", "ProcReadInputRegisters len no equal")
 		return
 	}
 	dataMap := make(map[uint16]float64)
 	for _, v := range o.model.DataUp {
 		if v.Cid != cid || v.Len == 0 {
-			logrus.Errorf("ProcReadInputRegisters Cid no equal")
+			util.GetTagLog().Errorf("modbus", "ProcReadInputRegisters Cid no equal")
 			continue
 		}
 		if int(v.Start+v.Len) > dataLen { //索引超长,忽略该项目
-			logrus.Errorf("物模型[TID=%d]配置的数据项[sid=%d]start加len超过采集响应的长度", o.model.TID, v.SID)
+			util.GetTagLog().Errorf("modbus", "物模型[TID=%d]配置的数据项[sid=%d]start加len超过采集响应的长度", o.model.TID, v.SID)
 			continue
 		}
 		var fVal float64
@@ -349,7 +353,7 @@ func (o *ModbusRtu) ProcReadInputRegisters(cid uint8, address, quality uint16, v
 		}
 	}
 
-	logrus.Debugf("ProcReadInputRegisters dataMap = %v", dataMap)
+	util.GetTagLog().Debugf("modbus", "ProcReadInputRegisters dataMap = %v", dataMap)
 	if rtumgr, ok := mapRtuUploadManager.Load(o.devinfo.DevCode); ok {
 		prtumgr := rtumgr.(*RtuUploadManager)
 		if prtumgr != nil {
@@ -366,7 +370,7 @@ func (o *ModbusRtu) ProcResult(err error, req *Request) {
 	} else {
 		//连续采集超过5次都错误,则报告设备离线
 		if req.ErrCnt == 5 {
-			logrus.Errorf("采集设备[DevCode=%s,SlaveID=%d,Tid=%d,Cid=%d]的数据发生错误:%s",
+			util.GetTagLog().Errorf("modbus", "采集设备[DevCode=%s,SlaveID=%d,Tid=%d,Cid=%d]的数据发生错误:%s",
 				req.Rtuinfo.DevCode, req.Rtuinfo.DevID, req.Rtuinfo.TID, req.CID, err.Error())
 
 			//离线状态报告
@@ -374,7 +378,7 @@ func (o *ModbusRtu) ProcResult(err error, req *Request) {
 			if str, err := obj.EnCode(req.Rtuinfo.DevCode, appConfig.GID, GetNextUint64(), err, req.Rtuinfo.TID, nil); err == nil {
 				topic := GetTopic(o.GetDevType(), req.Rtuinfo.DevCode, protocol.TP_MODBUS_DATA)
 				GetMQTTMgr().Publish(topic, str, 0, ToAll)
-				logrus.Debugf("topic:%s,payload:%s", topic, str)
+				util.GetTagLog().Debugf("modbus", "topic:%s,payload:%s", topic, str)
 			}
 		}
 	}
@@ -385,8 +389,8 @@ func (o *ModbusRtu) procRequest(req *Request) {
 	var result []byte
 	defer func() {
 		if err := recover(); err != nil {
-			logrus.Error("procRequest:panic:", err)
-			logrus.Error("stack:", string(debug.Stack()))
+			util.GetTagLog().Error("modbus", "procRequest:panic:", err)
+			util.GetTagLog().Error("modbus", "stack:", string(debug.Stack()))
 		}
 	}()
 	req.TxCnt++
@@ -394,30 +398,30 @@ func (o *ModbusRtu) procRequest(req *Request) {
 	// A bit of access read
 	case modbus.FuncCodeReadCoils:
 		result, err = o.ReadCoils(req.Rtuinfo.DevID, req.Address, req.Quantity)
-		logrus.Debugf("ReadCoils result:= %s", string(result))
+		util.GetTagLog().Debugf("modbus", "ReadCoils result:= %s", string(result))
 		if err == nil {
 			o.ProcReadCoils(req.CID, req.Address, req.Quantity, result)
 		}
 	case modbus.FuncCodeReadDiscreteInputs:
 		result, err = o.ReadDiscreteInputs(req.Rtuinfo.DevID, req.Address, req.Quantity)
-		logrus.Debugf("ReadDiscreteInputs result:= %s", string(result))
+		util.GetTagLog().Debugf("modbus", "ReadDiscreteInputs result:= %s", string(result))
 		if err == nil {
 			o.ProcReadDiscretes(req.CID, req.Address, req.Quantity, result)
 		}
 	// 16-bit access read
 	case modbus.FuncCodeReadHoldingRegisters: //03
 		result, err = o.ReadHoldingRegistersBytes(req.Rtuinfo.DevID, req.Address, req.Quantity)
-		logrus.Debugf("ReadHoldingRegistersBytes result:= %s", string(result))
+		util.GetTagLog().Debugf("modbus", "ReadHoldingRegistersBytes result:= %s", string(result))
 		if err == nil {
 			o.ProcReadHoldingRegisters(req.CID, req.Address, req.Quantity, result)
 		} else {
-			logrus.Errorf("设备上传网关数据为空!可能原因:未配置正确,请检查配置文件;设备线路或设备本身损坏;网关串口损坏.\n")
-			logrus.Errorf("error:%v\n", err)
+			util.GetTagLog().Errorf("modbus", "设备上传网关数据为空!可能原因:未配置正确,请检查配置文件;设备线路或设备本身损坏;网关串口损坏.\n")
+			util.GetTagLog().Errorf("modbus", "error:%v\n", err)
 		}
 
 	case modbus.FuncCodeReadInputRegisters:
 		result, err = o.ReadInputRegistersBytes(req.Rtuinfo.DevID, req.Address, req.Quantity)
-		logrus.Debugf("ReadInputRegistersBytes result:= %s", string(result))
+		util.GetTagLog().Debugf("modbus", "ReadInputRegistersBytes result:= %s", string(result))
 		if err == nil {
 			o.ProcReadInputRegisters(req.CID, req.Address, req.Quantity, result)
 		}
@@ -465,8 +469,8 @@ func (o *ModbusRtu) WriteData(slaveID, funcCode byte, address, quantity uint16,
 	var err error
 	defer func() {
 		if err := recover(); err != nil {
-			logrus.Error("WriteData:panic:", err)
-			logrus.Error("stack:", string(debug.Stack()))
+			util.GetTagLog().Error("modbus", "WriteData:panic:", err)
+			util.GetTagLog().Error("modbus", "stack:", string(debug.Stack()))
 		}
 	}()
 	switch funcCode {
@@ -486,7 +490,7 @@ func (o *ModbusRtu) WriteData(slaveID, funcCode byte, address, quantity uint16,
 	case modbus.FuncCodeWriteMultipleRegisters: //16
 		err = o.WriteMultipleRegistersBytes(slaveID, address, quantity, value)
 	default:
-		logrus.Errorf("不支持的功能码:%d", funcCode)
+		util.GetTagLog().Errorf("modbus", "不支持的功能码:%d", funcCode)
 		err = errors.New("不支持的功能码")
 	}
 	return err
@@ -507,17 +511,17 @@ func (o *ModbusRtu) Send(slaveID byte, request modbus.ProtocolDataUnit) (modbus.
 	if err != nil {
 		return response, err
 	}
-	logrus.Debugf("发送给设备: %s 的数据: [% x]", o.devinfo.DevCode, aduRequest)
+	util.GetTagLog().Debugf("modbus", "发送给设备: %s 的数据: [% x]", o.devinfo.DevCode, aduRequest)
 	aduResponse, err := o.SendRecvData(aduRequest)
-	logrus.Debugf("收到的数据: %s 的数据: [% x]", o.devinfo.DevCode, aduResponse)
+	util.GetTagLog().Debugf("modbus", "收到的数据: %s 的数据: [% x]", o.devinfo.DevCode, aduResponse)
 
 	if err != nil {
-		logrus.Debugf("ReadHoldingRegistersBytes SendRecvData err:= %v", err)
+		util.GetTagLog().Debugf("modbus", "ReadHoldingRegistersBytes SendRecvData err:= %v", err)
 		return response, err
 	}
 	rspSlaveID, pdu, err := modbus.DecodeRTUFrame(aduResponse)
 	if err != nil {
-		logrus.Debugf("ReadHoldingRegistersBytes DecodeRTUFrame err:= %v", err)
+		util.GetTagLog().Debugf("modbus", "ReadHoldingRegistersBytes DecodeRTUFrame err:= %v", err)
 		return response, err
 	}
 	response = modbus.ProtocolDataUnit{FuncCode: pdu[0], Data: pdu[1:]}

+ 3 - 3
edge/ipole/monitor.go

@@ -9,7 +9,7 @@ import (
 	"sync"
 	"time"
 
-	"github.com/sirupsen/logrus"
+	"lc/common/util"
 )
 
 const (
@@ -59,7 +59,7 @@ func CheckProRunning(serverName string) (bool, error) {
 		return false, err
 	}
 
-	logrus.Info("ps len", len(str), str)
+	util.GetTagLog().Info("sys", "ps len", len(str), str)
 	return len(str) > 30, nil
 }
 
@@ -83,6 +83,6 @@ func getFilelist(path string) {
 		return nil
 	})
 	if err != nil {
-		logrus.Infof("filepath.Walk() returned %v\r\n", err)
+		util.GetTagLog().Infof("sys", "filepath.Walk() returned %v\r\n", err)
 	}
 }

+ 40 - 0
edge/ipole/mqtt_init.go

@@ -0,0 +1,40 @@
+package main
+
+import (
+	"sync"
+
+	"lc/common/mqtt"
+)
+
+// 重导出类型和常量,避免修改所有业务文件
+type OptType = mqtt.OptType
+type MQTTMessage = mqtt.MQTTMessage
+
+const (
+	ToAll   = mqtt.ToAll
+	ToCloud = mqtt.ToCloud
+	ToEdge  = mqtt.ToEdge
+)
+
+var _mqttMgrOnce sync.Once
+var _mqttMgr *mqtt.MQTTMgr
+
+func GetMQTTMgr() *mqtt.MQTTMgr {
+	_mqttMgrOnce.Do(func() {
+		_mqttMgr = mqtt.NewMQTTMgr(mqtt.MQTTMgrConfig{
+			CloudServer:   appConfig.Cloud.Mqtt.Server,
+			CloudClientID: appConfig.GID + "@" + appname + version,
+			CloudUser:     appConfig.Cloud.Mqtt.User,
+			CloudPassword: appConfig.Cloud.Mqtt.Password,
+			CloudTimeout:  appConfig.Cloud.Mqtt.Timeout,
+			CloudOnline:   &MqttOnline{},
+			EdgeServer:    appConfig.Edge.Mqtt.Server,
+			EdgeClientID:  appConfig.GID + "@" + appname + version,
+			EdgeUser:      appConfig.Edge.Mqtt.User,
+			EdgePassword:  appConfig.Edge.Mqtt.Password,
+			EdgeTimeout:   appConfig.Edge.Mqtt.Timeout,
+			EdgeOnline:    &MqttOnline{},
+		})
+	})
+	return _mqttMgr
+}

+ 0 - 125
edge/ipole/mqttclient.go

@@ -1,125 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-)
-
-type BaseMqttOnline interface {
-	GetOnlineMsg() (string, string)
-	GetWillMsg() (string, string)
-}
-
-type EmptyMqttOnline struct {
-}
-
-func (o *EmptyMqttOnline) GetOnlineMsg() (string, string) {
-	return "", ""
-}
-func (o *EmptyMqttOnline) GetWillMsg() (string, string) {
-	return "", ""
-}
-
-type MqttClient struct {
-	mqtt       *mqtt.Client        //
-	mu         sync.Mutex          //保护mapTopics
-	mapTopics  map[string]mqtt.QOS //订阅的主题
-	timeout    uint                //超时时间,毫秒为单位
-	MqttOnline BaseMqttOnline      //是否发布上线消息&遗嘱消息
-}
-
-func NewMqttClient(server, clientid, user, password string, timeout uint, mqttOnline BaseMqttOnline) *MqttClient {
-	o := MqttClient{
-		mapTopics:  make(map[string]mqtt.QOS),
-		timeout:    timeout,
-		MqttOnline: mqttOnline,
-	}
-	client, err := mqtt.NewClient(mqtt.ClientOptions{
-		Servers:       []string{server},
-		ClientID:      clientid,
-		Username:      user,
-		Password:      password,
-		AutoReconnect: true,
-	}, &o)
-	if err != nil {
-		panic(fmt.Sprintln("MQTT错误:", err.Error()))
-		return nil
-	}
-	o.mqtt = client
-	err = client.Connect(o.Ctx())
-	return &o
-}
-
-func (o *MqttClient) ConnectionLostHandler(err error) {
-	logrus.Errorln("MqttClient.ConnectionLostHandler:MQTT连接已经断开,原因:", err)
-}
-
-func (o *MqttClient) OnConnectHandler() {
-	logrus.Infoln("MqttClient.OnConnectHandler:MQTT连接成功")
-	//连接成功则订阅主题
-	for k, v := range o.mapTopics {
-		o.Subscribe(k, v)
-	}
-	topic, str := o.MqttOnline.GetOnlineMsg()
-	if topic != "" {
-		o.PublishString(topic, str, 0)
-	}
-}
-
-func (o *MqttClient) GetWill() (topic string, payload string) {
-	return o.MqttOnline.GetWillMsg()
-}
-
-func (o *MqttClient) Connect() error {
-	if !o.mqtt.IsConnected() {
-		return o.mqtt.Connect(o.Ctx())
-	}
-	return nil
-}
-
-func (o *MqttClient) IsConnected() bool {
-	return o.mqtt.IsConnected()
-}
-
-func (o *MqttClient) Publish(topic string, payload []byte, qos mqtt.QOS) error {
-	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishString(topic string, payload string, qos mqtt.QOS) error {
-	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishJSON(topic string, payload interface{}, qos mqtt.QOS) error {
-	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
-}
-
-func (o *MqttClient) Subscribe(topic string, qos mqtt.QOS) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; !ok {
-		o.mapTopics[topic] = qos
-	}
-	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
-}
-
-func (o *MqttClient) Unsubscribe(topic string) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; ok {
-		delete(o.mapTopics, topic)
-	}
-	return o.mqtt.Unsubscribe(o.Ctx(), topic)
-}
-
-func (o *MqttClient) Handle(topic string, handler mqtt.MessageHandler) mqtt.Route {
-	return o.mqtt.Handle(topic, handler)
-}
-
-func (o *MqttClient) Ctx() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.timeout))
-	return ctx
-}

+ 50 - 16
edge/ipole/mqtthandle.go

@@ -1,9 +1,10 @@
 package main
 
 import (
-	"io/ioutil"
+	"fmt"
 	"os"
 	"strings"
+	"time"
 
 	"lc/common/mqtt"
 	"lc/common/protocol"
@@ -99,14 +100,20 @@ func HandleTpWModel(m mqtt.Message) {
 	}
 }
 func HandleTpQLog(m mqtt.Message) {
+	util.GetTagLog().Infof("sys", "HandleTpQLog:收到日志查询请求,topic=%s,payload=%s", m.Topic(), m.PayloadString())
 	var obj protocol.Pack_IDObject
 	var ret protocol.Pack_MutilFileObject
-	if err := obj.DeCode(m.PayloadString()); err == nil {
-		//读文件内容
-		ReadMutilFileContent(protocol.TP_GW_LOG, obj.Data.Id, &ret)
-		if str, err := ret.EnCode(appConfig.GID, obj.Seq); err == nil {
-			GetMQTTMgr().Publish(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_LOG_ACK), str, 0, ToCloud)
-		}
+	if err := obj.DeCode(m.PayloadString()); err != nil {
+		util.GetTagLog().Errorf("sys", "HandleTpQLog:DeCode失败,err=%v", err)
+		return
+	}
+	//读文件内容
+	ReadMutilFileContent(protocol.TP_GW_LOG, obj.Data.Id, &ret)
+	if str, err := ret.EnCode(appConfig.GID, obj.Seq); err == nil {
+		GetMQTTMgr().Publish(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_LOG_ACK), str, 0, ToCloud)
+		util.GetTagLog().Infof("sys", "HandleTpQLog:日志ACK已发布")
+	} else {
+		util.GetTagLog().Errorf("sys", "HandleTpQLog:EnCode失败,err=%v", err)
 	}
 }
 func HandleTpRLog(m mqtt.Message) {
@@ -114,7 +121,7 @@ func HandleTpRLog(m mqtt.Message) {
 	var ret protocol.Pack_Ack
 	var err error
 	if err = obj.DeCode(m.PayloadString()); err == nil {
-		rd, _ := ioutil.ReadDir(util.GetPath(3))
+		rd, _ := os.ReadDir(util.GetPath(3))
 		for _, fi := range rd {
 			if ok := strings.HasSuffix(fi.Name(), ".log"); ok {
 				err = os.Remove(util.GetPath(3) + fi.Name())
@@ -136,20 +143,45 @@ type MqttOnline struct {
 }
 
 func (o *MqttOnline) GetOnlineMsg() (string, string) {
-	//发布上线消息
-	var obj protocol.Pack_IDObject
-	str, err := obj.EnCode(appConfig.GID, GetNextUint64(), 0)
-	if err != nil {
-		return "", ""
-	}
-	return GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_ONLINE), str
+	// 手拼 JSON,避开 json-iterator ConfigFastest 的潜在 marshal 问题
+	seq := GetNextUint64()
+	now := protocol.BJNow().Format("2006-01-02 15:04:05")
+	payload := fmt.Sprintf(`{"id":"%s","seq":%d,"gid":"%s","time":"%s","data":{"id":0}}`,
+		appConfig.GID, seq, appConfig.GID, now)
+	topic := GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_ONLINE)
+	util.GetTagLog().Infof("sys", "GetOnlineMsg: topic=%s", topic)
+	return topic, payload
 }
 
 func (o *MqttOnline) GetWillMsg() (string, string) {
-	payload, _ := (&protocol.Pack_IDObject{}).EnCode(appConfig.GID, GetNextUint64(), 0) //遗嘱消息
+	seq := GetNextUint64()
+	now := protocol.BJNow().Format("2006-01-02 15:04:05")
+	payload := fmt.Sprintf(`{"id":"%s","seq":%d,"gid":"%s","time":"%s","data":{"id":0}}`,
+		appConfig.GID, seq, appConfig.GID, now)
 	return GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_WILL), payload
 }
 
+// Heartbeat 定期重发 online 消息,防止 QoS=0 丢包导致云端永久离线
+func Heartbeat(args ...interface{}) interface{} {
+	for {
+		time.Sleep(60 * time.Second)
+		mgr := GetMQTTMgr()
+		if mgr.Cloud != nil && mgr.Cloud.IsConnected() {
+			topic, str := (&MqttOnline{}).GetOnlineMsg()
+			if topic != "" {
+				if err := mgr.Cloud.PublishString(topic, str, 0); err != nil {
+					util.GetTagLog().Errorf("sys", "Heartbeat:发布online失败,topic=%s,err=%v", topic, err)
+				}
+			}
+		}
+	}
+}
+
+// HandleTpWDeploy 转换签名供 Subscribe 使用
+func HandleTpWDeploy(m mqtt.Message) {
+	HandleTpDeploy(m)
+}
+
 // InitCloudMqttSubscribeTopics 初始化网关级别的主题订阅及路由
 func InitCloudMqttSubscribeTopics() {
 	GetMQTTMgr().Subscribe(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_APP), mqtt.AtMostOnce, HandleTpQApp, ToCloud)
@@ -162,5 +194,7 @@ func InitCloudMqttSubscribeTopics() {
 	GetMQTTMgr().Subscribe(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_SET_MODEL), mqtt.AtMostOnce, HandleTpWModel, ToCloud)
 	GetMQTTMgr().Subscribe(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_LOG), mqtt.AtMostOnce, HandleTpQLog, ToCloud)
 	GetMQTTMgr().Subscribe(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_REMOVE_LOG), mqtt.AtMostOnce, HandleTpRLog, ToCloud)
+	GetMQTTMgr().Subscribe(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_LOG_CFG), mqtt.AtMostOnce, HandleTpLogCfg, ToCloud)
 	GetMQTTMgr().Subscribe(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_SYS), mqtt.AtMostOnce, HandleTpQSys, ToCloud)
+	GetMQTTMgr().Subscribe(GetTopic(protocol.DT_GATEWAY, appConfig.GID, protocol.TP_GW_DEPLOY), mqtt.AtMostOnce, HandleTpWDeploy, ToCloud)
 }

+ 0 - 183
edge/ipole/mqttmgr.go

@@ -1,183 +0,0 @@
-package main
-
-import (
-	"runtime/debug"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-	"lc/common/util"
-)
-
-type OptType uint8
-
-const (
-	ToAll   OptType = 0 //发布和订阅边缘端与云端的消息
-	ToCloud OptType = 1 //发布和订阅云端的消息
-	ToEdge  OptType = 2 //发布和订阅边缘端的消息
-)
-
-var _mqttMgronce sync.Once
-var _mqttMgrsingle *MQTTMgr
-
-func GetMQTTMgr() *MQTTMgr {
-	_mqttMgronce.Do(func() {
-		_mqttMgrsingle = _newMQTTMgr()
-	})
-	return _mqttMgrsingle
-}
-
-type MQTTMgr struct {
-	Cloud *MqttClient
-	Edge  *MqttClient
-	Queue *util.MlQueue
-}
-
-func _newMQTTMgr() *MQTTMgr {
-	mgr := &MQTTMgr{
-		Queue: util.NewQueue(2000),
-	}
-	if appConfig.Edge.Mqtt.Server != "" {
-		mgr.Edge = NewMqttClient(appConfig.Edge.Mqtt.Server,
-			appConfig.GID+"@"+appname+version,
-			appConfig.Edge.Mqtt.User,
-			appConfig.Edge.Mqtt.Password,
-			appConfig.Edge.Mqtt.Timeout,
-			&EmptyMqttOnline{})
-	}
-	if appConfig.Cloud.Mqtt.Server != "" {
-		mgr.Cloud = NewMqttClient(appConfig.Cloud.Mqtt.Server,
-			appConfig.GID+"@"+appname+version,
-			appConfig.Cloud.Mqtt.User,
-			appConfig.Cloud.Mqtt.Password,
-			appConfig.Cloud.Mqtt.Timeout,
-			&MqttOnline{})
-	}
-	return mgr
-}
-
-func (o *MQTTMgr) Subscribe(topic string, qos mqtt.QOS, handler mqtt.MessageHandler, tp OptType) {
-	switch tp {
-	case ToAll:
-		if o.Cloud != nil {
-			o.Cloud.Handle(topic, handler)
-			o.Cloud.Subscribe(topic, qos)
-		}
-		if o.Edge != nil {
-			o.Edge.Handle(topic, handler)
-			o.Edge.Subscribe(topic, qos)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			o.Cloud.Handle(topic, handler)
-			o.Cloud.Subscribe(topic, qos)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.Handle(topic, handler)
-			o.Edge.Subscribe(topic, qos)
-		}
-	}
-}
-
-func (o *MQTTMgr) UnSubscribe(topic string, tp OptType) {
-	switch tp {
-	case ToAll:
-		if o.Cloud != nil {
-			o.Cloud.Unsubscribe(topic)
-		}
-		if o.Edge != nil {
-			o.Edge.Unsubscribe(topic)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			o.Cloud.Unsubscribe(topic)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.Unsubscribe(topic)
-		}
-	}
-}
-
-func (o *MQTTMgr) Publish(topic string, payload string, qos mqtt.QOS, tp OptType) {
-	msg := MQTTMessage{
-		topic:   topic,
-		payload: payload,
-		qos:     qos,
-		tp:      tp,
-	}
-	o.Queue.Put(&msg)
-}
-
-func (o *MQTTMgr) _publish(msg *MQTTMessage) error {
-	var err error
-	switch msg.tp {
-	case ToAll:
-		if o.Cloud != nil {
-			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-		if o.Edge != nil {
-			o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	}
-	return err
-}
-
-func (o *MQTTMgr) MQTTConnectMgr(args ...interface{}) interface{} {
-	for {
-		time.Sleep(10 * time.Second)
-		//GetMonitorStatus().MQTTConnectMgr = time.Now() //更新状态
-		if o.Cloud != nil {
-			o.Cloud.Connect()
-		}
-		if o.Edge != nil {
-			o.Edge.Connect()
-		}
-	}
-}
-
-func (o *MQTTMgr) MQTTMessageHandle(args ...interface{}) interface{} {
-	defer func() {
-		if err := recover(); err != nil {
-			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常:%v", err)
-			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常,堆栈信息:%s", string(debug.Stack()))
-			gopool.Add(o.MQTTMessageHandle, args)
-		}
-	}()
-	var err error
-	for {
-		if m, ok, _ := o.Queue.Get(); ok {
-			if msg, ok := m.(*MQTTMessage); ok {
-			RETRY:
-				err = o._publish(msg)
-				if err != nil {
-					logrus.Errorf("发布主题为%s的消息失败,原因:%s", msg.topic, err.Error())
-					time.Sleep(time.Second)
-					goto RETRY
-				}
-			}
-		} else {
-			time.Sleep(200 * time.Millisecond)
-			//更新状态时间
-			//GetMonitorStatus().MQTTMessageHandle = time.Now()
-		}
-	}
-}
-
-type MQTTMessage struct {
-	topic   string
-	payload string
-	qos     mqtt.QOS
-	tp      OptType
-}

+ 7 - 7
edge/ipole/radar.go

@@ -2,8 +2,8 @@ package main
 
 //雷达
 import (
-	"github.com/sirupsen/logrus"
 	"lc/common/protocol"
+	"lc/common/util"
 	"lc/edge/ipole/modbus"
 	"math"
 	"net"
@@ -21,7 +21,7 @@ func RadarReceive(args ...interface{}) interface{} {
 		Port: PORT,
 	})
 	if err != nil {
-		logrus.Fatal("Listen failed,", err)
+		util.GetTagLog().Error("radar", "RadarReceive:UDP监听失败,", err)
 		return 0
 	}
 
@@ -29,19 +29,19 @@ func RadarReceive(args ...interface{}) interface{} {
 		var data [1024]byte
 		n, addr, err := UDPConn.ReadFromUDP(data[:]) //同步读
 		if err != nil {
-			logrus.Warningf("Read from udp server:%s failed,err:%s", addr, err)
+			util.GetTagLog().Warnf("radar", "Read from udp server:%s failed,err:%s", addr, err)
 			continue
 		}
-		logrus.Debugf("Read from udp server addr: %s", addr)
+		util.GetTagLog().Debugf("radar", "Read from udp server addr: %s", addr)
 		if b, sa := verify(data[:n-2]); !b {
-			logrus.Warningf("解析来自:%s 的数据出错,===%v ==== err:%s", addr, string(data[:n]), sa)
+			util.GetTagLog().Warnf("radar", "解析来自:%s 的数据出错,===%v ==== err:%s", addr, string(data[:n]), sa)
 			continue
 		} else {
 			l := len(sa)
 			for _, s := range sa[:l-1] {
 				d := strings.Split(s, ",")
 				if len(d) != 5 {
-					logrus.Warningf("解析的数据出错, 长度不为5")
+					util.GetTagLog().Warnf("radar", "解析的数据出错, 长度不为5")
 					continue
 				}
 				go func() {
@@ -54,7 +54,7 @@ func RadarReceive(args ...interface{}) interface{} {
 					data1[3] = math.Abs(data1[3])
 					var obj protocol.Pack_UploadData
 					if str, err := obj.EnCode(addr.String(), appConfig.GID, GetNextUint64(), nil, 0, data1); err == nil {
-						logrus.Debugf("Publish TP_RADAR_DATA")
+						util.GetTagLog().Debugf("radar", "Publish TP_RADAR_DATA")
 						topic := GetTopic(protocol.DT_Radar, appConfig.GID, protocol.TP_RADAR_DATA)
 						GetMQTTMgr().Publish(topic, str, 0, ToCloud) //上传消息 雷达
 					}

+ 16 - 4
edge/ipole/serialmgr.go

@@ -5,9 +5,9 @@ import (
 	"time"
 
 	"github.com/goburrow/serial"
-	"github.com/sirupsen/logrus"
 
 	"lc/common/protocol"
+	"lc/common/util"
 )
 
 var _once sync.Once
@@ -39,10 +39,10 @@ func openSerialPort(sp *protocol.SerialPort) (*serialPort, error) {
 	serial.SetAutoReconnect(SerialDefaultAutoReconnect)
 	err = serial.connect()
 	if err != nil {
-		logrus.Errorf("打开串口[code=%d,address=%s]失败:%s", sp.Code, sp.Address, err.Error())
+		util.GetTagLog().Errorf("sys", "打开串口[code=%d,address=%s]失败:%s", sp.Code, sp.Address, err.Error())
 		return nil, err
 	}
-	logrus.Infof("打开串口[code=%d,address=%s]成功", sp.Code, sp.Address)
+	util.GetTagLog().Infof("sys", "打开串口[code=%d,address=%s]成功", sp.Code, sp.Address)
 	return &serial, nil
 }
 
@@ -78,7 +78,7 @@ func (o *SerialMgr) UpdateSerialPort(sp *protocol.SerialPort) error {
 	if s, ok := o.mapSerial[sp.Code]; ok { //存在则关闭,并重新打开
 		err := s.Close() //关闭
 		if err != nil {
-			logrus.Errorf("关闭串口[code=%d,address=%s]失败:%s", sp.Code, sp.Address, err.Error())
+			util.GetTagLog().Errorf("sys", "关闭串口[code=%d,address=%s]失败:%s", sp.Code, sp.Address, err.Error())
 		}
 		delete(o.mapSerial, sp.Code)
 	}
@@ -120,3 +120,15 @@ func (o *SerialMgr) GetSerialPort(code uint8) *serialPort {
 	}
 	return nil
 }
+
+// GetFailedPorts 返回打开失败的串口号列表(供健康检测使用)
+func (o *SerialMgr) GetFailedPorts() []uint8 {
+	var failed []uint8
+	for portNum := range serialConfig.Serial {
+		addr := o.GetSerialPort(portNum)
+		if addr == nil {
+			failed = append(failed, portNum)
+		}
+	}
+	return failed
+}

+ 1 - 2
edge/ipole/uploaddata.go

@@ -2,7 +2,6 @@ package main
 
 import (
 	"fmt"
-	"github.com/sirupsen/logrus"
 	"lc/common/models"
 	"math"
 	"sort"
@@ -46,7 +45,7 @@ func NewRtuUploadManager(rtuinfo *protocol.DevInfo) *RtuUploadManager {
 	if rtuinfo.DevType == 4 && (rtuinfo.TID == 3 || rtuinfo.TID == 5) {
 		o.LiguidDataMgr = &LiguidDataMgr{}
 	}
-	logrus.Infof("NewRtuUploadManager rtuinfo = %v \n", rtuinfo)
+	util.GetTagLog().Infof("sys", "NewRtuUploadManager rtuinfo = %v \n", rtuinfo)
 	o.Timer.AddJobFunc(o.SendData, time.Duration(o.Rtuinfo.SendCloud)*time.Millisecond)
 	return &o
 }

+ 48 - 36
edge/ipole/ym485.go

@@ -7,7 +7,6 @@ import (
 	"time"
 
 	"github.com/go-redis/redis/v7"
-	"github.com/sirupsen/logrus"
 
 	"lc/common/mqtt"
 	"lc/common/protocol"
@@ -51,12 +50,12 @@ func NewYmLampController(info *protocol.DevInfo) Device {
 
 	iot, err := loadModel(info.TID)
 	if err != nil {
-		logrus.Errorf("NewYmLampController:加载模型[tid=%d]文件发生错误:%s", info.TID, err.Error())
+		util.GetTagLog().Errorf("ym485", "NewYmLampController:加载模型[tid=%d]文件发生错误:%s", info.TID, err.Error())
 	} else {
 		if iot.Protocol == YmProtocol {
 			dev.model = iot
 		} else {
-			logrus.Error("NewYmLampController:物模型错误,非YmProtocol协议")
+			util.GetTagLog().Error("ym485", "NewYmLampController:物模型错误,非YmProtocol协议")
 		}
 	}
 	dev.SetTopicHandle()
@@ -73,7 +72,7 @@ func (o *YmLampController) MQTTSubscribe() {
 	GetMQTTMgr().Subscribe(GetTopic(protocol.DT_LAMPCONTROLLER, o.devinfo.DevCode, protocol.TP_YM_SET_ONOFFTIME), mqtt.ExactlyOnce, o.HandleCache, ToCloud) //设置时间
 }
 func (o *YmLampController) HandleCache(m mqtt.Message) {
-	logrus.Infof("Topic:%v,message:%v\n", m.Topic(), m.PayloadString())
+	util.GetTagLog().Infof("ym485", "Topic:%v,message:%v\n", m.Topic(), m.PayloadString())
 	o.downQueue.Put(m)
 }
 func (o *YmLampController) Start() {
@@ -132,20 +131,20 @@ func (o *YmLampController) UpdateModel2(mi *ModelInfo) {
 		return
 	}
 	if mi.Flag == 0 {
-		logrus.Errorf("设备[%s]的物模型[tid=%d]模型文件被删除,下次启动即将生效。", o.devinfo.DevCode, mi.TID)
+		util.GetTagLog().Errorf("ym485", "设备[%s]的物模型[tid=%d]模型文件被删除,下次启动即将生效。", o.devinfo.DevCode, mi.TID)
 		return
 	}
-	logrus.Debugf("YmLampController.UpdateModel2:更新设备[%s]的物模型[%d]", o.devinfo.DevCode, mi.TID)
+	util.GetTagLog().Debugf("ym485", "YmLampController.UpdateModel2:更新设备[%s]的物模型[%d]", o.devinfo.DevCode, mi.TID)
 	iot, err := loadModel(mi.TID)
 	if err != nil {
-		logrus.Errorf("YmLampController.UpdateModel2:加载模型[%d]文件错误:%s", mi.TID, err.Error())
+		util.GetTagLog().Errorf("ym485", "YmLampController.UpdateModel2:加载模型[%d]文件错误:%s", mi.TID, err.Error())
 		return
 	}
-	if iot.Protocol == ModbusRtuProtocol { //合法的物模型
+	if iot.Protocol == YmProtocol { //合法的物模型
 		o.model = iot
-		logrus.Infof("YmLampController.UpdateModel2:更新设备[%s]的物模型[%d]成功", o.devinfo.DevCode, mi.TID)
+		util.GetTagLog().Infof("ym485", "YmLampController.UpdateModel2:更新设备[%s]的物模型[%d]成功", o.devinfo.DevCode, mi.TID)
 	} else {
-		logrus.Error("YmLampController.UpdateModel2:物模型错误,TID和文件名tid不一致或协议非ModbusRTU协议")
+		util.GetTagLog().Error("ym485", "YmLampController.UpdateModel2:物模型错误,TID和文件名tid不一致或协议非ModbusRTU协议")
 	}
 }
 
@@ -160,18 +159,19 @@ func (o *YmLampController) GetDevType() string {
 func (o *YmLampController) HandleData() {
 	defer func() {
 		if err := recover(); err != nil {
-			logrus.Error("YmLampController.HandleData:panic:", err)
-			logrus.Error("stack:", string(debug.Stack()))
+			util.GetTagLog().Error("ym485", "YmLampController.HandleData:panic:", err)
+			util.GetTagLog().Error("ym485", "stack:", string(debug.Stack()))
+			time.Sleep(5 * time.Second)
 			go o.HandleData()
 		}
 	}()
 	o.QueryLampControllerAddr() //线上挂了两个灯控,则不能
-	logrus.Infof("lamp state:%d", oldstate)
+	util.GetTagLog().Infof("ym485", "lamp state:%d", oldstate)
 	nextFillTime := time.Time{}
 	for {
 		select {
 		case <-o.ctx.Done():
-			logrus.Errorf("设备[%s]的HandleData退出,原因:%v", o.devinfo.DevCode, o.ctx.Err())
+			util.GetTagLog().Errorf("ym485", "设备[%s]的HandleData退出,原因:%v", o.devinfo.DevCode, o.ctx.Err())
 			return
 		case devinfo_ := <-o.chanDevInfo:
 			o.devinfo = devinfo_
@@ -184,7 +184,7 @@ func (o *YmLampController) HandleData() {
 					if fn, ok := o.mapTopicHandle[mm.Topic()]; ok {
 						fn(mm)
 					} else {
-						logrus.Errorf("YmLampController.Handle:不支持的主题:%s", mm.Topic())
+						util.GetTagLog().Errorf("ym485", "YmLampController.Handle:不支持的主题:%s", mm.Topic())
 					}
 				}
 			} else {
@@ -192,7 +192,10 @@ func (o *YmLampController) HandleData() {
 					t := util.MlNow()
 
 					o.QueryDeviceState() //获取灯的数据
-					o.ConfirmState(t)    //获取设置灯的开/关
+					// QueryDeviceState失败时State为过期数据,跳过ConfirmState避免误操作
+					if len(o.State) > 0 {
+						o.ConfirmState(t) //获取设置灯的开/关
+					}
 
 					nextFillTime = util.MlNow().Add(time.Duration(o.devinfo.SendCloud) * time.Millisecond)
 				}
@@ -209,8 +212,12 @@ func (o *YmLampController) SendRecvData(aduRequest []byte, retry int) (aduRespon
 	// time.Sleep(time.Millisecond * 10)
 	serial := GetSerialMgr().GetSerialPort(o.devinfo.Code)
 	if serial == nil {
+		util.GetTagLog().Errorf("ym485", "YM串口未找到: Code=%d", o.devinfo.Code) // ← 加这行
 		return nil, ErrClosedConnection
 	}
+	if !serial.IsConnected() {
+		util.GetTagLog().Errorf("ym485", "YM串口未连接: Code=%d", o.devinfo.Code) // ← 加这行
+	}
 	if retry <= 0 {
 		retry = 1
 	}
@@ -253,10 +260,10 @@ func (o *YmLampController) QueryLampControllerAddr() {
 	}
 	var ret ym485.QueryAddrACK
 	if err = ret.DeCode(recvbuf); err == nil {
-		logrus.Infof("灯控地址:%s", ret.Addr)
+		util.GetTagLog().Infof("ym485", "灯控地址:%s", ret.Addr)
 	}
 	if ret.Addr != o.devinfo.DevCode {
-		logrus.Errorf("请将DevCode配置为灯控地址!")
+		util.GetTagLog().Errorf("ym485", "请将DevCode配置为灯控地址!")
 	}
 }
 
@@ -273,7 +280,7 @@ func (o *YmLampController) QuerySoftVer() {
 	}
 	var ret ym485.QuerySoftVerAck
 	if err = ret.DeCode(recvbuf); err == nil {
-		logrus.Infof("灯控型号:%s,软件版本:%s", ret.Model, ret.Version)
+		util.GetTagLog().Infof("ym485", "灯控型号:%s,软件版本:%s", ret.Model, ret.Version)
 	}
 }
 
@@ -290,7 +297,7 @@ func (o *YmLampController) QueryBasicsettings() {
 	}
 	var ret ym485.PackingBasic
 	if err = ret.DeCode(recvbuf); err == nil {
-		logrus.Infof("灯控使能状态:%d,是否上电开灯:%d", ret.Enabled, ret.OnOff)
+		util.GetTagLog().Infof("ym485", "灯控使能状态:%d,是否上电开灯:%d", ret.Enabled, ret.OnOff)
 	}
 }
 
@@ -304,7 +311,7 @@ func (o *YmLampController) QueryDeviceState() {
 	}
 	recvbuf, err := o.SendRecvData(buf.Bytes(), 1) //根据地址得到数据
 	if err != nil {
-		logrus.Error("单灯状态数据查询错误")
+		util.GetTagLog().Errorf("ym485", "单灯状态数据查询错误,DevCode=%s,err=%v", o.devinfo.DevCode, err)
 		return
 	}
 
@@ -326,7 +333,7 @@ func (o *YmLampController) QueryDeviceState() {
 		//状态改变记录日志
 		if oldstate != o.State[0].State {
 			oldstate = o.State[0].State
-			logrus.Infof("lamp state:%d", oldstate)
+			util.GetTagLog().Infof("ym485", "lamp state:%d", oldstate)
 		}
 	}
 	mapData := make(map[string]*protocol.CHZB_LampData)
@@ -351,7 +358,8 @@ func (o *YmLampController) QueryDeviceState() {
 	mapData[o.devinfo.DevCode] = &data
 	var ret1 protocol.Pack_CHZB_UploadData
 	if str, err := ret1.EnCode(o.devinfo.DevCode, appConfig.GID, GetNextUint64(), o.devinfo.TID, mapData); err == nil {
-		GetMQTTMgr().Publish(GetTopic(protocol.DT_LAMPCONTROLLER, o.devinfo.DevCode, protocol.TP_YM_DATA), str, mqtt.AtMostOnce, ToCloud)
+		topic := GetTopic(protocol.DT_LAMPCONTROLLER, o.devinfo.DevCode, protocol.TP_YM_DATA)
+		GetMQTTMgr().Publish(topic, str, mqtt.AtLeastOnce, ToCloud)
 	}
 }
 
@@ -368,15 +376,17 @@ func (o *YmLampController) TurnOnOff(flag uint8) error {
 	}
 	recvbuf, err := o.SendRecvData(buf.Bytes(), 3)
 	if err != nil {
+		util.GetTagLog().Errorf("ym485", "TurnOnOff:串口通信失败,DevCode=%s,flag=%d,err=%v", o.devinfo.DevCode, flag, err)
 		return err
 	}
 	var ret ym485.PackingResult
 	err = ret.DeCode(recvbuf)
 	if err == nil {
-		if ret.Result == 89 { //字母'Y'
+		if ret.Result == 89 { //字母Y
 			return nil
 		}
 	}
+	util.GetTagLog().Errorf("ym485", "TurnOnOff:设备返回失败,DevCode=%s,flag=%d,Result=0x%02X", o.devinfo.DevCode, flag, ret.Result)
 	return errors.New(protocol.FAILED_STR)
 }
 
@@ -415,9 +425,9 @@ func (o *YmLampController) SetBasicsettings(enabled, on uint8) {
 	var ret ym485.PackingResult
 	if err = ret.DeCode(recvbuf); err == nil {
 		if ret.Result == 89 { //字母'Y'
-			logrus.Infoln("设置灯控基本信息执行成功")
+			util.GetTagLog().Info("ym485", "设置灯控基本信息执行成功")
 		} else {
-			logrus.Errorln("设置灯控基本信息执行失败")
+			util.GetTagLog().Error("ym485", "设置灯控基本信息执行失败")
 		}
 	}
 }
@@ -428,7 +438,7 @@ func (o *YmLampController) Switch(Switch, Brightness uint8) error {
 	} else {
 		err := o.SetBrightness(Brightness)
 		if err != nil {
-			logrus.Errorf("调节亮度失败:%s", err.Error())
+			util.GetTagLog().Errorf("ym485", "调节亮度失败:%s", err.Error())
 		}
 		return o.TurnOnOff(Switch)
 	}
@@ -488,7 +498,7 @@ func (o *YmLampController) ConfirmState(t time.Time) {
 func (o *YmLampController) HandleTpYmSetSwitch(m mqtt.Message) {
 	var obj protocol.Pack_CHZB_Switch
 	if err := obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("ym485", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	if obj.Id != o.devinfo.DevCode {
@@ -499,7 +509,9 @@ func (o *YmLampController) HandleTpYmSetSwitch(m mqtt.Message) {
 		brightness = obj.Data.Brightness
 	}
 	err := o.Switch(obj.Data.Switch, obj.Data.Brightness)
-	if err == nil {
+	if err != nil {
+		util.GetTagLog().Errorf("ym485", "HandleTpYmSetSwitch:开关灯失败,DevCode=%s,Switch=%d,err=%v", o.devinfo.DevCode, obj.Data.Switch, err)
+	} else {
 		o.tswitch = util.MlNow()
 		mapRedisTempLampsOOT := make(map[string]interface{}) //临时开关灯记录,用于排除异常亮灯正常亮灯的情况
 		ltr := LampTimeRange{
@@ -511,7 +523,7 @@ func (o *YmLampController) HandleTpYmSetSwitch(m mqtt.Message) {
 		o.mapTempLampsOOT = &ltr                         //内存
 		mapRedisTempLampsOOT[o.devinfo.DevCode] = ltrstr //redis
 		if err := redisEdgeData.HSet(LampSwitchPrefix+o.devinfo.DevCode, mapRedisTempLampsOOT).Err(); err != nil {
-			logrus.Errorf("手动开关灯时间设置[内容:%v]缓存到redis失败:%s", mapRedisTempLampsOOT, err.Error())
+			util.GetTagLog().Errorf("ym485", "手动开关灯时间设置[内容:%v]缓存到redis失败:%s", mapRedisTempLampsOOT, err.Error())
 		}
 	}
 	var ret protocol.Pack_Ack
@@ -523,14 +535,14 @@ func (o *YmLampController) HandleTpYmSetSwitch(m mqtt.Message) {
 func (o *YmLampController) HandleTpYmSetOnofftime(m mqtt.Message) {
 	var obj protocol.Pack_SetOnOffTime
 	if err := obj.DeCode(m.PayloadString()); err != nil {
-		logrus.Errorf("协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
+		util.GetTagLog().Errorf("ym485", "协议解析错误:%s,协议主题:%s,协议内容:%s", err.Error(), m.Topic(), m.PayloadString())
 		return
 	}
 	if obj.Id != o.devinfo.DevCode {
 		return
 	}
 	if len(obj.Data.OnOffTime) == 0 {
-		logrus.Errorf("Handle_TP_YM_SET_ONOFFTIME:错误,灯控编号[%v],时间段个数:%v", obj.Id, obj.Data.OnOffTime)
+		util.GetTagLog().Errorf("ym485", "Handle_TP_YM_SET_ONOFFTIME:错误,灯控编号[%v],时间段个数:%v", obj.Id, obj.Data.OnOffTime)
 		return
 	}
 	mapRedisOOT := make(map[string]interface{})
@@ -541,7 +553,7 @@ func (o *YmLampController) HandleTpYmSetOnofftime(m mqtt.Message) {
 	//持久缓存到redis,以便于重启后读取进内存中
 	err := redisEdgeData.HSet(LampOotPrefix+o.devinfo.DevCode, mapRedisOOT).Err()
 	if err != nil {
-		logrus.Errorf("灯控时间设置[内容:%v]缓存到redis失败:%s", mapRedisOOT, err.Error())
+		util.GetTagLog().Errorf("ym485", "灯控时间设置[内容:%v]缓存到redis失败:%s", mapRedisOOT, err.Error())
 	}
 	var ret protocol.Pack_Ack
 	if str, err := ret.EnCode(o.devinfo.DevCode, appConfig.GID, obj.Seq, err); err == nil {
@@ -555,7 +567,7 @@ func (o *YmLampController) ReloadOOTFromRedis() error {
 		if err == redis.Nil {
 			return nil
 		}
-		logrus.Errorf("YmLampController.ReloadOOTFromRedis设备[%s]从redis加载时间策略失败:%s", o.devinfo.DevCode, err.Error())
+		util.GetTagLog().Errorf("ym485", "YmLampController.ReloadOOTFromRedis设备[%s]从redis加载时间策略失败:%s", o.devinfo.DevCode, err.Error())
 		return err
 	}
 	for k, v := range mapdata {
@@ -576,7 +588,7 @@ func (o *YmLampController) ReloadSwitchOOTFromRedis() error {
 		if err == redis.Nil {
 			return nil
 		}
-		logrus.Errorf("YmLampController.ReloadSwitchOOTFromRedis设备[%s]从redis加载时间策略失败:%s", o.devinfo.DevCode, err.Error())
+		util.GetTagLog().Errorf("ym485", "YmLampController.ReloadSwitchOOTFromRedis设备[%s]从redis加载时间策略失败:%s", o.devinfo.DevCode, err.Error())
 		return err
 	}
 	for k, v := range mapdata {
@@ -597,7 +609,7 @@ func (o *YmLampController) ReloadLampAlarmFromRedis() error {
 		if err == redis.Nil {
 			return nil
 		}
-		logrus.Errorf("YmLampController.ReloadLampAlarmFromRedis设备[%s]从redis加载广播恢复截止时间失败:%s", o.devinfo.DevCode, err.Error())
+		util.GetTagLog().Errorf("ym485", "YmLampController.ReloadLampAlarmFromRedis设备[%s]从redis加载广播恢复截止时间失败:%s", o.devinfo.DevCode, err.Error())
 		return err
 	}
 	for k, v := range mapAlarm {

+ 1 - 0
edge/its/datamgr.go

@@ -170,6 +170,7 @@ func (o *ITSDeviceMgr) Handle(args ...interface{}) interface{} {
 		if err := recover(); err != nil {
 			logrus.Errorf("ITSDeviceMgr.HandleData发生异常:%v", err)
 			logrus.Errorf("ITSDeviceMgr.HandleData发生异常,堆栈信息:%s", string(debug.Stack()))
+			time.Sleep(time.Second)
 			gopool.Add(o.Handle, args)
 		}
 	}()

+ 6 - 6
edge/its/main.go

@@ -1,21 +1,19 @@
 package main
 
 import (
-	"math/rand"
 	"runtime"
-	"time"
 
 	"github.com/sirupsen/logrus"
 
 	"lc/common/util"
 )
 
-//这个网页是新建的, 建好之后,有4点需要处理:admin 168
+// 这个网页是新建的, 建好之后,有4点需要处理:admin 168
 // 1. 网关build的目录下有对应的index.html,
 // 2. 设制网关外网地址(如: http://192.168.0.63:9180/index.html)
 // 3. 新建的vsn文件重新发布;
 // 4. 节目管理中排优先播放
-//不是很确定,主要是云端这个以前设置的,而现在又没有这样的接口,可以先将云端的svn修改一下名称,再刚才的本地工具删除云端的节目再次发布一下本地的试试看
+// 不是很确定,主要是云端这个以前设置的,而现在又没有这样的接口,可以先将云端的svn修改一下名称,再刚才的本地工具删除云端的节目再次发布一下本地的试试看
 var IDGen util.IdWorker
 var gopool *util.Pool
 
@@ -31,7 +29,7 @@ func GetNextUint64() uint64 {
 	return uint64(u64)
 }
 
-//cgo学习资料:https://www.cnblogs.com/binHome/p/12984064.html
+// cgo学习资料:https://www.cnblogs.com/binHome/p/12984064.html
 func main() {
 	defer uninit()
 
@@ -58,9 +56,11 @@ func main() {
 	GetMQTTMgr()
 	GetWebSvr().MQTTSubscribe()
 	GetITSDeviceMgr().LoadITSDevice()
-	rand.Seed(time.Now().UnixNano())
 
 	gopool = util.NewPool(10)
+	GetMQTTMgr().SetRestartFn(func(fn func(args ...interface{}) interface{}, args ...interface{}) {
+		gopool.Add(fn, args)
+	})
 	gopool.Add(GetMQTTMgr().MQTTMessageHandle, 1)
 	gopool.Add(GetITSDeviceMgr().Handle, 2)
 	gopool.Add(GetWebSvr().StartSvr, 3)

+ 37 - 0
edge/its/mqtt_init.go

@@ -0,0 +1,37 @@
+package main
+
+import (
+	"sync"
+
+	"lc/common/mqtt"
+)
+
+type OptType = mqtt.OptType
+type MQTTMessage = mqtt.MQTTMessage
+
+const (
+	ToAll   = mqtt.ToAll
+	ToCloud = mqtt.ToCloud
+	ToEdge  = mqtt.ToEdge
+)
+
+var _mqttMgrOnce sync.Once
+var _mqttMgr *mqtt.MQTTMgr
+
+func GetMQTTMgr() *mqtt.MQTTMgr {
+	_mqttMgrOnce.Do(func() {
+		_mqttMgr = mqtt.NewMQTTMgr(mqtt.MQTTMgrConfig{
+			CloudServer:   appConfig.Cloud.Mqtt.Server,
+			CloudClientID: appConfig.GID + "@" + appname + version,
+			CloudUser:     appConfig.Cloud.Mqtt.User,
+			CloudPassword: appConfig.Cloud.Mqtt.Password,
+			CloudTimeout:  appConfig.Cloud.Mqtt.Timeout,
+			EdgeServer:    appConfig.Edge.Mqtt.Server,
+			EdgeClientID:  appConfig.GID + "@" + appname + version,
+			EdgeUser:      appConfig.Edge.Mqtt.User,
+			EdgePassword:  appConfig.Edge.Mqtt.Password,
+			EdgeTimeout:   appConfig.Edge.Mqtt.Timeout,
+		})
+	})
+	return _mqttMgr
+}

+ 0 - 125
edge/its/mqttclient.go

@@ -1,125 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-)
-
-type BaseMqttOnline interface {
-	GetOnlineMsg() (string, string)
-	GetWillMsg() (string, string)
-}
-
-type EmptyMqttOnline struct {
-}
-
-func (o *EmptyMqttOnline) GetOnlineMsg() (string, string) {
-	return "", ""
-}
-func (o *EmptyMqttOnline) GetWillMsg() (string, string) {
-	return "", ""
-}
-
-type MqttClient struct {
-	mqtt       *mqtt.Client        //
-	mu         sync.Mutex          //保护mapTopics
-	mapTopics  map[string]mqtt.QOS //订阅的主题
-	timeout    uint                //超时时间,毫秒为单位
-	MqttOnline BaseMqttOnline      //是否发布上线消息&遗嘱消息
-}
-
-func NewMqttClient(server, clientid, user, password string, timeout uint, mqttOnline BaseMqttOnline) *MqttClient {
-	o := MqttClient{
-		mapTopics:  make(map[string]mqtt.QOS),
-		timeout:    timeout,
-		MqttOnline: mqttOnline,
-	}
-	client, err := mqtt.NewClient(mqtt.ClientOptions{
-		Servers:       []string{server},
-		ClientID:      clientid,
-		Username:      user,
-		Password:      password,
-		AutoReconnect: true,
-	}, &o)
-	if err != nil {
-		panic(fmt.Sprintf("MQTT错误:", err.Error()))
-		return nil
-	}
-	o.mqtt = client
-	err = client.Connect(o.Ctx())
-	return &o
-}
-
-func (o *MqttClient) ConnectionLostHandler(err error) {
-	logrus.Errorln("MqttClient.ConnectionLostHandler:MQTT连接已经断开,原因:", err)
-}
-
-func (o *MqttClient) OnConnectHandler() {
-	logrus.Infoln("MqttClient.OnConnectHandler:MQTT连接成功")
-	//连接成功则订阅主题
-	for k, v := range o.mapTopics {
-		o.Subscribe(k, v)
-	}
-	topic, str := o.MqttOnline.GetOnlineMsg()
-	if topic != "" {
-		o.PublishString(topic, str, 0)
-	}
-}
-
-func (o *MqttClient) GetWill() (topic string, payload string) {
-	return o.MqttOnline.GetWillMsg()
-}
-
-func (o *MqttClient) Connect() error {
-	if !o.mqtt.IsConnected() {
-		return o.mqtt.Connect(o.Ctx())
-	}
-	return nil
-}
-
-func (o *MqttClient) IsConnected() bool {
-	return o.mqtt.IsConnected()
-}
-
-func (o *MqttClient) Publish(topic string, payload []byte, qos mqtt.QOS) error {
-	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishString(topic string, payload string, qos mqtt.QOS) error {
-	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishJSON(topic string, payload interface{}, qos mqtt.QOS) error {
-	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
-}
-
-func (o *MqttClient) Subscribe(topic string, qos mqtt.QOS) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; !ok {
-		o.mapTopics[topic] = qos
-	}
-	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
-}
-
-func (o *MqttClient) Unsubscribe(topic string) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; ok {
-		delete(o.mapTopics, topic)
-	}
-	return o.mqtt.Unsubscribe(o.Ctx(), topic)
-}
-
-func (o *MqttClient) Handle(topic string, handler mqtt.MessageHandler) mqtt.Route {
-	return o.mqtt.Handle(topic, handler)
-}
-
-func (o *MqttClient) Ctx() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.timeout))
-	return ctx
-}

+ 0 - 180
edge/its/mqttmgr.go

@@ -1,180 +0,0 @@
-package main
-
-import (
-	"runtime/debug"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-	"lc/common/util"
-)
-
-type OptType uint8
-
-const (
-	ToAll   OptType = 0 //发布和订阅边缘端与云端的消息
-	ToCloud OptType = 1 //发布和订阅云端的消息
-	ToEdge  OptType = 2 //发布和订阅边缘端的消息
-)
-
-var _mqttMgronce sync.Once
-var _mqttMgrsingle *MQTTMgr
-
-func GetMQTTMgr() *MQTTMgr {
-	_mqttMgronce.Do(func() {
-		_mqttMgrsingle = _newMQTTMgr()
-	})
-	return _mqttMgrsingle
-}
-
-type MQTTMgr struct {
-	Cloud *MqttClient
-	Edge  *MqttClient
-	Queue *util.MlQueue
-}
-
-func _newMQTTMgr() *MQTTMgr {
-	mgr := &MQTTMgr{
-		Queue: util.NewQueue(2000),
-	}
-	if appConfig.Edge.Mqtt.Server != "" {
-		mgr.Edge = NewMqttClient(appConfig.Edge.Mqtt.Server,
-			appConfig.GID+"@"+appname+version,
-			appConfig.Edge.Mqtt.User,
-			appConfig.Edge.Mqtt.Password,
-			appConfig.Edge.Mqtt.Timeout,
-			&EmptyMqttOnline{})
-	}
-	if appConfig.Cloud.Mqtt.Server != "" {
-		mgr.Cloud = NewMqttClient(appConfig.Cloud.Mqtt.Server,
-			appConfig.GID+"@"+appname+version,
-			appConfig.Cloud.Mqtt.User,
-			appConfig.Cloud.Mqtt.Password,
-			appConfig.Cloud.Mqtt.Timeout,
-			&EmptyMqttOnline{})
-	}
-	return mgr
-}
-
-func (o *MQTTMgr) Subscribe(topic string, qos mqtt.QOS, handler mqtt.MessageHandler, tp OptType) {
-	switch tp {
-	case ToAll:
-		if o.Cloud != nil {
-			o.Cloud.Handle(topic, handler)
-			o.Cloud.Subscribe(topic, qos)
-		}
-		if o.Edge != nil {
-			o.Edge.Handle(topic, handler)
-			o.Edge.Subscribe(topic, qos)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			o.Cloud.Handle(topic, handler)
-			o.Cloud.Subscribe(topic, qos)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.Handle(topic, handler)
-			o.Edge.Subscribe(topic, qos)
-		}
-	}
-}
-
-func (o *MQTTMgr) UnSubscribe(topic string, tp OptType) {
-	switch tp {
-	case ToAll:
-		if o.Cloud != nil {
-			o.Cloud.Unsubscribe(topic)
-		}
-		if o.Edge != nil {
-			o.Edge.Unsubscribe(topic)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			o.Cloud.Unsubscribe(topic)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.Unsubscribe(topic)
-		}
-	}
-}
-
-func (o *MQTTMgr) Publish(topic string, payload string, qos mqtt.QOS, tp OptType) {
-	msg := MQTTMessage{
-		topic:   topic,
-		payload: payload,
-		qos:     qos,
-		tp:      tp,
-	}
-	o.Queue.Put(&msg)
-}
-
-func (o *MQTTMgr) _publish(msg *MQTTMessage) error {
-	var err error
-	switch msg.tp {
-	case ToAll:
-		if o.Cloud != nil {
-			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-		if o.Edge != nil {
-			o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	}
-	return err
-}
-
-func (o *MQTTMgr) MQTTConnectMgr(args ...interface{}) interface{} {
-	for {
-		time.Sleep(10 * time.Second)
-		if o.Cloud != nil {
-			o.Cloud.Connect()
-		}
-		if o.Edge != nil {
-			o.Edge.Connect()
-		}
-	}
-}
-
-func (o *MQTTMgr) MQTTMessageHandle(args ...interface{}) interface{} {
-	defer func() {
-		if err := recover(); err != nil {
-			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常:%v", err)
-			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常,堆栈信息:%s", string(debug.Stack()))
-			gopool.Add(o.MQTTMessageHandle, args)
-		}
-	}()
-	var err error
-	for {
-		if m, ok, _ := o.Queue.Get(); ok {
-			if msg, ok := m.(*MQTTMessage); ok {
-			RETRY:
-				err = o._publish(msg)
-				if err != nil {
-					logrus.Errorf("发布主题为%s的消息失败,原因:%s", msg.topic, err.Error())
-					time.Sleep(time.Second)
-					goto RETRY
-				}
-			}
-		} else {
-			time.Sleep(200 * time.Millisecond)
-		}
-	}
-}
-
-type MQTTMessage struct {
-	topic   string
-	payload string
-	qos     mqtt.QOS
-	tp      OptType
-}

+ 2 - 1
edge/its/websvr.go

@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"errors"
-	"fmt"
 	"net/http"
 	"runtime/debug"
 	"strconv"
@@ -156,6 +155,7 @@ func (o *WebSvr) Handle(args ...interface{}) interface{} {
 		if err := recover(); err != nil {
 			logrus.Errorf("WebSvr.StartSvr发生异常:%v", err)
 			logrus.Errorf("WebSvr.StartSvr发生异常,堆栈信息:%s", string(debug.Stack()))
+			time.Sleep(time.Second)
 			gopool.Add(o.StartSvr, args)
 		}
 	}()
@@ -197,6 +197,7 @@ func (o *WebSvr) StartSvr(args ...interface{}) interface{} {
 		if err := recover(); err != nil {
 			logrus.Errorf("WebSvr.StartSvr发生异常:%v", err)
 			logrus.Errorf("WebSvr.StartSvr发生异常,堆栈信息:%s", string(debug.Stack()))
+			time.Sleep(time.Second)
 			gopool.Add(o.StartSvr, args)
 		}
 	}()

+ 3 - 0
edge/its_win/main.go

@@ -43,6 +43,9 @@ func main() {
 	GetWebSvr().MQTTSubscribe()
 
 	gopool = util.NewPool(10)
+	GetMQTTMgr().SetRestartFn(func(fn func(args ...interface{}) interface{}, args ...interface{}) {
+		gopool.Add(fn, args)
+	})
 	gopool.Add(GetMQTTMgr().MQTTMessageHandle, 1)
 	gopool.Add(GetWebSvr().StartSvr, 3)
 	gopool.Add(GetWebSvr().Handle, 4)

+ 37 - 0
edge/its_win/mqtt_init.go

@@ -0,0 +1,37 @@
+package main
+
+import (
+	"sync"
+
+	"lc/common/mqtt"
+)
+
+type OptType = mqtt.OptType
+type MQTTMessage = mqtt.MQTTMessage
+
+const (
+	ToAll   = mqtt.ToAll
+	ToCloud = mqtt.ToCloud
+	ToEdge  = mqtt.ToEdge
+)
+
+var _mqttMgrOnce sync.Once
+var _mqttMgr *mqtt.MQTTMgr
+
+func GetMQTTMgr() *mqtt.MQTTMgr {
+	_mqttMgrOnce.Do(func() {
+		_mqttMgr = mqtt.NewMQTTMgr(mqtt.MQTTMgrConfig{
+			CloudServer:   appConfig.Cloud.Mqtt.Server,
+			CloudClientID: appConfig.GID + "@" + appname + version,
+			CloudUser:     appConfig.Cloud.Mqtt.User,
+			CloudPassword: appConfig.Cloud.Mqtt.Password,
+			CloudTimeout:  appConfig.Cloud.Mqtt.Timeout,
+			EdgeServer:    appConfig.Edge.Mqtt.Server,
+			EdgeClientID:  appConfig.GID + "@" + appname + version,
+			EdgeUser:      appConfig.Edge.Mqtt.User,
+			EdgePassword:  appConfig.Edge.Mqtt.Password,
+			EdgeTimeout:   appConfig.Edge.Mqtt.Timeout,
+		})
+	})
+	return _mqttMgr
+}

+ 0 - 125
edge/its_win/mqttclient.go

@@ -1,125 +0,0 @@
-package main
-
-import (
-	"context"
-	"fmt"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-)
-
-type BaseMqttOnline interface {
-	GetOnlineMsg() (string, string)
-	GetWillMsg() (string, string)
-}
-
-type EmptyMqttOnline struct {
-}
-
-func (o *EmptyMqttOnline) GetOnlineMsg() (string, string) {
-	return "", ""
-}
-func (o *EmptyMqttOnline) GetWillMsg() (string, string) {
-	return "", ""
-}
-
-type MqttClient struct {
-	mqtt       *mqtt.Client        //
-	mu         sync.Mutex          //保护mapTopics
-	mapTopics  map[string]mqtt.QOS //订阅的主题
-	timeout    uint                //超时时间,毫秒为单位
-	MqttOnline BaseMqttOnline      //是否发布上线消息&遗嘱消息
-}
-
-func NewMqttClient(server, clientid, user, password string, timeout uint, mqttOnline BaseMqttOnline) *MqttClient {
-	o := MqttClient{
-		mapTopics:  make(map[string]mqtt.QOS),
-		timeout:    timeout,
-		MqttOnline: mqttOnline,
-	}
-	client, err := mqtt.NewClient(mqtt.ClientOptions{
-		Servers:       []string{server},
-		ClientID:      clientid,
-		Username:      user,
-		Password:      password,
-		AutoReconnect: true,
-	}, &o)
-	if err != nil {
-		panic(fmt.Sprintf("MQTT错误:%v", err.Error()))
-		return nil
-	}
-	o.mqtt = client
-	err = client.Connect(o.Ctx())
-	return &o
-}
-
-func (o *MqttClient) ConnectionLostHandler(err error) {
-	logrus.Errorln("MqttClient.ConnectionLostHandler:MQTT连接已经断开,原因:", err)
-}
-
-func (o *MqttClient) OnConnectHandler() {
-	logrus.Infoln("MqttClient.OnConnectHandler:MQTT连接成功")
-	//连接成功则订阅主题
-	for k, v := range o.mapTopics {
-		o.Subscribe(k, v)
-	}
-	topic, str := o.MqttOnline.GetOnlineMsg()
-	if topic != "" {
-		o.PublishString(topic, str, 0)
-	}
-}
-
-func (o *MqttClient) GetWill() (topic string, payload string) {
-	return o.MqttOnline.GetWillMsg()
-}
-
-func (o *MqttClient) Connect() error {
-	if !o.mqtt.IsConnected() {
-		return o.mqtt.Connect(o.Ctx())
-	}
-	return nil
-}
-
-func (o *MqttClient) IsConnected() bool {
-	return o.mqtt.IsConnected()
-}
-
-func (o *MqttClient) Publish(topic string, payload []byte, qos mqtt.QOS) error {
-	return o.mqtt.Publish(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishString(topic string, payload string, qos mqtt.QOS) error {
-	return o.mqtt.PublishString(o.Ctx(), topic, payload, qos)
-}
-func (o *MqttClient) PublishJSON(topic string, payload interface{}, qos mqtt.QOS) error {
-	return o.mqtt.PublishJSON(o.Ctx(), topic, payload, qos)
-}
-
-func (o *MqttClient) Subscribe(topic string, qos mqtt.QOS) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; !ok {
-		o.mapTopics[topic] = qos
-	}
-	return o.mqtt.Subscribe(o.Ctx(), topic, qos)
-}
-
-func (o *MqttClient) Unsubscribe(topic string) error {
-	o.mu.Lock()
-	defer o.mu.Unlock()
-	if _, ok := o.mapTopics[topic]; ok {
-		delete(o.mapTopics, topic)
-	}
-	return o.mqtt.Unsubscribe(o.Ctx(), topic)
-}
-
-func (o *MqttClient) Handle(topic string, handler mqtt.MessageHandler) mqtt.Route {
-	return o.mqtt.Handle(topic, handler)
-}
-
-func (o *MqttClient) Ctx() context.Context {
-	ctx, _ := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(o.timeout))
-	return ctx
-}

+ 0 - 180
edge/its_win/mqttmgr.go

@@ -1,180 +0,0 @@
-package main
-
-import (
-	"runtime/debug"
-	"sync"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"lc/common/mqtt"
-	"lc/common/util"
-)
-
-type OptType uint8
-
-const (
-	ToAll   OptType = 0 //发布和订阅边缘端与云端的消息
-	ToCloud OptType = 1 //发布和订阅云端的消息
-	ToEdge  OptType = 2 //发布和订阅边缘端的消息
-)
-
-var _mqttMgronce sync.Once
-var _mqttMgrsingle *MQTTMgr
-
-func GetMQTTMgr() *MQTTMgr {
-	_mqttMgronce.Do(func() {
-		_mqttMgrsingle = _newMQTTMgr()
-	})
-	return _mqttMgrsingle
-}
-
-type MQTTMgr struct {
-	Cloud *MqttClient
-	Edge  *MqttClient
-	Queue *util.MlQueue
-}
-
-func _newMQTTMgr() *MQTTMgr {
-	mgr := &MQTTMgr{
-		Queue: util.NewQueue(2000),
-	}
-	if appConfig.Edge.Mqtt.Server != "" {
-		mgr.Edge = NewMqttClient(appConfig.Edge.Mqtt.Server,
-			appConfig.GID+"@"+appname+version,
-			appConfig.Edge.Mqtt.User,
-			appConfig.Edge.Mqtt.Password,
-			appConfig.Edge.Mqtt.Timeout,
-			&EmptyMqttOnline{})
-	}
-	if appConfig.Cloud.Mqtt.Server != "" {
-		mgr.Cloud = NewMqttClient(appConfig.Cloud.Mqtt.Server,
-			appConfig.GID+"@"+appname+version,
-			appConfig.Cloud.Mqtt.User,
-			appConfig.Cloud.Mqtt.Password,
-			appConfig.Cloud.Mqtt.Timeout,
-			&EmptyMqttOnline{})
-	}
-	return mgr
-}
-
-func (o *MQTTMgr) Subscribe(topic string, qos mqtt.QOS, handler mqtt.MessageHandler, tp OptType) {
-	switch tp {
-	case ToAll:
-		if o.Cloud != nil {
-			o.Cloud.Handle(topic, handler)
-			o.Cloud.Subscribe(topic, qos)
-		}
-		if o.Edge != nil {
-			o.Edge.Handle(topic, handler)
-			o.Edge.Subscribe(topic, qos)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			o.Cloud.Handle(topic, handler)
-			o.Cloud.Subscribe(topic, qos)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.Handle(topic, handler)
-			o.Edge.Subscribe(topic, qos)
-		}
-	}
-}
-
-func (o *MQTTMgr) UnSubscribe(topic string, tp OptType) {
-	switch tp {
-	case ToAll:
-		if o.Cloud != nil {
-			o.Cloud.Unsubscribe(topic)
-		}
-		if o.Edge != nil {
-			o.Edge.Unsubscribe(topic)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			o.Cloud.Unsubscribe(topic)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.Unsubscribe(topic)
-		}
-	}
-}
-
-func (o *MQTTMgr) Publish(topic string, payload string, qos mqtt.QOS, tp OptType) {
-	msg := MQTTMessage{
-		topic:   topic,
-		payload: payload,
-		qos:     qos,
-		tp:      tp,
-	}
-	o.Queue.Put(&msg)
-}
-
-func (o *MQTTMgr) _publish(msg *MQTTMessage) error {
-	var err error
-	switch msg.tp {
-	case ToAll:
-		if o.Cloud != nil {
-			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-		if o.Edge != nil {
-			o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	case ToCloud:
-		if o.Cloud != nil {
-			err = o.Cloud.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	case ToEdge:
-		if o.Edge != nil {
-			o.Edge.PublishString(msg.topic, msg.payload, msg.qos)
-		}
-	}
-	return err
-}
-
-func (o *MQTTMgr) MQTTConnectMgr(args ...interface{}) interface{} {
-	for {
-		time.Sleep(10 * time.Second)
-		if o.Cloud != nil {
-			o.Cloud.Connect()
-		}
-		if o.Edge != nil {
-			o.Edge.Connect()
-		}
-	}
-}
-
-func (o *MQTTMgr) MQTTMessageHandle(args ...interface{}) interface{} {
-	defer func() {
-		if err := recover(); err != nil {
-			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常:%v", err)
-			logrus.Errorf("MQTTMgr.MQTTMessageHandle发生异常,堆栈信息:%s", string(debug.Stack()))
-			gopool.Add(o.MQTTMessageHandle, args)
-		}
-	}()
-	var err error
-	for {
-		if m, ok, _ := o.Queue.Get(); ok {
-			if msg, ok := m.(*MQTTMessage); ok {
-			RETRY:
-				err = o._publish(msg)
-				if err != nil {
-					logrus.Errorf("发布主题为%s的消息失败,原因:%s", msg.topic, err.Error())
-					time.Sleep(time.Second)
-					goto RETRY
-				}
-			}
-		} else {
-			time.Sleep(200 * time.Millisecond)
-		}
-	}
-}
-
-type MQTTMessage struct {
-	topic   string
-	payload string
-	qos     mqtt.QOS
-	tp      OptType
-}

+ 2 - 1
edge/its_win/websvr.go

@@ -3,7 +3,6 @@ package main
 import (
 	"context"
 	"errors"
-	"fmt"
 	"net/http"
 	"runtime/debug"
 	"strconv"
@@ -156,6 +155,7 @@ func (o *WebSvr) Handle(args ...interface{}) interface{} {
 		if err := recover(); err != nil {
 			logrus.Errorf("WebSvr.StartSvr发生异常:%v", err)
 			logrus.Errorf("WebSvr.StartSvr发生异常,堆栈信息:%s", string(debug.Stack()))
+			time.Sleep(time.Second)
 			gopool.Add(o.StartSvr, args)
 		}
 	}()
@@ -196,6 +196,7 @@ func (o *WebSvr) StartSvr(args ...interface{}) interface{} {
 		if err := recover(); err != nil {
 			logrus.Errorf("WebSvr.StartSvr发生异常:%v", err)
 			logrus.Errorf("WebSvr.StartSvr发生异常,堆栈信息:%s", string(debug.Stack()))
+			time.Sleep(time.Second)
 			gopool.Add(o.StartSvr, args)
 		}
 	}()

+ 3 - 3
edge/led_screen/clt_client/delete.go

@@ -2,7 +2,7 @@ package clt_client
 
 import (
 	"github.com/sirupsen/logrus"
-	"io/ioutil"
+	"io"
 	"lc/common/mqtt"
 	"net/http"
 )
@@ -23,7 +23,7 @@ func (c cltclient) DelPrograms(m mqtt.Message, baseurl string) {
 			return
 		}
 		defer resp.Body.Close()
-		ioutil.ReadAll(resp.Body)
+		io.ReadAll(resp.Body)
 	}
 }
 
@@ -40,5 +40,5 @@ func (c cltclient) CleanAll(baseurl string) {
 		return
 	}
 	defer resp.Body.Close()
-	ioutil.ReadAll(resp.Body)
+	io.ReadAll(resp.Body)
 }

+ 3 - 3
edge/led_screen/clt_client/get.go

@@ -2,7 +2,7 @@ package clt_client
 
 import (
 	"github.com/sirupsen/logrus"
-	"io/ioutil"
+	"io"
 	"net/http"
 )
 
@@ -15,7 +15,7 @@ func (c cltclient) SnapShot(url string) []byte {
 		return nil
 	}
 	defer resp.Body.Close()
-	body, err := ioutil.ReadAll(resp.Body)
+	body, err := io.ReadAll(resp.Body)
 	if err != nil {
 		logrus.Errorf("查询结果body读取错误:%v", err)
 		return nil
@@ -31,7 +31,7 @@ func (c cltclient) CommonGet(url string) ([]byte, error) {
 		return nil, err
 	}
 	defer resp.Body.Close()
-	all, err := ioutil.ReadAll(resp.Body)
+	all, err := io.ReadAll(resp.Body)
 	if err != nil {
 		return nil, err
 	}

+ 2 - 3
edge/led_screen/clt_client/post.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"github.com/sirupsen/logrus"
 	"io"
-	"io/ioutil"
 	"lc/common/mqtt"
 	"mime/multipart"
 	"net/http"
@@ -37,7 +36,7 @@ DoReq:
 	n := 0
 	resp, err := c.HttpClient.Do(req)
 	defer resp.Body.Close()
-	ioutil.ReadAll(resp.Body)
+	io.ReadAll(resp.Body)
 	if err != nil && n < 3 {
 		logrus.Errorf("节目发布http错误:%v, 消息内容: %+v", err, pro)
 		n++
@@ -97,6 +96,6 @@ func (c cltclient) CommonPost(payload []byte, url string) error {
 		return err
 	}
 	defer resp.Body.Close()
-	ioutil.ReadAll(resp.Body)
+	io.ReadAll(resp.Body)
 	return nil
 }

+ 2 - 3
edge/led_screen/clt_client/put.go

@@ -5,7 +5,6 @@ import (
 	"errors"
 	"github.com/sirupsen/logrus"
 	"io"
-	"io/ioutil"
 	"lc/common/mqtt"
 	"net/http"
 )
@@ -27,7 +26,7 @@ func (c cltclient) SwitchProgram(m mqtt.Message, baseurl string) {
 		return
 	}
 	defer resp.Body.Close()
-	ioutil.ReadAll(resp.Body)
+	io.ReadAll(resp.Body)
 }
 
 // brightness
@@ -45,6 +44,6 @@ func (c cltclient) CommonPut(payload []byte, url string) error {
 		return err
 	}
 	defer resp.Body.Close()
-	ioutil.ReadAll(resp.Body)
+	io.ReadAll(resp.Body)
 	return nil
 }

+ 0 - 0
edge/led_screen/led_device.go


Some files were not shown because too many files changed in this diff