From f506dfdbcdb84aec67db358cc9649456b8c22751 Mon Sep 17 00:00:00 2001 From: wangqiyang Date: Thu, 23 Jul 2026 14:53:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E5=AE=9E=E6=97=B6=E6=95=85?= =?UTF-8?q?=E9=9A=9C=E5=90=8E=E7=AB=AF=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/m9zCtrlTty/config/config-bak.json | 3 +- apps/m9zCtrlTty/config/config.json | 3 +- internal/library/m9z/m9z_SubLoopParameters.go | 84 +++++++++++- .../library/m9z/m9z_SubLoopParameters_test.go | 105 +++++++++++++++ internal/model/config/config.go | 1 + internal/module/m9zTtyApi/cron.go | 127 ++++++++---------- internal/module/m9zTtyApi/read.go | 74 +++++++--- internal/module/m9zTtyApi/scan.go | 19 +++ 8 files changed, 323 insertions(+), 93 deletions(-) create mode 100644 internal/library/m9z/m9z_SubLoopParameters_test.go create mode 100644 internal/module/m9zTtyApi/scan.go diff --git a/apps/m9zCtrlTty/config/config-bak.json b/apps/m9zCtrlTty/config/config-bak.json index 02eff1e..712a5c9 100644 --- a/apps/m9zCtrlTty/config/config-bak.json +++ b/apps/m9zCtrlTty/config/config-bak.json @@ -28,7 +28,8 @@ "deviceInfo": { "deviceId": "TGK", "version": "1.0.2", - "meterAddr": "0x04" + "meterAddr": "0x04", + "loopCount": 10 }, "MessageInterval": 15, "Firefox": { diff --git a/apps/m9zCtrlTty/config/config.json b/apps/m9zCtrlTty/config/config.json index f0eb270..c2fb1ae 100644 --- a/apps/m9zCtrlTty/config/config.json +++ b/apps/m9zCtrlTty/config/config.json @@ -32,7 +32,8 @@ "deviceId": "862809073336480", "version": "1.0.2", "meterAddr": "0x04", - "meterFirm": "ADL400" + "meterFirm": "ADL400", + "loopCount": 10 }, "MessageInterval": 15, "Firefox": { diff --git a/internal/library/m9z/m9z_SubLoopParameters.go b/internal/library/m9z/m9z_SubLoopParameters.go index 9eab20b..1e4221c 100644 --- a/internal/library/m9z/m9z_SubLoopParameters.go +++ b/internal/library/m9z/m9z_SubLoopParameters.go @@ -3,13 +3,16 @@ package m9z import ( "encoding/binary" "fmt" + "strconv" + "strings" + "github.com/gogf/gf/v2/util/gconv" ) // LoopCommonParameters 回路分控器公共参数结构体 (15个寄存器 = 30字节) type LoopCommonParameters struct { DCVoltage float32 // 直流电压 (0.1V单位) - DCCurrent float32 // 直流电流 (0.1A单位) + DCCurrent float32 // 直流电流 (0.001A单位) DimmingValue float32 // 调光幅度值 (0-100%) AlarmStatus uint16 // 告警状态 (位图形式) 告警状态寄存器 0x01欠压 0x02过压 0x08电源故障 其中:欠压=短路故障,过压=开路故障,电源故障。 AlarmCode string // 告警代码 @@ -42,6 +45,83 @@ type LoopFullParameters struct { Modules []LoopModuleParameters // 8个模块的参数 (208字节) } +type LoopFault struct { + LoopIdx uint `json:"loop_idx"` + DeviceIdx uint `json:"device_idx,omitempty"` + Type string `json:"type"` + Message string `json:"message"` + Code string `json:"code"` +} + +func BuildLoopFaults(loopIdx uint, alarmStatus uint16, alarmCode string) []LoopFault { + status := byte(alarmStatus) + if status == 0 { + return nil + } + + faults := make([]LoopFault, 0, 3) + if status&0x01 != 0 { + faults = append(faults, LoopFault{ + LoopIdx: loopIdx, + Type: "short_circuit", + Message: fmt.Sprintf("回路%d短路故障", loopIdx), + Code: "01", + }) + } + if status&0x02 != 0 { + faults = append(faults, LoopFault{ + LoopIdx: loopIdx, + Type: "open_circuit", + Message: fmt.Sprintf("回路%d开路故障", loopIdx), + Code: "02", + }) + } + if status&0x04 != 0 { + deviceMask := alarmDeviceMask(alarmCode) + faults = append(faults, LoopFault{ + LoopIdx: loopIdx, + Type: "power", + Message: buildPowerFaultMessage(loopIdx, deviceMask), + Code: fmt.Sprintf("04 %02X", deviceMask), + }) + } + + if unknownStatus := status &^ 0x07; unknownStatus != 0 { + faults = append(faults, LoopFault{ + LoopIdx: loopIdx, + Type: "unknown", + Message: fmt.Sprintf("回路%d未知异常", loopIdx), + Code: strings.TrimSpace(alarmCode), + }) + } + return faults +} + +func alarmDeviceMask(alarmCode string) byte { + fields := strings.Fields(alarmCode) + if len(fields) < 2 { + return 0 + } + value, err := strconv.ParseUint(fields[1], 16, 8) + if err != nil { + return 0 + } + return byte(value) +} + +func buildPowerFaultMessage(loopIdx uint, deviceMask byte) string { + deviceNames := make([]string, 0, 8) + for i := 0; i < 8; i++ { + if deviceMask&(1< 0.0001 { + t.Fatalf("DCCurrent = %v, want 0.447", params.DCCurrent) + } +} diff --git a/internal/model/config/config.go b/internal/model/config/config.go index 26b0179..779584f 100644 --- a/internal/model/config/config.go +++ b/internal/model/config/config.go @@ -100,6 +100,7 @@ type ( Version string `json:"version" mapstructure:"version"` MeterAddr string `json:"meterAddr" mapstructure:"meterAddr"` MeterFirm string `json:"meterFirm" mapstructure:"meterFirm"` + LoopCount int `json:"loopCount" mapstructure:"loopCount"` } Firefox struct { diff --git a/internal/module/m9zTtyApi/cron.go b/internal/module/m9zTtyApi/cron.go index 67c755d..586f1be 100644 --- a/internal/module/m9zTtyApi/cron.go +++ b/internal/module/m9zTtyApi/cron.go @@ -3,35 +3,35 @@ package m9zApi import ( "encoding/json" "fmt" - "github.com/gogf/gf/v2/errors/gerror" - - "github.com/towgo/towgo/lib/system" "os" "path/filepath" "sort" "sync" - "tgk-touch/internal/global" - "tgk-touch/internal/library/m9z" "time" + + "github.com/gogf/gf/v2/errors/gerror" + "github.com/towgo/towgo/lib/system" + + g "tgk-touch/internal/global" + "tgk-touch/internal/library/m9z" ) var l sync.Mutex -var filePath = system.GetPathOfProgram() + "data/data.json" // JSON 文件路径 +var historyLock sync.Mutex +var filePath = system.GetPathOfProgram() + "data/data.json" const ( - maxItems = 50 // 最大存储条数 + maxItems = 50 ) func ScanM9z() { - - tryLock := l.TryLock() - if !tryLock { + if !l.TryLock() { return } defer l.Unlock() - /** 挨个查询是否有报警*/ + cuid := g.Config().DeviceInfo.DeviceId - for i := 0; i < 10; i++ { + for i := 0; i < configuredLoopCount(); i++ { read, err := m9z.SubLoopParametersRead(cuid, uint(i)) if err != nil { g.Log().Error("sub loop parameters read error:", err) @@ -40,48 +40,35 @@ func ScanM9z() { if read == nil { continue } - if read.AlarmStatus != 0 { - var mph M9zFaultHistory - mph.CommUid = cuid - mph.LoopIdx = uint(i) + 1 - mph.FaultCode = read.AlarmCode - mph.Voltage = read.DCVoltage - mph.FaultTime = time.Now() - - switch read.AlarmStatus { - case 1: - mph.FaultMsg = "欠压/短路故障" - break - case 2: - mph.FaultMsg = "过压/开路故障" - break - case 8: - mph.FaultMsg = "电源故障" - break - case 9: - mph.FaultMsg = "欠压/电源故障" - break - default: - mph.FaultMsg = "未知异常" - } - err = addData(mph) - if err != nil { - g.Log().Error(gerror.Wrap(err, "create error")) - - } + faults := m9z.BuildLoopFaults(uint(i+1), read.AlarmStatus, read.AlarmCode) + recordLoopFaults(cuid, uint(i+1), read.DCVoltage, faults) + } +} + +func recordLoopFaults(commUid string, loopIdx uint, voltage float32, faults []m9z.LoopFault) { + for _, fault := range faults { + item := M9zFaultHistory{ + CommUid: commUid, + LoopIdx: loopIdx, + Voltage: voltage, + FaultMsg: fault.Message, + FaultCode: fault.Code, + FaultTime: time.Now(), + } + if err := addData(item); err != nil { + g.Log().Error(gerror.Wrap(err, "create error")) } } } -// 读取现有数据(如果文件不存在则返回空切片) func readData() ([]M9zFaultHistory, error) { - if _, err := os.Stat(filePath); os.IsNotExist(err) { - _, err := createFileWithPath(filePath) + file, err := createFileWithPath(filePath) if err != nil { return nil, gerror.Wrap(err, "create file error") } - return []M9zFaultHistory{}, nil // 文件不存在,返回空切片 + _ = file.Close() + return []M9zFaultHistory{}, nil } data, err := os.ReadFile(filePath) @@ -95,16 +82,13 @@ func readData() ([]M9zFaultHistory, error) { return nil, fmt.Errorf("解析 JSON 失败: %v", err) } } - // 确保ID匹配索引+1(处理旧数据或缺失ID) for i := range items { items[i].ID = int64(i + 1) } return items, nil } -// 写入数据到 JSON 文件 func writeData(items []M9zFaultHistory) error { - data, err := json.MarshalIndent(items, "", " ") if err != nil { return fmt.Errorf("序列化 JSON 失败: %v", err) @@ -112,65 +96,68 @@ func writeData(items []M9zFaultHistory) error { return os.WriteFile(filePath, data, 0644) } -// 新增数据(自动处理容量限制) func addData(newItem M9zFaultHistory) error { - // 1. 读取现有数据 + historyLock.Lock() + defer historyLock.Unlock() + items, err := readData() if err != nil { return gerror.Wrap(err, "readData") } - // 2. 追加新数据 + for _, item := range items { + if item.CommUid == newItem.CommUid && + item.LoopIdx == newItem.LoopIdx && + item.FaultCode == newItem.FaultCode && + item.FaultMsg == newItem.FaultMsg && + item.OperateStatus == 0 { + return nil + } + } + + if newItem.FaultTime.IsZero() { + newItem.FaultTime = time.Now() + } items = append(items, newItem) - // 3. 检查容量,如果超过 maxItems 则删除最早的数据(按时间排序) if len(items) > maxItems { - // 按 Timestamp 排序(最早的在前) sort.Slice(items, func(i, j int) bool { return items[i].FaultTime.Before(items[j].FaultTime) }) - // 删除最早的数据(保留最新的 maxItems 条) - items = items[1:] - for i, _ := range items { - items[i].ID = int64(i + 1) - } + items = items[len(items)-maxItems:] + } + for i := range items { + items[i].ID = int64(i + 1) } - // 4. 写回文件 - err = writeData(items) - - if err != nil { + if err := writeData(items); err != nil { return gerror.Wrap(err, "writeData") } - return nil } -// 按ID删除数据 func deleteById(id int) error { + historyLock.Lock() + defer historyLock.Unlock() + items, err := readData() if err != nil { return gerror.Wrap(err, "readData") } - if id < 1 || id > len(items) { return fmt.Errorf("ID %d 超出范围", id) } - // 删除指定索引的数据(id-1) items = append(items[:id-1], items[id:]...) - - // 重新分配ID(确保后续数据ID连续) for i := id - 1; i < len(items); i++ { items[i].ID = int64(i + 1) } return writeData(items) } + func createFileWithPath(filePath string) (*os.File, error) { - // 创建所有必需的父目录(使用 0755 权限) if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { return nil, err } - // 创建并打开文件(如果已存在则截断) return os.Create(filePath) } diff --git a/internal/module/m9zTtyApi/read.go b/internal/module/m9zTtyApi/read.go index db2b79a..90bb41c 100644 --- a/internal/module/m9zTtyApi/read.go +++ b/internal/module/m9zTtyApi/read.go @@ -5,6 +5,7 @@ import ( "github.com/gogf/gf/v2/errors/gerror" "math" "sort" + "strings" "sync" "tgk-touch/internal/library/m9z" "time" @@ -124,13 +125,18 @@ func getDeviceStatus(rpc towgo.JsonRpcConnection) { type DeviceStatusLoop struct { m9z.Loop - V float32 - A float32 - Kw float32 + V float32 + A float32 + Kw float32 + HasFault bool `json:"has_fault"` + FaultMsg string `json:"fault_msg"` + FaultCode string `json:"fault_code"` + Faults []m9z.LoopFault `json:"faults"` } type DeviceStatus struct { m9z.DeviceStatus - Loops []DeviceStatusLoop + ScanDeviceCount int `json:"scan_device_count"` + Loops []DeviceStatusLoop `json:"Loops"` } // 实时监控数据 @@ -143,40 +149,70 @@ func getDeviceStatus2(rpc towgo.JsonRpcConnection) { } d := new(DeviceStatus) d.DeviceStatus = *read + loopCount := configuredLoopCount() + d.ScanDeviceCount = loopCount + loops := make([]DeviceStatusLoop, loopCount) var wg sync.WaitGroup - for i := 0; i < 10; i++ { + for i := 0; i < loopCount; i++ { + idx := i wg.Add(1) go func() { defer wg.Done() var loop DeviceStatusLoop - loop.Name = fmt.Sprintf("#%+v", i+1) - loop.Index = i - loop.IsOpen = read.Relays[i] - loop.Pwm = int(read.PWMOutputs[i]) - parametersRead, _ := m9z.SubLoopParametersRead(cuid, uint(i)) + loop.Name = fmt.Sprintf("#%+v", idx+1) + loop.Index = idx + loop.IsOpen = read.Relays[idx] + loop.Pwm = int(read.PWMOutputs[idx]) + parametersRead, _ := m9z.SubLoopParametersRead(cuid, uint(idx)) if parametersRead != nil { - kw := float32(0.0) - loop.V = (parametersRead.DCVoltage * 10) / 10 - for _, v := range parametersRead.Modules { - loop.A = loop.A + v.OutputCurrent - kw = float32(math.Round(float64(v.OutputCurrent*v.OutputVoltage*1000))/1000) + kw - } - loop.Kw = ((kw / 1000) * 1000) / 1000 - loop.A = (loop.A * 100) / 100 + loop.V = parametersRead.DCVoltage + loop.A = parametersRead.DCCurrent + kw := float64(loop.V*loop.A) / 1000 + loop.Kw = float32(math.Round(kw*1000) / 1000) + loop.Faults = m9z.BuildLoopFaults(uint(idx+1), parametersRead.AlarmStatus, parametersRead.AlarmCode) + loop.HasFault = len(loop.Faults) > 0 + loop.FaultMsg = joinLoopFaultMessages(loop.Faults) + loop.FaultCode = joinLoopFaultCodes(loop.Faults) } - d.Loops = append(d.Loops, loop) + loops[idx] = loop }() } wg.Wait() + d.Loops = loops // 按 Timestamp 排序(最早的在前) sort.Slice(d.Loops, func(i, j int) bool { return d.Loops[i].Index < d.Loops[j].Index }) + for _, loop := range d.Loops { + if loop.HasFault { + recordLoopFaults(cuid, uint(loop.Index+1), loop.V, loop.Faults) + } + } rpc.WriteResult(d) } +func joinLoopFaultMessages(faults []m9z.LoopFault) string { + messages := make([]string, 0, len(faults)) + for _, fault := range faults { + if fault.Message != "" { + messages = append(messages, fault.Message) + } + } + return strings.Join(messages, ";") +} + +func joinLoopFaultCodes(faults []m9z.LoopFault) string { + codes := make([]string, 0, len(faults)) + for _, fault := range faults { + if fault.Code != "" { + codes = append(codes, fault.Code) + } + } + return strings.Join(codes, ",") +} + // 获取设备的配置 func getDeviceConfig(rpc towgo.JsonRpcConnection) { cuid, _ := getDeviceId(rpc) diff --git a/internal/module/m9zTtyApi/scan.go b/internal/module/m9zTtyApi/scan.go new file mode 100644 index 0000000..fd7b8ba --- /dev/null +++ b/internal/module/m9zTtyApi/scan.go @@ -0,0 +1,19 @@ +package m9zApi + +import g "tgk-touch/internal/global" + +const ( + defaultLoopCount = 10 + maxLoopCount = 10 +) + +func configuredLoopCount() int { + loopCount := g.Config().DeviceInfo.LoopCount + if loopCount <= 0 { + return defaultLoopCount + } + if loopCount > maxLoopCount { + return maxLoopCount + } + return loopCount +}