实现实时故障后端支持

main
wangqiyang 3 days ago
parent b272738d8e
commit f506dfdbcd

@ -28,7 +28,8 @@
"deviceInfo": { "deviceInfo": {
"deviceId": "TGK", "deviceId": "TGK",
"version": "1.0.2", "version": "1.0.2",
"meterAddr": "0x04" "meterAddr": "0x04",
"loopCount": 10
}, },
"MessageInterval": 15, "MessageInterval": 15,
"Firefox": { "Firefox": {

@ -32,7 +32,8 @@
"deviceId": "862809073336480", "deviceId": "862809073336480",
"version": "1.0.2", "version": "1.0.2",
"meterAddr": "0x04", "meterAddr": "0x04",
"meterFirm": "ADL400" "meterFirm": "ADL400",
"loopCount": 10
}, },
"MessageInterval": 15, "MessageInterval": 15,
"Firefox": { "Firefox": {

@ -3,13 +3,16 @@ package m9z
import ( import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"strconv"
"strings"
"github.com/gogf/gf/v2/util/gconv" "github.com/gogf/gf/v2/util/gconv"
) )
// LoopCommonParameters 回路分控器公共参数结构体 (15个寄存器 = 30字节) // LoopCommonParameters 回路分控器公共参数结构体 (15个寄存器 = 30字节)
type LoopCommonParameters struct { type LoopCommonParameters struct {
DCVoltage float32 // 直流电压 (0.1V单位) DCVoltage float32 // 直流电压 (0.1V单位)
DCCurrent float32 // 直流电流 (0.1A单位) DCCurrent float32 // 直流电流 (0.001A单位)
DimmingValue float32 // 调光幅度值 (0-100%) DimmingValue float32 // 调光幅度值 (0-100%)
AlarmStatus uint16 // 告警状态 (位图形式) 告警状态寄存器 0x01欠压 0x02过压 0x08电源故障 其中:欠压=短路故障,过压=开路故障,电源故障。 AlarmStatus uint16 // 告警状态 (位图形式) 告警状态寄存器 0x01欠压 0x02过压 0x08电源故障 其中:欠压=短路故障,过压=开路故障,电源故障。
AlarmCode string // 告警代码 AlarmCode string // 告警代码
@ -42,6 +45,83 @@ type LoopFullParameters struct {
Modules []LoopModuleParameters // 8个模块的参数 (208字节) 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<<i) != 0 {
deviceNames = append(deviceNames, strconv.Itoa(i+1))
}
}
if len(deviceNames) == 0 {
return fmt.Sprintf("回路%d电源故障", loopIdx)
}
return fmt.Sprintf("回路%d设备%s电源故障", loopIdx, strings.Join(deviceNames, "/"))
}
// ParseLoopFullParameters 解析完整的238字节分控器数据 // ParseLoopFullParameters 解析完整的238字节分控器数据
func ParseLoopFullParameters(data []byte) (*LoopFullParameters, error) { func ParseLoopFullParameters(data []byte) (*LoopFullParameters, error) {
if len(data) < 238 { if len(data) < 238 {
@ -56,7 +136,7 @@ func ParseLoopFullParameters(data []byte) (*LoopFullParameters, error) {
common.DCVoltage = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1 common.DCVoltage = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1
offset += 2 offset += 2
common.DCCurrent = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1 common.DCCurrent = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.001
offset += 2 offset += 2
common.DimmingValue = float32(binary.LittleEndian.Uint16(data[offset:])) * 10 * 0.01 common.DimmingValue = float32(binary.LittleEndian.Uint16(data[offset:])) * 10 * 0.01
offset += 2 offset += 2

@ -0,0 +1,105 @@
package m9z
import (
"math"
"testing"
)
func TestBuildLoopFaults(t *testing.T) {
tests := []struct {
name string
loopIdx uint
alarmStatus uint16
alarmCode string
want []LoopFault
}{
{
name: "short circuit",
loopIdx: 1,
alarmStatus: 0x01,
alarmCode: "01 00",
want: []LoopFault{{
LoopIdx: 1,
Type: "short_circuit",
Message: "回路1短路故障",
Code: "01",
}},
},
{
name: "open circuit",
loopIdx: 2,
alarmStatus: 0x02,
alarmCode: "02 00",
want: []LoopFault{{
LoopIdx: 2,
Type: "open_circuit",
Message: "回路2开路故障",
Code: "02",
}},
},
{
name: "power mask",
loopIdx: 3,
alarmStatus: 0x04,
alarmCode: "04 03",
want: []LoopFault{{
LoopIdx: 3,
Type: "power",
Message: "回路3设备1/2电源故障",
Code: "04 03",
}},
},
{
name: "combined faults",
loopIdx: 4,
alarmStatus: 0x05,
alarmCode: "04 02",
want: []LoopFault{
{
LoopIdx: 4,
Type: "short_circuit",
Message: "回路4短路故障",
Code: "01",
},
{
LoopIdx: 4,
Type: "power",
Message: "回路4设备2电源故障",
Code: "04 02",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := BuildLoopFaults(tt.loopIdx, tt.alarmStatus, tt.alarmCode)
if len(got) != len(tt.want) {
t.Fatalf("len(got) = %d, want %d; got=%+v", len(got), len(tt.want), got)
}
for i := range got {
if got[i] != tt.want[i] {
t.Fatalf("got[%d] = %+v, want %+v", i, got[i], tt.want[i])
}
}
})
}
}
func TestParseLoopFullParametersDCCurrentScale(t *testing.T) {
data := make([]byte, 238)
data[2] = 0xBF
data[3] = 0x01
data[10] = 0x01
params, err := ParseLoopFullParameters(data)
if err != nil {
t.Fatal(err)
}
if params == nil {
t.Fatal("expected params, got nil")
}
if math.Abs(float64(params.DCCurrent-0.447)) > 0.0001 {
t.Fatalf("DCCurrent = %v, want 0.447", params.DCCurrent)
}
}

@ -100,6 +100,7 @@ type (
Version string `json:"version" mapstructure:"version"` Version string `json:"version" mapstructure:"version"`
MeterAddr string `json:"meterAddr" mapstructure:"meterAddr"` MeterAddr string `json:"meterAddr" mapstructure:"meterAddr"`
MeterFirm string `json:"meterFirm" mapstructure:"meterFirm"` MeterFirm string `json:"meterFirm" mapstructure:"meterFirm"`
LoopCount int `json:"loopCount" mapstructure:"loopCount"`
} }
Firefox struct { Firefox struct {

@ -3,35 +3,35 @@ package m9zApi
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/towgo/towgo/lib/system"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
"sync" "sync"
"tgk-touch/internal/global"
"tgk-touch/internal/library/m9z"
"time" "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 l sync.Mutex
var filePath = system.GetPathOfProgram() + "data/data.json" // JSON 文件路径 var historyLock sync.Mutex
var filePath = system.GetPathOfProgram() + "data/data.json"
const ( const (
maxItems = 50 // 最大存储条数 maxItems = 50
) )
func ScanM9z() { func ScanM9z() {
if !l.TryLock() {
tryLock := l.TryLock()
if !tryLock {
return return
} }
defer l.Unlock() defer l.Unlock()
/** 挨个查询是否有报警*/
cuid := g.Config().DeviceInfo.DeviceId cuid := g.Config().DeviceInfo.DeviceId
for i := 0; i < 10; i++ { for i := 0; i < configuredLoopCount(); i++ {
read, err := m9z.SubLoopParametersRead(cuid, uint(i)) read, err := m9z.SubLoopParametersRead(cuid, uint(i))
if err != nil { if err != nil {
g.Log().Error("sub loop parameters read error:", err) g.Log().Error("sub loop parameters read error:", err)
@ -40,48 +40,35 @@ func ScanM9z() {
if read == nil { if read == nil {
continue continue
} }
if read.AlarmStatus != 0 { faults := m9z.BuildLoopFaults(uint(i+1), read.AlarmStatus, read.AlarmCode)
var mph M9zFaultHistory recordLoopFaults(cuid, uint(i+1), read.DCVoltage, faults)
mph.CommUid = cuid }
mph.LoopIdx = uint(i) + 1 }
mph.FaultCode = read.AlarmCode
mph.Voltage = read.DCVoltage func recordLoopFaults(commUid string, loopIdx uint, voltage float32, faults []m9z.LoopFault) {
mph.FaultTime = time.Now() for _, fault := range faults {
item := M9zFaultHistory{
switch read.AlarmStatus { CommUid: commUid,
case 1: LoopIdx: loopIdx,
mph.FaultMsg = "欠压/短路故障" Voltage: voltage,
break FaultMsg: fault.Message,
case 2: FaultCode: fault.Code,
mph.FaultMsg = "过压/开路故障" FaultTime: time.Now(),
break }
case 8: if err := addData(item); err != nil {
mph.FaultMsg = "电源故障" g.Log().Error(gerror.Wrap(err, "create error"))
break
case 9:
mph.FaultMsg = "欠压/电源故障"
break
default:
mph.FaultMsg = "未知异常"
}
err = addData(mph)
if err != nil {
g.Log().Error(gerror.Wrap(err, "create error"))
}
} }
} }
} }
// 读取现有数据(如果文件不存在则返回空切片)
func readData() ([]M9zFaultHistory, error) { func readData() ([]M9zFaultHistory, error) {
if _, err := os.Stat(filePath); os.IsNotExist(err) { if _, err := os.Stat(filePath); os.IsNotExist(err) {
_, err := createFileWithPath(filePath) file, err := createFileWithPath(filePath)
if err != nil { if err != nil {
return nil, gerror.Wrap(err, "create file error") return nil, gerror.Wrap(err, "create file error")
} }
return []M9zFaultHistory{}, nil // 文件不存在,返回空切片 _ = file.Close()
return []M9zFaultHistory{}, nil
} }
data, err := os.ReadFile(filePath) data, err := os.ReadFile(filePath)
@ -95,16 +82,13 @@ func readData() ([]M9zFaultHistory, error) {
return nil, fmt.Errorf("解析 JSON 失败: %v", err) return nil, fmt.Errorf("解析 JSON 失败: %v", err)
} }
} }
// 确保ID匹配索引+1处理旧数据或缺失ID
for i := range items { for i := range items {
items[i].ID = int64(i + 1) items[i].ID = int64(i + 1)
} }
return items, nil return items, nil
} }
// 写入数据到 JSON 文件
func writeData(items []M9zFaultHistory) error { func writeData(items []M9zFaultHistory) error {
data, err := json.MarshalIndent(items, "", " ") data, err := json.MarshalIndent(items, "", " ")
if err != nil { if err != nil {
return fmt.Errorf("序列化 JSON 失败: %v", err) return fmt.Errorf("序列化 JSON 失败: %v", err)
@ -112,65 +96,68 @@ func writeData(items []M9zFaultHistory) error {
return os.WriteFile(filePath, data, 0644) return os.WriteFile(filePath, data, 0644)
} }
// 新增数据(自动处理容量限制)
func addData(newItem M9zFaultHistory) error { func addData(newItem M9zFaultHistory) error {
// 1. 读取现有数据 historyLock.Lock()
defer historyLock.Unlock()
items, err := readData() items, err := readData()
if err != nil { if err != nil {
return gerror.Wrap(err, "readData") 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) items = append(items, newItem)
// 3. 检查容量,如果超过 maxItems 则删除最早的数据(按时间排序)
if len(items) > maxItems { if len(items) > maxItems {
// 按 Timestamp 排序(最早的在前)
sort.Slice(items, func(i, j int) bool { sort.Slice(items, func(i, j int) bool {
return items[i].FaultTime.Before(items[j].FaultTime) return items[i].FaultTime.Before(items[j].FaultTime)
}) })
// 删除最早的数据(保留最新的 maxItems 条) items = items[len(items)-maxItems:]
items = items[1:] }
for i, _ := range items { for i := range items {
items[i].ID = int64(i + 1) items[i].ID = int64(i + 1)
}
} }
// 4. 写回文件 if err := writeData(items); err != nil {
err = writeData(items)
if err != nil {
return gerror.Wrap(err, "writeData") return gerror.Wrap(err, "writeData")
} }
return nil return nil
} }
// 按ID删除数据
func deleteById(id int) error { func deleteById(id int) error {
historyLock.Lock()
defer historyLock.Unlock()
items, err := readData() items, err := readData()
if err != nil { if err != nil {
return gerror.Wrap(err, "readData") return gerror.Wrap(err, "readData")
} }
if id < 1 || id > len(items) { if id < 1 || id > len(items) {
return fmt.Errorf("ID %d 超出范围", id) return fmt.Errorf("ID %d 超出范围", id)
} }
// 删除指定索引的数据id-1
items = append(items[:id-1], items[id:]...) items = append(items[:id-1], items[id:]...)
// 重新分配ID确保后续数据ID连续
for i := id - 1; i < len(items); i++ { for i := id - 1; i < len(items); i++ {
items[i].ID = int64(i + 1) items[i].ID = int64(i + 1)
} }
return writeData(items) return writeData(items)
} }
func createFileWithPath(filePath string) (*os.File, error) { func createFileWithPath(filePath string) (*os.File, error) {
// 创建所有必需的父目录(使用 0755 权限)
if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil { if err := os.MkdirAll(filepath.Dir(filePath), os.ModePerm); err != nil {
return nil, err return nil, err
} }
// 创建并打开文件(如果已存在则截断)
return os.Create(filePath) return os.Create(filePath)
} }

@ -5,6 +5,7 @@ import (
"github.com/gogf/gf/v2/errors/gerror" "github.com/gogf/gf/v2/errors/gerror"
"math" "math"
"sort" "sort"
"strings"
"sync" "sync"
"tgk-touch/internal/library/m9z" "tgk-touch/internal/library/m9z"
"time" "time"
@ -124,13 +125,18 @@ func getDeviceStatus(rpc towgo.JsonRpcConnection) {
type DeviceStatusLoop struct { type DeviceStatusLoop struct {
m9z.Loop m9z.Loop
V float32 V float32
A float32 A float32
Kw 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 { type DeviceStatus struct {
m9z.DeviceStatus 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 := new(DeviceStatus)
d.DeviceStatus = *read d.DeviceStatus = *read
loopCount := configuredLoopCount()
d.ScanDeviceCount = loopCount
loops := make([]DeviceStatusLoop, loopCount)
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < 10; i++ { for i := 0; i < loopCount; i++ {
idx := i
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
var loop DeviceStatusLoop var loop DeviceStatusLoop
loop.Name = fmt.Sprintf("#%+v", i+1) loop.Name = fmt.Sprintf("#%+v", idx+1)
loop.Index = i loop.Index = idx
loop.IsOpen = read.Relays[i] loop.IsOpen = read.Relays[idx]
loop.Pwm = int(read.PWMOutputs[i]) loop.Pwm = int(read.PWMOutputs[idx])
parametersRead, _ := m9z.SubLoopParametersRead(cuid, uint(i)) parametersRead, _ := m9z.SubLoopParametersRead(cuid, uint(idx))
if parametersRead != nil { if parametersRead != nil {
kw := float32(0.0) loop.V = parametersRead.DCVoltage
loop.V = (parametersRead.DCVoltage * 10) / 10 loop.A = parametersRead.DCCurrent
for _, v := range parametersRead.Modules { kw := float64(loop.V*loop.A) / 1000
loop.A = loop.A + v.OutputCurrent loop.Kw = float32(math.Round(kw*1000) / 1000)
kw = float32(math.Round(float64(v.OutputCurrent*v.OutputVoltage*1000))/1000) + kw loop.Faults = m9z.BuildLoopFaults(uint(idx+1), parametersRead.AlarmStatus, parametersRead.AlarmCode)
} loop.HasFault = len(loop.Faults) > 0
loop.Kw = ((kw / 1000) * 1000) / 1000 loop.FaultMsg = joinLoopFaultMessages(loop.Faults)
loop.A = (loop.A * 100) / 100 loop.FaultCode = joinLoopFaultCodes(loop.Faults)
} }
d.Loops = append(d.Loops, loop) loops[idx] = loop
}() }()
} }
wg.Wait() wg.Wait()
d.Loops = loops
// 按 Timestamp 排序(最早的在前) // 按 Timestamp 排序(最早的在前)
sort.Slice(d.Loops, func(i, j int) bool { sort.Slice(d.Loops, func(i, j int) bool {
return d.Loops[i].Index < d.Loops[j].Index 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) 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) { func getDeviceConfig(rpc towgo.JsonRpcConnection) {
cuid, _ := getDeviceId(rpc) cuid, _ := getDeviceId(rpc)

@ -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
}
Loading…
Cancel
Save