package service import ( "errors" "fmt" "log/slog" "strings" "time" "joylink.club/iot/protocol/modbus" "joylink.club/iot/service/model" sproto "joylink.club/iot/service/proto" ) // Modbus驱采服务 type modbusQcService struct { config *sproto.ModbusConfig cli modbus.MasterClient qc model.QC tasks []QcDataHandleScheduleTask stopped bool // cancel context.CancelFunc // done chan struct{} // 服务协程退出信号 } // RegisterQcDataHandleScheduleTask implements IotQcMappingService. func (s *modbusQcService) RegisterQcDataHandleScheduleTask(task func(), interval time.Duration) { if s.stopped { panic(fmt.Errorf("modbus驱采映射服务已停止")) } s.tasks = append(s.tasks, NewQcHandleTask(task, interval)) } func (m *modbusQcService) Stop() error { if m.stopped { // 已经停止 return nil } m.stopped = true for _, task := range m.tasks { task.Stop() } m.tasks = nil slog.Debug("Modbus驱采映射循环取消,关闭modbus客户端", "url", m.config.Url) defer modbus.DeleteClient(m.config.Url) slog.Info("Modbus驱采映射服务线程退出", "url", m.config.Url) return nil } // GetCjBits implements IotQcMappingService. func (s *modbusQcService) GetCjBits() []bool { return s.qc.GetCjBits() } // GetCjBytes implements IotQcMappingService. func (s *modbusQcService) GetCjBytes() []byte { return s.qc.GetCjBytes() } // GetQdBits implements IotQcMappingService. func (s *modbusQcService) GetQdBits() []bool { return s.qc.GetQdBits() } // GetQdBytes implements IotQcMappingService. func (s *modbusQcService) GetQdBytes() []byte { return s.qc.GetQdBytes() } // WriteCjBytes implements IotQcMappingService. func (s *modbusQcService) WriteCjBytes(bytes []byte) error { if len(bytes) != len(s.qc.GetCjBytes()) { return fmt.Errorf("写入采集字节长度有误,应为%d,实为%d", len(s.qc.GetCjBytes()), len(bytes)) } err := s.onWrite(sproto.DataType_CollectTable, bytes) if err == nil { s.qc.SetCjBytes(bytes) } return err } // WriteQdBytes implements IotQcMappingService. func (s *modbusQcService) WriteQdBytes(bytes []byte) error { if len(bytes) != len(s.qc.GetQdBytes()) { return fmt.Errorf("写入驱动字节长度有误,应为%d,实为%d", len(s.qc.GetQdBytes()), len(bytes)) } err := s.onWrite(sproto.DataType_DriveTable, bytes) if err == nil { s.qc.SetQdBytes(bytes) } return err } func NewModbusQcService(config *sproto.ModbusConfig, qd []byte, cj []byte) (IotQcMappingService, error) { // 基础配置检查 if err := checkConfig(config); err != nil { return nil, err } // 映射范围配置检查 if err := checkConfigMappingRange(config, qd, cj); err != nil { return nil, err } _, ok := modbus.GetClient(config.Url) if ok { return nil, fmt.Errorf("modbus客户端已存在,url=%s", config.Url) } cli, err := modbus.NewClient(&modbus.ClientConfig{ Url: config.Url, Timeout: config.Timeout, }) if err != nil { return nil, errors.Join(err, fmt.Errorf("modbus客户端创建失败,url=%s", config.Url)) } cli.SetUnitId(uint8(config.UnitId)) cli.SetEndianness(convertEndianness(config.Endianness)) cli.Start() s := &modbusQcService{ config: config, cli: cli, qc: model.NewDC(qd, cj), // done: make(chan struct{}), } s.RegisterQcDataHandleScheduleTask(s.readTaskExecute, time.Duration(config.Interval)*time.Millisecond) // s.initOnUpdateTask() // ctx, cancel := context.WithCancel(context.Background()) // go s.run(ctx) // s.cancel = cancel return s, nil } func (m *modbusQcService) onWrite(dt sproto.DataType, bytes []byte) error { mapping := m.config.Mapping for _, mdm := range mapping { if isWriteFunction(mdm.Function) { if mdm.Type == dt { if !m.cli.IsConnected() { slog.Warn("Modbus驱动采集服务数据写入失败,modbus客户端未连接", "url", m.config.Url, "unitid", m.config.UnitId, "Function", mdm.Function) return errors.New("数据写入失败,modbus客户端未连接") } switch mdm.Function { case sproto.Modbus_WriteCoil, sproto.Modbus_WriteCoils, sproto.Modbus_RWCoils: data := getQcBits(bytes, mdm) err := m.cli.WriteCoils(uint16(mdm.Addr), data) if err != nil { slog.Error("Modbus驱动采集服务写入线圈失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err, "Function", mdm.Function) return err } else { slog.Debug("Modbus驱动采集服务写入线圈成功", "url", m.config.Url, "unitid", m.config.UnitId, "Function", mdm.Function, "data", model.BitsDebug(data), "mapping", mdm) } case sproto.Modbus_WriteRegister, sproto.Modbus_WriteRegisters, sproto.Modbus_RWRegisters: data := getQcBytes(bytes, mdm) err := m.cli.WriteRegisterBytes(uint16(mdm.Addr), data) if err != nil { slog.Error("Modbus驱动采集服务写入寄存器失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err, "Function", mdm.Function) return err } else { slog.Debug("Modbus驱动采集服务写入寄存器成功", "url", m.config.Url, "unitid", m.config.UnitId, "Function", mdm.Function, "data", model.BytesDebug(data), "mapping", mdm) } } } } } return nil } // func (m *modbusQcService) initOnUpdateTask() { // mapping := m.config.Mapping // for _, mdm := range mapping { // if mdm.WriteStrategy == sproto.Modbus_OnUpdate && isWriteFunction(mdm.Function) { // et := model.DCE_Drive_Update // if mdm.Type == sproto.DataType_CollectTable { // et = model.DCE_Collect_Update // } // m.qc.On(et, func(d model.QC) { // if !m.cli.IsConnected() { // slog.Warn("Modbus驱动采集服务数据更新写入失败,modbus客户端未连接", "url", m.config.Url, "Function", mdm.Function) // return // } // switch mdm.Function { // case sproto.Modbus_WriteCoil, sproto.Modbus_WriteCoils, sproto.Modbus_RWCoils: // err := m.cli.WriteCoils(uint16(mdm.Addr), m.GetDcBits(mdm)) // if err != nil { // slog.Error("Modbus驱动采集服务写入线圈失败", "url", m.config.Url, "error", err, "Function", mdm.Function) // } else { // slog.Info("Modbus驱动采集服务写入线圈成功", "url", m.config.Url, "Function", mdm.Function) // } // case sproto.Modbus_WriteRegister, sproto.Modbus_WriteRegisters, sproto.Modbus_RWRegisters: // err := m.cli.WriteRegisterBytes(uint16(mdm.Addr), m.GetDcBytes(mdm)) // if err != nil { // slog.Error("Modbus驱动采集服务写入寄存器失败", "url", m.config.Url, "error", err, "Function", mdm.Function) // } else { // slog.Info("Modbus驱动采集服务写入寄存器成功", "url", m.config.Url, "Function", mdm.Function) // } // } // }) // } // } // } // func (m *modbusQcService) run(ctx context.Context) { // defer close(m.done) // mainLoop: // for { // select { // case <-ctx.Done(): // slog.Debug("Modbus驱采映射循环取消,关闭modbus客户端", "url", m.config.Url) // modbus.DeleteClient(m.config.Url) // break mainLoop // default: // } // m.readTaskExecute() // time.Sleep(time.Millisecond * time.Duration(m.config.Interval)) // } // } func (m *modbusQcService) readTaskExecute() { if m.cli.IsConnected() { for _, mdm := range m.config.Mapping { switch mdm.Function { case sproto.Modbus_ReadCoil, sproto.Modbus_RWCoils: data, err := m.cli.ReadCoil(uint16(mdm.Addr), uint16(mdm.Quantity)) if err != nil { slog.Error("Modbus驱动采集服务读取线圈失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) continue } err = m.updateDcByBits(mdm, data) if err != nil { slog.Error("Modbus驱动采集服务更新驱采数据失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) continue } case sproto.Modbus_ReadDiscreteInput: data, err := m.cli.ReadDiscreteInput(uint16(mdm.Addr), uint16(mdm.Quantity)) if err != nil { slog.Error("Modbus驱动采集服务读取离散输入失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) continue } err = m.updateDcByBits(mdm, data) if err != nil { slog.Error("Modbus驱动采集服务更新驱采数据失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) continue } case sproto.Modbus_ReadHoldingRegister, sproto.Modbus_RWRegisters: data, err := m.cli.ReadHoldingRegisterBytes(uint16(mdm.Addr), uint16(mdm.Quantity*2)) if err != nil { slog.Error("Modbus驱动采集服务读取保持寄存器失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) continue } err = m.updateDcByBytes(mdm, data) if err != nil { slog.Error("Modbus驱动采集服务更新驱采数据失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) continue } case sproto.Modbus_ReadInputRegister: data, err := m.cli.ReadInputRegisterBytes(uint16(mdm.Addr), uint16(mdm.Quantity*2)) if err != nil { slog.Error("Modbus驱动采集服务读取输入寄存器失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) continue } err = m.updateDcByBytes(mdm, data) if err != nil { slog.Error("Modbus驱动采集服务更新驱采数据失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) continue } // case sproto.Modbus_WriteCoil: // if mdm.WriteStrategy == sproto.Modbus_OnScheduled { // 定时写 // bits := m.GetDcBits(mdm) // err := m.cli.WriteCoil(uint16(mdm.Addr), bits[0]) // if err != nil { // slog.Error("Modbus驱动采集服务写单线圈失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) // continue // } // } // case sproto.Modbus_WriteCoils: // if mdm.WriteStrategy == sproto.Modbus_OnScheduled { // 定时写 // bits := m.GetDcBits(mdm) // err := m.cli.WriteCoils(uint16(mdm.Addr), bits) // if err != nil { // slog.Error("Modbus驱动采集服务写多线圈失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) // continue // } // } // case sproto.Modbus_WriteRegister: // if mdm.WriteStrategy == sproto.Modbus_OnScheduled { // 定时写 // data := m.GetDcBytes(mdm) // err := m.cli.WriteRegisterBytes(uint16(mdm.Addr), data) // if err != nil { // slog.Error("Modbus驱动采集服务写单寄存器失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) // continue // } // } // case sproto.Modbus_WriteRegisters: // if mdm.WriteStrategy == sproto.Modbus_OnScheduled { // 定时写 // data := m.GetDcBytes(mdm) // err := m.cli.WriteRegisterBytes(uint16(mdm.Addr), data) // if err != nil { // slog.Error("Modbus驱动采集服务写多寄存器失败", "url", m.config.Url, "unitid", m.config.UnitId, "error", err) // continue // } // } } } } else { slog.Error("Modbus驱动采集服务映射任务执行失败,Modbus未连接", "url", m.config.Url, "unitid", m.config.UnitId) } } func getQcBits(bytes []byte, mdm *sproto.ModbusDcMapping) []bool { bits := model.DecodeBools(bytes) start := mdm.Start quantity := mdm.Quantity if start+quantity > uint32(len(bits)) { panic(fmt.Errorf("getQcBits超出范围")) } return bits[start : start+quantity] } func getQcBytes(bytes []byte, mdm *sproto.ModbusDcMapping) []byte { start := mdm.Start quantity := mdm.Quantity * 2 if start+quantity > uint32(len(bytes)) { panic(fmt.Errorf("getQcBytes超出范围")) } return bytes[start : start+quantity] } // func (m *modbusQcService) GetDcBits(mdm *sproto.ModbusDcMapping) []bool { // switch mdm.Type { // case sproto.DataType_CollectTable: // 采集数据 // return m.qc.GetCjBitsOf(mdm.Start, mdm.Quantity) // case sproto.DataType_DriveTable: // 驱动数据 // return m.qc.GetQdBitsOf(mdm.Start, mdm.Quantity) // default: // panic("未知数据类型") // } // } // func (m *modbusQcService) GetDcBytes(mdm *sproto.ModbusDcMapping) []byte { // switch mdm.Type { // case sproto.DataType_CollectTable: // 采集数据 // return m.qc.GetCjBytesOf(mdm.Start, mdm.Quantity*2) // case sproto.DataType_DriveTable: // 驱动数据 // return m.qc.GetQdBytesOf(mdm.Start, mdm.Quantity*2) // default: // panic("未知数据类型") // } // } func (m *modbusQcService) updateDcByBits(mdm *sproto.ModbusDcMapping, bits []bool) error { switch mdm.Type { case sproto.DataType_CollectTable: // 采集数据 return m.qc.UpdateCjByBits(mdm.Start, bits) case sproto.DataType_DriveTable: // 驱动数据 return m.qc.UpdateQdByBits(mdm.Start, bits) } return nil } func (m *modbusQcService) updateDcByBytes(mdm *sproto.ModbusDcMapping, bytes []byte) error { switch mdm.Type { case sproto.DataType_CollectTable: // 采集数据 return m.qc.UpdateCjByBytes(mdm.Start, bytes) case sproto.DataType_DriveTable: // 驱动数据 return m.qc.UpdateQdByBytes(mdm.Start, bytes) } return nil } func checkConfig(config *sproto.ModbusConfig) error { if config.Url == "" { return fmt.Errorf("Modbus配置未设置url") } if !strings.HasPrefix(config.Url, "tcp") { return fmt.Errorf("Modbus配置url必须以tcp开头") } if config.UnitId == 0 { return fmt.Errorf("Modbus配置未设置unitId") } if config.Interval == 0 { return fmt.Errorf("Modbus配置未设置interval") } if len(config.Mapping) == 0 { return fmt.Errorf("Modbus配置无映射配置") } return nil } func checkConfigMappingRange(modbusConfig *sproto.ModbusConfig, qd []byte, cj []byte) error { for _, mdm := range modbusConfig.Mapping { if mdm.Type == sproto.DataType_CollectTable { err := checkMappingOutRange(mdm, cj) if err != nil { return err } } else if mdm.Type == sproto.DataType_DriveTable { err := checkMappingOutRange(mdm, qd) if err != nil { return err } } } return nil } func checkMappingOutRange(mdm *sproto.ModbusDcMapping, bytes []byte) error { f := mdm.Function start := mdm.Start quantity := mdm.Quantity if isCoilFunction(f) { end := start + quantity if end > uint32(len(bytes)*8) { return fmt.Errorf("modbus地址映射配置错误,采集表地址超出范围: 功能=%s,起始位地址=%d,位数量=%d,实际位长度=%d", f, start, quantity, len(bytes)*8) } } else { end := start + quantity*2 if end > uint32(len(bytes)) { return fmt.Errorf("modbus地址映射配置错误,采集表地址超出范围: 功能=%s,起始字节地址=%d,字数量=%d,实际位长度=%d", f, start, quantity, len(bytes)) } } return nil } func convertEndianness(endianness sproto.Modbus_Endianness) modbus.Endianness { switch endianness { case sproto.Modbus_BigEndian: return modbus.BigEndian case sproto.Modbus_LittleEndian: return modbus.LittleEndian } return modbus.BigEndian } func isWriteFunction(modbus_Function sproto.Modbus_Function) bool { return modbus_Function == sproto.Modbus_WriteCoil || modbus_Function == sproto.Modbus_WriteCoils || modbus_Function == sproto.Modbus_WriteRegister || modbus_Function == sproto.Modbus_WriteRegisters || modbus_Function == sproto.Modbus_RWCoils || modbus_Function == sproto.Modbus_RWRegisters } func isCoilFunction(modbus_Function sproto.Modbus_Function) bool { return modbus_Function == sproto.Modbus_ReadCoil || modbus_Function == sproto.Modbus_ReadDiscreteInput || modbus_Function == sproto.Modbus_RWCoils || modbus_Function == sproto.Modbus_WriteCoil || modbus_Function == sproto.Modbus_WriteCoils } // func isRegisterFunction(modbus_Function sproto.Modbus_Function) bool { // return modbus_Function == sproto.Modbus_ReadInputRegister || // modbus_Function == sproto.Modbus_ReadHoldingRegister || // modbus_Function == sproto.Modbus_RWRegisters || // modbus_Function == sproto.Modbus_WriteRegister || // modbus_Function == sproto.Modbus_WriteRegisters // }