实现实时故障后端支持

main
wangqiyang 3 days ago
parent b272738d8e
commit f506dfdbcd

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

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

@ -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<<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字节分控器数据
func ParseLoopFullParameters(data []byte) (*LoopFullParameters, error) {
if len(data) < 238 {
@ -56,7 +136,7 @@ func ParseLoopFullParameters(data []byte) (*LoopFullParameters, error) {
common.DCVoltage = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1
offset += 2
common.DCCurrent = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.1
common.DCCurrent = float32(binary.LittleEndian.Uint16(data[offset:])) * 0.001
offset += 2
common.DimmingValue = float32(binary.LittleEndian.Uint16(data[offset:])) * 10 * 0.01
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"`
MeterAddr string `json:"meterAddr" mapstructure:"meterAddr"`
MeterFirm string `json:"meterFirm" mapstructure:"meterFirm"`
LoopCount int `json:"loopCount" mapstructure:"loopCount"`
}
Firefox struct {

@ -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)
}

@ -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)

@ -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