jl-iot/service/modbus_qc_mapping.go

455 lines
16 KiB
Go
Raw Normal View History

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
// }