|
|
package maincontrollerClient
|
|
|
|
|
|
import (
|
|
|
"encoding/hex"
|
|
|
"encoding/json"
|
|
|
"fmt"
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
|
"github.com/towgo/towgo/towgo"
|
|
|
"go.uber.org/zap"
|
|
|
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
|
|
|
|
|
|
err = deviceCallByDeviceID(deviceID, method, token, params, destResult)
|
|
|
// 构建日志基础信息
|
|
|
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
|
|
|
}
|
|
|
|
|
|
func deviceCallByDeviceID(deviceID, method, token string, params, destResult any) error {
|
|
|
if serialPortMode {
|
|
|
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 gerror.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 gerror.Wrap(err, "json.Marshal(result) err")
|
|
|
}
|
|
|
|
|
|
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 = gerror.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),
|
|
|
}
|
|
|
}
|