evcc-io/util/modbus/modbus.go

340 lines
10 KiB
Go

package modbus
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/evcc-io/evcc/util"
"github.com/volkszaehler/mbmd/meters"
"github.com/volkszaehler/mbmd/meters/rs485"
"github.com/volkszaehler/mbmd/meters/sunspec"
)
type Protocol int
const (
Tcp Protocol = iota
Rtu
Ascii
CoilOn uint16 = 0xFF00
)
// Settings contains the ModBus TCP settings
// RTU field is included for compatibility with modbus.tpl which renders rtu: false for TCP
// TODO remove RTU field (https://github.com/evcc-io/evcc/issues/3360)
type TcpSettings struct {
URI string
ID uint8
RTU *bool `mapstructure:"rtu"`
}
// Settings contains the ModBus settings
type Settings struct {
ID uint8
SubDevice int
URI, Device, Comset string
Baudrate int
RTU *bool // indicates RTU over TCP if true
}
func (s *Settings) String() string {
if s.URI != "" {
return s.URI
}
return s.Device
}
// Connection decorates a meters.Connection with transparent slave id and error handling
type Connection struct {
slaveID uint8
mu sync.Mutex
conn meters.Connection
delay time.Duration
}
func (mb *Connection) prepare(slaveID uint8) {
mb.conn.Slave(slaveID)
if mb.delay > 0 {
time.Sleep(mb.delay)
}
}
func (mb *Connection) handle(res []byte, err error) ([]byte, error) {
if err != nil {
mb.conn.Close()
}
return res, err
}
// Delay sets delay so use between subsequent modbus operations
func (mb *Connection) Delay(delay time.Duration) {
mb.delay = delay
}
// ConnectDelay sets the initial delay after connecting before starting communication
func (mb *Connection) ConnectDelay(delay time.Duration) {
mb.conn.ConnectDelay(delay)
}
// Logger sets logger implementation
func (mb *Connection) Logger(logger meters.Logger) {
mb.conn.Logger(logger)
}
// Timeout sets the connection timeout (not idle timeout)
func (mb *Connection) Timeout(timeout time.Duration) {
mb.conn.Timeout(timeout)
}
// ReadCoils wraps the underlying implementation
func (mb *Connection) ReadCoilsWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().ReadCoils(address, quantity))
}
// WriteSingleCoil wraps the underlying implementation
func (mb *Connection) WriteSingleCoilWithSlave(slaveID uint8, address, value uint16) ([]byte, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().WriteSingleCoil(address, value))
}
// ReadInputRegisters wraps the underlying implementation
func (mb *Connection) ReadInputRegistersWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().ReadInputRegisters(address, quantity))
}
// ReadHoldingRegisters wraps the underlying implementation
func (mb *Connection) ReadHoldingRegistersWithSlave(slaveID uint8, address, quantity uint16) ([]byte, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().ReadHoldingRegisters(address, quantity))
}
// WriteSingleRegister wraps the underlying implementation
func (mb *Connection) WriteSingleRegisterWithSlave(slaveID uint8, address, value uint16) ([]byte, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().WriteSingleRegister(address, value))
}
// WriteMultipleRegisters wraps the underlying implementation
func (mb *Connection) WriteMultipleRegistersWithSlave(slaveID uint8, address, quantity uint16, value []byte) ([]byte, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().WriteMultipleRegisters(address, quantity, value))
}
// ReadDiscreteInputs wraps the underlying implementation
func (mb *Connection) ReadDiscreteInputsWithSlave(slaveID uint8, address, quantity uint16) (results []byte, err error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().ReadDiscreteInputs(address, quantity))
}
// WriteMultipleCoils wraps the underlying implementation
func (mb *Connection) WriteMultipleCoilsWithSlave(slaveID uint8, address, quantity uint16, value []byte) (results []byte, err error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().WriteMultipleCoils(address, quantity, value))
}
// ReadWriteMultipleRegisters wraps the underlying implementation
func (mb *Connection) ReadWriteMultipleRegistersWithSlave(slaveID uint8, readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity, value))
}
// MaskWriteRegister wraps the underlying implementation
func (mb *Connection) MaskWriteRegisterWithSlave(slaveID uint8, address, andMask, orMask uint16) (results []byte, err error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().MaskWriteRegister(address, andMask, orMask))
}
// ReadFIFOQueue wraps the underlying implementation
func (mb *Connection) ReadFIFOQueueWithSlave(slaveID uint8, address uint16) (results []byte, err error) {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.prepare(slaveID)
return mb.handle(mb.conn.ModbusClient().ReadFIFOQueue(address))
}
func (mb *Connection) ReadCoils(address, quantity uint16) ([]byte, error) {
return mb.ReadCoilsWithSlave(mb.slaveID, address, quantity)
}
func (mb *Connection) WriteSingleCoil(address, value uint16) ([]byte, error) {
return mb.WriteSingleCoilWithSlave(mb.slaveID, address, value)
}
func (mb *Connection) ReadInputRegisters(address, quantity uint16) ([]byte, error) {
return mb.ReadInputRegistersWithSlave(mb.slaveID, address, quantity)
}
func (mb *Connection) ReadHoldingRegisters(address, quantity uint16) ([]byte, error) {
return mb.ReadHoldingRegistersWithSlave(mb.slaveID, address, quantity)
}
func (mb *Connection) WriteSingleRegister(address, value uint16) ([]byte, error) {
return mb.WriteSingleRegisterWithSlave(mb.slaveID, address, value)
}
func (mb *Connection) WriteMultipleRegisters(address, quantity uint16, value []byte) ([]byte, error) {
return mb.WriteMultipleRegistersWithSlave(mb.slaveID, address, quantity, value)
}
func (mb *Connection) ReadDiscreteInputs(address, quantity uint16) (results []byte, err error) {
return mb.ReadDiscreteInputsWithSlave(mb.slaveID, address, quantity)
}
func (mb *Connection) WriteMultipleCoils(address, quantity uint16, value []byte) (results []byte, err error) {
return mb.WriteMultipleCoilsWithSlave(mb.slaveID, address, quantity, value)
}
func (mb *Connection) ReadWriteMultipleRegisters(readAddress, readQuantity, writeAddress, writeQuantity uint16, value []byte) (results []byte, err error) {
return mb.ReadWriteMultipleRegistersWithSlave(mb.slaveID, readAddress, readQuantity, writeAddress, writeQuantity, value)
}
func (mb *Connection) MaskWriteRegister(address, andMask, orMask uint16) (results []byte, err error) {
return mb.MaskWriteRegisterWithSlave(mb.slaveID, address, andMask, orMask)
}
func (mb *Connection) ReadFIFOQueue(address uint16) (results []byte, err error) {
return mb.ReadFIFOQueueWithSlave(mb.slaveID, address)
}
var (
connections = make(map[string]meters.Connection)
mu sync.Mutex
)
func registeredConnection(key string, newConn meters.Connection) meters.Connection {
mu.Lock()
defer mu.Unlock()
if conn, ok := connections[key]; ok {
return conn
}
connections[key] = newConn
return newConn
}
// ProtocolFromRTU identifies the wire format from the RTU setting
func ProtocolFromRTU(rtu *bool) Protocol {
if rtu != nil && *rtu {
return Rtu
}
return Tcp
}
// NewConnection creates physical modbus device from config
func NewConnection(uri, device, comset string, baudrate int, proto Protocol, slaveID uint8) (*Connection, error) {
var conn meters.Connection
if device != "" && uri != "" {
return nil, errors.New("invalid modbus configuration: can only have either uri or device")
}
if device != "" {
switch strings.ToUpper(comset) {
case "8N1", "8E1", "8N2":
case "80":
comset = "8E1"
default:
return nil, fmt.Errorf("invalid comset: %s", comset)
}
if baudrate == 0 {
return nil, errors.New("invalid modbus configuration: need baudrate and comset")
}
if proto == Ascii {
conn = registeredConnection(device, meters.NewASCII(device, baudrate, comset))
} else {
conn = registeredConnection(device, meters.NewRTU(device, baudrate, comset))
}
}
if uri != "" {
uri = util.DefaultPort(uri, 502)
switch proto {
case Rtu:
conn = registeredConnection(uri, meters.NewRTUOverTCP(uri))
case Ascii:
conn = registeredConnection(uri, meters.NewASCIIOverTCP(uri))
default:
conn = registeredConnection(uri, meters.NewTCP(uri))
}
}
if conn == nil {
return nil, errors.New("invalid modbus configuration: need either uri or device")
}
slaveConn := &Connection{
slaveID: slaveID,
conn: conn,
}
return slaveConn, nil
}
// NewDevice creates physical modbus device from config
func NewDevice(model string, subdevice int) (device meters.Device, err error) {
if IsRS485(model) {
device, err = rs485.NewDevice(strings.ToUpper(model))
} else {
device = sunspec.NewDevice(strings.ToUpper(model), subdevice)
}
if device == nil {
err = errors.New("invalid modbus configuration: need either uri or device")
}
return device, err
}
// IsRS485 determines if model is a known MBMD rs485 device model
func IsRS485(model string) bool {
for k := range rs485.Producers {
if strings.EqualFold(model, k) {
return true
}
}
return false
}
// RS485FindDeviceOp checks is RS485 device supports operation
func RS485FindDeviceOp(device *rs485.RS485, measurement meters.Measurement) (op rs485.Operation, err error) {
ops := device.Producer().Produce()
for _, op := range ops {
if op.IEC61850 == measurement {
return op, nil
}
}
return op, fmt.Errorf("unsupported measurement: %s", measurement.String())
}