package maincontrollerClient import ( "encoding/hex" "encoding/json" "fmt" "github.com/towgo/towgo/errors/terror" "github.com/towgo/towgo/towgo" "go.uber.org/zap" "sync" g "tgk-touch/internal/global" ) var controllerClient *MainControllerClient type Device struct { ID int64 `json:"id" xorm:"pk"` SerialNumber string `json:"serial_number" xorm:"index unique"` Name string `json:"name"` Classify string `json:"classify"` IsOnline bool `json:"is_online" xorm:"-"` LastSubsistTime int64 `json:"last_subsist_time"` UpdatedAt int64 `xorm:"updated"` CreatedAt int64 `xorm:"created"` } func ControllerClient() *MainControllerClient { if controllerClient == nil { controllerClient = NewMainControllerClient(MainControllerClientConfig{ ServerHost: g.Config().MainControllerClient.ServerHost, SecretKey: g.Config().MainControllerClient.SecretKey, }) } return controllerClient } type MainControllerClientConfig struct { ServerHost string `json:"server_host"` SecretKey string `json:"secret_key"` } type MainControllerClient struct { MainControllerClientConfig *towgo.WebScoketClient } func NewMainControllerClient(config MainControllerClientConfig) *MainControllerClient { g.Log().Info("初始化主控客户端") ws := towgo.NewWebsocketClient(config.ServerHost, config.ServerHost) m := &MainControllerClient{} m.MainControllerClientConfig = config m.WebScoketClient = ws m.CallTimeOut = 10 m.OnConnect = func(wsc *towgo.WebScoketClient) { m.mainControllerClientReg(wsc) } m.OnClose = func(wsc *towgo.WebScoketClient) {} m.Connect() return m } // 服务注册 func (m *MainControllerClient) mainControllerClientReg(wsc *towgo.WebScoketClient) { g.Log().Info("注册主控客户端到服务端") request := towgo.NewJsonrpcrequest() request.Method = "/iotserver/maincontroller/reg" request.Params = m.MainControllerClientConfig wsc.Call(request, func(jrc towgo.JsonRpcConnection) { if jrc.GetRpcResponse().Error.Code != 200 { g.Log().Errorf("mainControllerClientReg call err {%v}", jrc.GetRpcResponse().Error.Message) } }) request.Await() } func getParamsType(params any) string { switch params.(type) { case []byte: return "[]byte" } return "" } // 定义日志格式化回调类型,由调用者自定义友好显示内容 type LogFormatter func(deviceID, method string, params, result interface{}) (deviceType, cmdDetail, cmdDisplay, resultDisplay string) // 带日志记录的设备调用函数,新增formatter参数用于自定义日志格式 func DeviceCallByDeviceID(deviceID, method, token string, params, destResult any, formatter LogFormatter) error { var err error // 最多重试3次 for i := 0; i < 3; i++ { err = deviceCallByDeviceID(deviceID, method, token, params, destResult) if err == nil { break } } // 构建日志基础信息 logEntry := New(deviceID, params, destResult) if err != nil { logEntry.Error = err.Error() } // 应用自定义格式化(如果提供) if formatter != nil { devType, cmdDetail, cmdDisp, resultDisp := formatter(deviceID, method, params, destResult) if cmdDisp == "" { cmdDisp = formatDefault(params) } if resultDisp == "" { resultDisp = formatDefault(destResult) } logEntry.CmdDisply = cmdDisp logEntry.ResultDisply = resultDisp logEntry.DeviceType = devType logEntry.CmdDetail = cmdDetail } else { // 默认格式化(处理字节切片等特殊类型) logEntry.CmdDisply = formatDefault(params) logEntry.ResultDisply = formatDefault(destResult) } zap.S().Debugf("%+v", logEntry) return err } // formatDefault 默认格式化:优先用%q处理字符串类,特殊类型单独适配 func formatDefault(data interface{}) string { if data == nil { return "%q" // 明确标记nil,比"null"更符合%q的语义 } // 1. 处理字符串:直接用%q,自动转义特殊字符 if s, ok := data.(string); ok { return fmt.Sprintf("%q", s) } // 2. 处理字节切片:先转十六进制,再用%q包裹(避免乱码,且显式标记类型) if b, ok := data.([]byte); ok { // 若字节切片是可打印ASCII,优先转ASCII+%q;否则转十六进制 if isPrintableASCII(b) { return fmt.Sprintf("[]byte(ascii): %q", string(b)) } return fmt.Sprintf("[]byte(hex): %q", hex.EncodeToString(b)) } // 3. 处理符文(rune):用%q显示字符+Unicode码点 if r, ok := data.(rune); ok { return fmt.Sprintf("rune: %q (U+%04X)", r, r) } // 4. 复杂类型(结构体/切片/映射):用JSON序列化,字符串字段自动带%q效果 // (JSON序列化会自动转义特殊字符,等价于%q的最小化转义) jsonBytes, err := json.MarshalIndent(data, "", " ") if err == nil { if string(jsonBytes) != "null" && string(jsonBytes) != "" { return fmt.Sprintf("json: %q", string(jsonBytes)) // 用%q包裹JSON字符串,显式转义双引号 } } // 5. 其他基础类型(int/bool/float等):用%+v(%q会多一层引号,冗余) return fmt.Sprintf("%+v", data) } // 辅助函数:判断字节切片是否全是可打印ASCII(32-126) func isPrintableASCII(b []byte) bool { for _, c := range b { if c < 32 || c > 126 { return false } } return true } var pushDeviceLock sync.Mutex func deviceCallByDeviceID(deviceID, method, token string, params, destResult any) error { if serialPortMode { pushDeviceLock.Lock() defer pushDeviceLock.Unlock() b, err := ToBytes(params) if err != nil { return err } result, err := serialPort.BIO(b) if err != nil { g.Log().Error("serialPort.BIO err:", err.Error()) return terror.Wrap(err, "serialPort.BIO result err") } b, err = json.Marshal(result) if err != nil { g.Log().Error("serialPort.BIO json.Marshal(result) err:", err.Error()) } return json.Unmarshal(b, destResult) } deviceRequest := towgo.NewJsonrpcrequest() deviceRequest.Method = method deviceRequest.Session = token deviceRequest.Params = params deviceRequest.DataType = getParamsType(params) mainControllerRequest := towgo.NewJsonrpcrequest() mainControllerRequest.Method = "/iotserver/maincontroller/device/call" mainControllerRequest.Params = deviceRequest mainControllerRequest.Route.DestAddr = deviceID var err error ControllerClient(). Call(mainControllerRequest, func(jrc towgo.JsonRpcConnection) { //g.Log().Debug(jrc.GetRpcResponse()) if jrc.GetRpcResponse().Error.Code != 200 { err = terror.Newf("deviceId [%s],msg %s", deviceID, jrc.GetRpcResponse().Error.Message) return } jrc.ReadResult(destResult) }) mainControllerRequest.Await() return err } type DeviceControllerLog struct { DeviceId string `json:"device_id" xorm:"comment('网关Id')" ` DeviceType string `json:"device_type,omitempty" xorm:"comment('设备类型')"` CmdDetail string `json:"cmd_detail" xorm:"text comment('命令描述')"` Cmd string `json:"cmd" xorm:"text notnull comment('指令%q字符串')" ` Result string `json:"result" xorm:"text notnull comment('结果%q字符串')" ` CmdDisply string `json:"cmd_disply" xorm:"text comment('命令友好显示')"` ResultDisply string `json:"result_disply" xorm:"text comment('结果友好显示')"` Error string `json:"error" xorm:" varchar(255) comment('指令执行异常')" ` } func (DeviceControllerLog) TableName() string { return "device_controller_log" } func New(deviceId string, cmd, result interface{}) *DeviceControllerLog { return &DeviceControllerLog{ DeviceId: deviceId, Cmd: formatDefault(cmd), Result: formatDefault(result), } }