1377 lines
34 KiB
Go
1377 lines
34 KiB
Go
package modbus
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log"
|
|
"net"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type RegType uint
|
|
type Endianness uint
|
|
type WordOrder uint
|
|
|
|
const (
|
|
PARITY_NONE uint = 0
|
|
PARITY_EVEN uint = 1
|
|
PARITY_ODD uint = 2
|
|
|
|
HOLDING_REGISTER RegType = 0
|
|
INPUT_REGISTER RegType = 1
|
|
|
|
// endianness of 16-bit registers
|
|
BIG_ENDIAN Endianness = 1
|
|
LITTLE_ENDIAN Endianness = 2
|
|
|
|
// word order of 32-bit registers
|
|
HIGH_WORD_FIRST WordOrder = 1
|
|
LOW_WORD_FIRST WordOrder = 2
|
|
)
|
|
|
|
// Modbus client configuration object.
|
|
type ClientConfiguration struct {
|
|
// URL sets the client mode and target location in the form
|
|
// <mode>://<serial device or host:port> e.g. tcp://plc:502
|
|
URL string
|
|
// Speed sets the serial link speed (in bps, rtu only)
|
|
Speed uint
|
|
// DataBits sets the number of bits per serial character (rtu only)
|
|
DataBits uint
|
|
// Parity sets the serial link parity mode (rtu only)
|
|
Parity uint
|
|
// StopBits sets the number of serial stop bits (rtu only)
|
|
StopBits uint
|
|
// Timeout sets the request timeout value
|
|
Timeout time.Duration
|
|
// TLSClientCert sets the client-side TLS key pair (tcp+tls only)
|
|
TLSClientCert *tls.Certificate
|
|
// TLSRootCAs sets the list of CA certificates used to authenticate
|
|
// the server (tcp+tls only). Leaf (i.e. server) certificates can also
|
|
// be used in case of self-signed certs, or if cert pinning is required.
|
|
TLSRootCAs *x509.CertPool
|
|
// Logger provides a custom sink for log messages.
|
|
// If nil, messages will be written to stdout.
|
|
Logger *log.Logger
|
|
}
|
|
|
|
// Modbus client object.
|
|
type ModbusClient struct {
|
|
conf ClientConfiguration
|
|
logger *logger
|
|
lock sync.Mutex
|
|
endianness Endianness
|
|
wordOrder WordOrder
|
|
transport transport
|
|
unitId uint8
|
|
transportType transportType
|
|
}
|
|
|
|
// NewClient creates, configures and returns a modbus client object.
|
|
func NewClient(conf *ClientConfiguration) (mc *ModbusClient, err error) {
|
|
var clientType string
|
|
var splitURL []string
|
|
|
|
mc = &ModbusClient{
|
|
conf: *conf,
|
|
}
|
|
|
|
splitURL = strings.SplitN(mc.conf.URL, "://", 2)
|
|
if len(splitURL) == 2 {
|
|
clientType = splitURL[0]
|
|
mc.conf.URL = splitURL[1]
|
|
}
|
|
|
|
mc.logger = newLogger(
|
|
fmt.Sprintf("modbus-client(%s)", mc.conf.URL), conf.Logger)
|
|
|
|
switch clientType {
|
|
case "rtu":
|
|
// set useful defaults
|
|
if mc.conf.Speed == 0 {
|
|
mc.conf.Speed = 19200
|
|
}
|
|
|
|
// note: the "modbus over serial line v1.02" document specifies an
|
|
// 11-bit character frame, with even parity and 1 stop bit as default,
|
|
// and mandates the use of 2 stop bits when no parity is used.
|
|
// This stack defaults to 8/N/2 as most devices seem to use no parity,
|
|
// but giving 8/N/1, 8/E/1 and 8/O/1 a shot may help with serial
|
|
// issues.
|
|
if mc.conf.DataBits == 0 {
|
|
mc.conf.DataBits = 8
|
|
}
|
|
|
|
if mc.conf.StopBits == 0 {
|
|
if mc.conf.Parity == PARITY_NONE {
|
|
mc.conf.StopBits = 2
|
|
} else {
|
|
mc.conf.StopBits = 1
|
|
}
|
|
}
|
|
|
|
if mc.conf.Timeout == 0 {
|
|
mc.conf.Timeout = 300 * time.Millisecond
|
|
}
|
|
|
|
mc.transportType = modbusRTU
|
|
|
|
case "rtuovertcp":
|
|
if mc.conf.Speed == 0 {
|
|
mc.conf.Speed = 19200
|
|
}
|
|
|
|
if mc.conf.Timeout == 0 {
|
|
mc.conf.Timeout = 1 * time.Second
|
|
}
|
|
|
|
mc.transportType = modbusRTUOverTCP
|
|
|
|
case "rtuoverudp":
|
|
if mc.conf.Speed == 0 {
|
|
mc.conf.Speed = 19200
|
|
}
|
|
|
|
if mc.conf.Timeout == 0 {
|
|
mc.conf.Timeout = 1 * time.Second
|
|
}
|
|
|
|
mc.transportType = modbusRTUOverUDP
|
|
|
|
case "tcp":
|
|
if mc.conf.Timeout == 0 {
|
|
mc.conf.Timeout = 1 * time.Second
|
|
}
|
|
|
|
mc.transportType = modbusTCP
|
|
|
|
case "tcp+tls":
|
|
if mc.conf.Timeout == 0 {
|
|
mc.conf.Timeout = 1 * time.Second
|
|
}
|
|
|
|
// expect a client-side certificate for mutual auth as the
|
|
// modbus/mpab protocol has no inherent auth facility.
|
|
// (see requirements R-08 and R-19 of the MBAPS spec)
|
|
if mc.conf.TLSClientCert == nil {
|
|
mc.logger.Errorf("missing client certificate")
|
|
err = ErrConfigurationError
|
|
return
|
|
}
|
|
|
|
// expect a CertPool object containing at least 1 CA or
|
|
// leaf certificate to validate the server-side cert
|
|
if mc.conf.TLSRootCAs == nil {
|
|
mc.logger.Errorf("missing CA/server certificate")
|
|
err = ErrConfigurationError
|
|
return
|
|
}
|
|
|
|
mc.transportType = modbusTCPOverTLS
|
|
|
|
case "udp":
|
|
if mc.conf.Timeout == 0 {
|
|
mc.conf.Timeout = 1 * time.Second
|
|
}
|
|
|
|
mc.transportType = modbusTCPOverUDP
|
|
|
|
default:
|
|
if len(splitURL) != 2 {
|
|
mc.logger.Errorf("missing client type in URL '%s'", mc.conf.URL)
|
|
} else {
|
|
mc.logger.Errorf("unsupported client type '%s'", clientType)
|
|
}
|
|
err = ErrConfigurationError
|
|
return
|
|
}
|
|
|
|
mc.unitId = 1
|
|
mc.endianness = BIG_ENDIAN
|
|
mc.wordOrder = HIGH_WORD_FIRST
|
|
|
|
return
|
|
}
|
|
|
|
// Opens the underlying transport (network socket or serial line).
|
|
func (mc *ModbusClient) Open() (err error) {
|
|
var spw *serialPortWrapper
|
|
var sock net.Conn
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
switch mc.transportType {
|
|
case modbusRTU:
|
|
// create a serial port wrapper object
|
|
spw = newSerialPortWrapper(&serialPortConfig{
|
|
Device: mc.conf.URL,
|
|
Speed: mc.conf.Speed,
|
|
DataBits: mc.conf.DataBits,
|
|
Parity: mc.conf.Parity,
|
|
StopBits: mc.conf.StopBits,
|
|
})
|
|
|
|
// open the serial device
|
|
err = spw.Open()
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// discard potentially stale serial data
|
|
discard(spw)
|
|
|
|
// create the RTU transport
|
|
mc.transport = newRTUTransport(
|
|
spw, mc.conf.URL, mc.conf.Speed, mc.conf.Timeout, mc.conf.Logger)
|
|
|
|
case modbusRTUOverTCP:
|
|
// connect to the remote host
|
|
sock, err = net.DialTimeout("tcp", mc.conf.URL, 5*time.Second)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// discard potentially stale serial data
|
|
discard(sock)
|
|
|
|
// create the RTU transport
|
|
mc.transport = newRTUTransport(
|
|
sock, mc.conf.URL, mc.conf.Speed, mc.conf.Timeout, mc.conf.Logger)
|
|
|
|
case modbusRTUOverUDP:
|
|
// open a socket to the remote host (note: no actual connection is
|
|
// being made as UDP is connection-less)
|
|
sock, err = net.DialTimeout("udp", mc.conf.URL, 5*time.Second)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// create the RTU transport, wrapping the UDP socket in
|
|
// an adapter to allow the transport to read the stream of
|
|
// packets byte per byte
|
|
mc.transport = newRTUTransport(
|
|
newUDPSockWrapper(sock),
|
|
mc.conf.URL, mc.conf.Speed, mc.conf.Timeout, mc.conf.Logger)
|
|
|
|
case modbusTCP:
|
|
// connect to the remote host
|
|
sock, err = net.DialTimeout("tcp", mc.conf.URL, 5*time.Second)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// create the TCP transport
|
|
mc.transport = newTCPTransport(sock, mc.conf.Timeout, mc.conf.Logger)
|
|
|
|
case modbusTCPOverTLS:
|
|
// connect to the remote host with TLS
|
|
sock, err = tls.DialWithDialer(
|
|
&net.Dialer{
|
|
Deadline: time.Now().Add(15 * time.Second),
|
|
}, "tcp", mc.conf.URL,
|
|
&tls.Config{
|
|
Certificates: []tls.Certificate{
|
|
*mc.conf.TLSClientCert,
|
|
},
|
|
RootCAs: mc.conf.TLSRootCAs,
|
|
// mandate TLS 1.2 or higher (see R-01 of the MBAPS spec)
|
|
MinVersion: tls.VersionTLS12,
|
|
})
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// force the TLS handshake
|
|
err = sock.(*tls.Conn).Handshake()
|
|
if err != nil {
|
|
sock.Close()
|
|
return
|
|
}
|
|
|
|
// create the TCP transport, wrapping the TLS socket in
|
|
// an adapter to work around write timeouts corrupting internal
|
|
// state (see https://pkg.go.dev/crypto/tls#Conn.SetWriteDeadline)
|
|
mc.transport = newTCPTransport(
|
|
newTLSSockWrapper(sock), mc.conf.Timeout, mc.conf.Logger)
|
|
|
|
case modbusTCPOverUDP:
|
|
// open a socket to the remote host (note: no actual connection is
|
|
// being made as UDP is connection-less)
|
|
sock, err = net.DialTimeout("udp", mc.conf.URL, 5*time.Second)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// create the TCP transport, wrapping the UDP socket in
|
|
// an adapter to allow the transport to read the stream of
|
|
// packets byte per byte
|
|
mc.transport = newTCPTransport(
|
|
newUDPSockWrapper(sock), mc.conf.Timeout, mc.conf.Logger)
|
|
|
|
default:
|
|
// should never happen
|
|
err = ErrConfigurationError
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Closes the underlying transport.
|
|
func (mc *ModbusClient) Close() (err error) {
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
if mc.transport != nil {
|
|
err = mc.transport.Close()
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Sets the unit id of subsequent requests.
|
|
func (mc *ModbusClient) SetUnitId(id uint8) (err error) {
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
mc.unitId = id
|
|
|
|
return
|
|
}
|
|
|
|
// Sets the encoding (endianness and word ordering) of subsequent requests.
|
|
func (mc *ModbusClient) SetEncoding(endianness Endianness, wordOrder WordOrder) (err error) {
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
if endianness != BIG_ENDIAN && endianness != LITTLE_ENDIAN {
|
|
mc.logger.Errorf("unknown endianness value %v", endianness)
|
|
err = ErrUnexpectedParameters
|
|
return
|
|
}
|
|
|
|
if wordOrder != HIGH_WORD_FIRST && wordOrder != LOW_WORD_FIRST {
|
|
mc.logger.Errorf("unknown word order value %v", wordOrder)
|
|
err = ErrUnexpectedParameters
|
|
return
|
|
}
|
|
|
|
mc.endianness = endianness
|
|
mc.wordOrder = wordOrder
|
|
|
|
return
|
|
}
|
|
|
|
// Reads multiple coils (function code 01).
|
|
func (mc *ModbusClient) ReadCoils(addr uint16, quantity uint16) (values []bool, err error) {
|
|
values, err = mc.readBools(addr, quantity, false)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads a single coil (function code 01).
|
|
func (mc *ModbusClient) ReadCoil(addr uint16) (value bool, err error) {
|
|
var values []bool
|
|
|
|
values, err = mc.readBools(addr, 1, false)
|
|
if err == nil {
|
|
value = values[0]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads multiple discrete inputs (function code 02).
|
|
func (mc *ModbusClient) ReadDiscreteInputs(addr uint16, quantity uint16) (values []bool, err error) {
|
|
values, err = mc.readBools(addr, quantity, true)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads a single discrete input (function code 02).
|
|
func (mc *ModbusClient) ReadDiscreteInput(addr uint16) (value bool, err error) {
|
|
var values []bool
|
|
|
|
values, err = mc.readBools(addr, 1, true)
|
|
if err == nil {
|
|
value = values[0]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads multiple 16-bit registers (function code 03 or 04).
|
|
func (mc *ModbusClient) ReadRegisters(addr uint16, quantity uint16, regType RegType) (values []uint16, err error) {
|
|
var mbPayload []byte
|
|
|
|
// read quantity uint16 registers, as bytes
|
|
mbPayload, err = mc.readRegisters(addr, quantity, regType)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// decode payload bytes as uint16s
|
|
values = bytesToUint16s(mc.endianness, mbPayload)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads multiple 16-bit registers with function code
|
|
func (mc *ModbusClient) ReadRegistersWithFunctionCode(addr uint16, quantity uint16, funcCode uint8) (values []uint16, err error) {
|
|
var mbPayload []byte
|
|
|
|
// read quantity uint16 registers, as bytes
|
|
mbPayload, err = mc.readRegistersWithFunctionCode(addr, quantity, funcCode)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// decode payload bytes as uint16s
|
|
values = bytesToUint16s(mc.endianness, mbPayload)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads a single 16-bit register (function code 03 or 04).
|
|
func (mc *ModbusClient) ReadRegister(addr uint16, regType RegType) (value uint16, err error) {
|
|
var values []uint16
|
|
|
|
// read 1 uint16 register, as bytes
|
|
values, err = mc.ReadRegisters(addr, 1, regType)
|
|
if err == nil {
|
|
value = values[0]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads multiple 32-bit registers.
|
|
func (mc *ModbusClient) ReadUint32s(addr uint16, quantity uint16, regType RegType) (values []uint32, err error) {
|
|
var mbPayload []byte
|
|
|
|
// read 2 * quantity uint16 registers, as bytes
|
|
mbPayload, err = mc.readRegisters(addr, quantity*2, regType)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// decode payload bytes as uint32s
|
|
values = bytesToUint32s(mc.endianness, mc.wordOrder, mbPayload)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads a single 32-bit register.
|
|
func (mc *ModbusClient) ReadUint32(addr uint16, regType RegType) (value uint32, err error) {
|
|
var values []uint32
|
|
|
|
values, err = mc.ReadUint32s(addr, 1, regType)
|
|
if err == nil {
|
|
value = values[0]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads multiple 32-bit float registers.
|
|
func (mc *ModbusClient) ReadFloat32s(addr uint16, quantity uint16, regType RegType) (values []float32, err error) {
|
|
var mbPayload []byte
|
|
|
|
// read 2 * quantity uint16 registers, as bytes
|
|
mbPayload, err = mc.readRegisters(addr, quantity*2, regType)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// decode payload bytes as float32s
|
|
values = bytesToFloat32s(mc.endianness, mc.wordOrder, mbPayload)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads a single 32-bit float register.
|
|
func (mc *ModbusClient) ReadFloat32(addr uint16, regType RegType) (value float32, err error) {
|
|
var values []float32
|
|
|
|
values, err = mc.ReadFloat32s(addr, 1, regType)
|
|
if err == nil {
|
|
value = values[0]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads multiple 64-bit registers.
|
|
func (mc *ModbusClient) ReadUint64s(addr uint16, quantity uint16, regType RegType) (values []uint64, err error) {
|
|
var mbPayload []byte
|
|
|
|
// read 4 * quantity uint16 registers, as bytes
|
|
mbPayload, err = mc.readRegisters(addr, quantity*4, regType)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// decode payload bytes as uint64s
|
|
values = bytesToUint64s(mc.endianness, mc.wordOrder, mbPayload)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads a single 64-bit register.
|
|
func (mc *ModbusClient) ReadUint64(addr uint16, regType RegType) (value uint64, err error) {
|
|
var values []uint64
|
|
|
|
values, err = mc.ReadUint64s(addr, 1, regType)
|
|
if err == nil {
|
|
value = values[0]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads multiple 64-bit float registers.
|
|
func (mc *ModbusClient) ReadFloat64s(addr uint16, quantity uint16, regType RegType) (values []float64, err error) {
|
|
var mbPayload []byte
|
|
|
|
// read 4 * quantity uint16 registers, as bytes
|
|
mbPayload, err = mc.readRegisters(addr, quantity*4, regType)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// decode payload bytes as float64s
|
|
values = bytesToFloat64s(mc.endianness, mc.wordOrder, mbPayload)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads a single 64-bit float register.
|
|
func (mc *ModbusClient) ReadFloat64(addr uint16, regType RegType) (value float64, err error) {
|
|
var values []float64
|
|
|
|
values, err = mc.ReadFloat64s(addr, 1, regType)
|
|
if err == nil {
|
|
value = values[0]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads one or multiple 16-bit registers (function code 03 or 04) as bytes.
|
|
// A per-register byteswap is performed if endianness is set to LITTLE_ENDIAN.
|
|
func (mc *ModbusClient) ReadBytes(addr uint16, quantity uint16, regType RegType) (values []byte, err error) {
|
|
values, err = mc.readBytes(addr, quantity, regType, true)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads one or multiple 16-bit registers (function code 03 or 04) as bytes.
|
|
// No byte or word reordering is performed: bytes are returned exactly as they come
|
|
// off the wire, allowing the caller to handle encoding/endianness/word order manually.
|
|
func (mc *ModbusClient) ReadRawBytes(addr uint16, quantity uint16, regType RegType) (values []byte, err error) {
|
|
values, err = mc.readBytes(addr, quantity, regType, false)
|
|
|
|
return
|
|
}
|
|
|
|
// Writes a single coil (function code 05)
|
|
func (mc *ModbusClient) WriteCoil(addr uint16, value bool) (err error) {
|
|
var req *pdu
|
|
var res *pdu
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
// create and fill in the request object
|
|
req = &pdu{
|
|
unitId: mc.unitId,
|
|
functionCode: fcWriteSingleCoil,
|
|
}
|
|
|
|
// coil address
|
|
req.payload = uint16ToBytes(BIG_ENDIAN, addr)
|
|
// coil value
|
|
if value {
|
|
req.payload = append(req.payload, 0xff, 0x00)
|
|
} else {
|
|
req.payload = append(req.payload, 0x00, 0x00)
|
|
}
|
|
|
|
// run the request across the transport and wait for a response
|
|
res, err = mc.executeRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// validate the response code
|
|
switch {
|
|
case res.functionCode == req.functionCode:
|
|
// expect 4 bytes (2 byte of address + 2 bytes of value)
|
|
if len(res.payload) != 4 ||
|
|
// bytes 1-2 should be the coil address
|
|
bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr ||
|
|
// bytes 3-4 should either be {0xff, 0x00} or {0x00, 0x00}
|
|
// depending on the coil value
|
|
(value == true && res.payload[2] != 0xff) ||
|
|
res.payload[3] != 0x00 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
case res.functionCode == (req.functionCode | 0x80):
|
|
if len(res.payload) != 1 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
err = mapExceptionCodeToError(res.payload[0])
|
|
|
|
default:
|
|
err = ErrProtocolError
|
|
mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Writes multiple coils (function code 15)
|
|
func (mc *ModbusClient) WriteCoils(addr uint16, values []bool) (err error) {
|
|
var req *pdu
|
|
var res *pdu
|
|
var quantity uint16
|
|
var encodedValues []byte
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
quantity = uint16(len(values))
|
|
if quantity == 0 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of coils is 0")
|
|
return
|
|
}
|
|
|
|
if quantity > 0x7b0 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of coils exceeds 1968")
|
|
return
|
|
}
|
|
|
|
if uint32(addr)+uint32(quantity)-1 > 0xffff {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("end coil address is past 0xffff")
|
|
return
|
|
}
|
|
|
|
encodedValues = encodeBools(values)
|
|
|
|
// create and fill in the request object
|
|
req = &pdu{
|
|
unitId: mc.unitId,
|
|
functionCode: fcWriteMultipleCoils,
|
|
}
|
|
|
|
// start address
|
|
req.payload = uint16ToBytes(BIG_ENDIAN, addr)
|
|
// quantity
|
|
req.payload = append(req.payload, uint16ToBytes(BIG_ENDIAN, quantity)...)
|
|
// byte count
|
|
req.payload = append(req.payload, byte(len(encodedValues)))
|
|
// payload
|
|
req.payload = append(req.payload, encodedValues...)
|
|
|
|
// run the request across the transport and wait for a response
|
|
res, err = mc.executeRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// validate the response code
|
|
switch {
|
|
case res.functionCode == req.functionCode:
|
|
// expect 4 bytes (2 byte of address + 2 bytes of quantity)
|
|
if len(res.payload) != 4 ||
|
|
// bytes 1-2 should be the base coil address
|
|
bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr ||
|
|
// bytes 3-4 should be the quantity of coils
|
|
bytesToUint16(BIG_ENDIAN, res.payload[2:4]) != quantity {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
case res.functionCode == (req.functionCode | 0x80):
|
|
if len(res.payload) != 1 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
err = mapExceptionCodeToError(res.payload[0])
|
|
|
|
default:
|
|
err = ErrProtocolError
|
|
mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Writes a single 16-bit register (function code 06).
|
|
func (mc *ModbusClient) WriteRegister(addr uint16, value uint16) (err error) {
|
|
var req *pdu
|
|
var res *pdu
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
// create and fill in the request object
|
|
req = &pdu{
|
|
unitId: mc.unitId,
|
|
functionCode: fcWriteSingleRegister,
|
|
}
|
|
|
|
// register address
|
|
req.payload = uint16ToBytes(BIG_ENDIAN, addr)
|
|
// register value
|
|
req.payload = append(req.payload, uint16ToBytes(mc.endianness, value)...)
|
|
|
|
// run the request across the transport and wait for a response
|
|
res, err = mc.executeRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// validate the response code
|
|
switch {
|
|
case res.functionCode == req.functionCode:
|
|
// expect 4 bytes (2 byte of address + 2 bytes of value)
|
|
if len(res.payload) != 4 ||
|
|
// bytes 1-2 should be the register address
|
|
bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr ||
|
|
// bytes 3-4 should be the value
|
|
bytesToUint16(mc.endianness, res.payload[2:4]) != value {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
case res.functionCode == (req.functionCode | 0x80):
|
|
if len(res.payload) != 1 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
err = mapExceptionCodeToError(res.payload[0])
|
|
|
|
default:
|
|
err = ErrProtocolError
|
|
mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Writes a single 16-bit register (function code 06).
|
|
func (mc *ModbusClient) WriteRegisterWithRes(addr uint16, value uint16) (bytes []byte, err error) {
|
|
var req *pdu
|
|
var res *pdu
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
// create and fill in the request object
|
|
req = &pdu{
|
|
unitId: mc.unitId,
|
|
functionCode: fcWriteSingleRegister,
|
|
}
|
|
|
|
// register address
|
|
req.payload = uint16ToBytes(BIG_ENDIAN, addr)
|
|
// register value
|
|
req.payload = append(req.payload, uint16ToBytes(mc.endianness, value)...)
|
|
|
|
// run the request across the transport and wait for a response
|
|
res, err = mc.executeRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// validate the response code
|
|
switch {
|
|
case res.functionCode == req.functionCode:
|
|
// expect 4 bytes (2 byte of address + 2 bytes of value)
|
|
if len(res.payload) != 4 ||
|
|
// bytes 1-2 should be the register address
|
|
bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr ||
|
|
// bytes 3-4 should be the value
|
|
bytesToUint16(mc.endianness, res.payload[2:4]) != value {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
bytes = req.payload[1:]
|
|
|
|
case res.functionCode == (req.functionCode | 0x80):
|
|
if len(res.payload) != 1 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
err = mapExceptionCodeToError(res.payload[0])
|
|
|
|
default:
|
|
err = ErrProtocolError
|
|
mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Writes multiple 16-bit registers (function code 16).
|
|
func (mc *ModbusClient) WriteRegisters(addr uint16, values []uint16) (err error) {
|
|
var payload []byte
|
|
|
|
// turn registers to bytes
|
|
for _, value := range values {
|
|
payload = append(payload, uint16ToBytes(mc.endianness, value)...)
|
|
}
|
|
|
|
err = mc.writeRegisters(addr, payload)
|
|
|
|
return
|
|
}
|
|
|
|
// Writes multiple 32-bit registers.
|
|
func (mc *ModbusClient) WriteUint32s(addr uint16, values []uint32) (err error) {
|
|
var payload []byte
|
|
|
|
// turn registers to bytes
|
|
for _, value := range values {
|
|
payload = append(payload, uint32ToBytes(mc.endianness, mc.wordOrder, value)...)
|
|
}
|
|
|
|
err = mc.writeRegisters(addr, payload)
|
|
|
|
return
|
|
}
|
|
|
|
// Writes a single 32-bit register.
|
|
func (mc *ModbusClient) WriteUint32(addr uint16, value uint32) (err error) {
|
|
err = mc.writeRegisters(addr, uint32ToBytes(mc.endianness, mc.wordOrder, value))
|
|
|
|
return
|
|
}
|
|
|
|
// Writes multiple 32-bit float registers.
|
|
func (mc *ModbusClient) WriteFloat32s(addr uint16, values []float32) (err error) {
|
|
var payload []byte
|
|
|
|
// turn registers to bytes
|
|
for _, value := range values {
|
|
payload = append(payload, float32ToBytes(mc.endianness, mc.wordOrder, value)...)
|
|
}
|
|
|
|
err = mc.writeRegisters(addr, payload)
|
|
|
|
return
|
|
}
|
|
|
|
// Writes a single 32-bit float register.
|
|
func (mc *ModbusClient) WriteFloat32(addr uint16, value float32) (err error) {
|
|
err = mc.writeRegisters(addr, float32ToBytes(mc.endianness, mc.wordOrder, value))
|
|
|
|
return
|
|
}
|
|
|
|
// Writes multiple 64-bit registers.
|
|
func (mc *ModbusClient) WriteUint64s(addr uint16, values []uint64) (err error) {
|
|
var payload []byte
|
|
|
|
// turn registers to bytes
|
|
for _, value := range values {
|
|
payload = append(payload, uint64ToBytes(mc.endianness, mc.wordOrder, value)...)
|
|
}
|
|
|
|
err = mc.writeRegisters(addr, payload)
|
|
|
|
return
|
|
}
|
|
|
|
// Writes a single 64-bit register.
|
|
func (mc *ModbusClient) WriteUint64(addr uint16, value uint64) (err error) {
|
|
err = mc.writeRegisters(addr, uint64ToBytes(mc.endianness, mc.wordOrder, value))
|
|
|
|
return
|
|
}
|
|
|
|
// Writes multiple 64-bit float registers.
|
|
func (mc *ModbusClient) WriteFloat64s(addr uint16, values []float64) (err error) {
|
|
var payload []byte
|
|
|
|
// turn registers to bytes
|
|
for _, value := range values {
|
|
payload = append(payload, float64ToBytes(mc.endianness, mc.wordOrder, value)...)
|
|
}
|
|
|
|
err = mc.writeRegisters(addr, payload)
|
|
|
|
return
|
|
}
|
|
|
|
// Writes a single 64-bit float register.
|
|
func (mc *ModbusClient) WriteFloat64(addr uint16, value float64) (err error) {
|
|
err = mc.writeRegisters(addr, float64ToBytes(mc.endianness, mc.wordOrder, value))
|
|
|
|
return
|
|
}
|
|
|
|
// Writes the given slice of bytes to 16-bit registers starting at addr.
|
|
// A per-register byteswap is performed if endianness is set to LITTLE_ENDIAN.
|
|
// Odd byte quantities are padded with a null byte to fall on 16-bit register boundaries.
|
|
func (mc *ModbusClient) WriteBytes(addr uint16, values []byte) (err error) {
|
|
err = mc.writeBytes(addr, values, true)
|
|
|
|
return
|
|
}
|
|
|
|
// Writes the given slice of bytes to 16-bit registers starting at addr.
|
|
// No byte or word reordering is performed: bytes are pushed to the wire as-is,
|
|
// allowing the caller to handle encoding/endianness/word order manually.
|
|
// Odd byte quantities are padded with a null byte to fall on 16-bit register boundaries.
|
|
func (mc *ModbusClient) WriteRawBytes(addr uint16, values []byte) (err error) {
|
|
err = mc.writeBytes(addr, values, false)
|
|
|
|
return
|
|
}
|
|
|
|
/*** unexported methods ***/
|
|
// Reads one or multiple 16-bit registers (function code 03 or 04) as bytes.
|
|
func (mc *ModbusClient) readBytes(addr uint16, quantity uint16, regType RegType, observeEndianness bool) (values []byte, err error) {
|
|
var regCount uint16
|
|
|
|
// read enough registers to get the requested number of bytes
|
|
// (2 bytes per reg)
|
|
regCount = (quantity / 2) + (quantity % 2)
|
|
|
|
values, err = mc.readRegisters(addr, regCount, regType)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// swap bytes on register boundaries if requested by the caller
|
|
// and endianness is set to little endian
|
|
if observeEndianness && mc.endianness == LITTLE_ENDIAN {
|
|
for i := 0; i < len(values); i += 2 {
|
|
values[i], values[i+1] = values[i+1], values[i]
|
|
}
|
|
}
|
|
|
|
// pop the last byte on odd quantities
|
|
if quantity%2 == 1 {
|
|
values = values[0 : len(values)-1]
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Writes the given slice of bytes to 16-bit registers starting at addr.
|
|
func (mc *ModbusClient) writeBytes(addr uint16, values []byte, observeEndianness bool) (err error) {
|
|
// pad odd quantities to make for full registers
|
|
if len(values)%2 == 1 {
|
|
values = append(values, 0x00)
|
|
}
|
|
|
|
// swap bytes on register boundaries if requested by the caller
|
|
// and endianness is set to little endian
|
|
if observeEndianness && mc.endianness == LITTLE_ENDIAN {
|
|
for i := 0; i < len(values); i += 2 {
|
|
values[i], values[i+1] = values[i+1], values[i]
|
|
}
|
|
}
|
|
|
|
err = mc.writeRegisters(addr, values)
|
|
|
|
return
|
|
}
|
|
|
|
// Reads and returns quantity booleans.
|
|
// Digital inputs are read if di is true, otherwise coils are read.
|
|
func (mc *ModbusClient) readBools(addr uint16, quantity uint16, di bool) (values []bool, err error) {
|
|
var req *pdu
|
|
var res *pdu
|
|
var expectedLen int
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
if quantity == 0 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of coils/discrete inputs is 0")
|
|
return
|
|
}
|
|
|
|
if quantity > 2000 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of coils/discrete inputs exceeds 2000")
|
|
return
|
|
}
|
|
|
|
if uint32(addr)+uint32(quantity)-1 > 0xffff {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("end coil/discrete input address is past 0xffff")
|
|
return
|
|
}
|
|
|
|
// create and fill in the request object
|
|
req = &pdu{
|
|
unitId: mc.unitId,
|
|
}
|
|
|
|
if di {
|
|
req.functionCode = fcReadDiscreteInputs
|
|
} else {
|
|
req.functionCode = fcReadCoils
|
|
}
|
|
|
|
// start address
|
|
req.payload = uint16ToBytes(BIG_ENDIAN, addr)
|
|
// quantity
|
|
req.payload = append(req.payload, uint16ToBytes(BIG_ENDIAN, quantity)...)
|
|
|
|
// run the request across the transport and wait for a response
|
|
res, err = mc.executeRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// validate the response code
|
|
switch {
|
|
case res.functionCode == req.functionCode:
|
|
// expect a payload of 1 byte (byte count) + 1 byte for 8 coils/discrete inputs)
|
|
expectedLen = 1
|
|
expectedLen += int(quantity) / 8
|
|
if quantity%8 != 0 {
|
|
expectedLen++
|
|
}
|
|
|
|
if len(res.payload) != expectedLen {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
// validate the byte count field
|
|
if int(res.payload[0])+1 != expectedLen {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
// turn bits into a bool slice
|
|
values = decodeBools(quantity, res.payload[1:])
|
|
|
|
case res.functionCode == (req.functionCode | 0x80):
|
|
if len(res.payload) != 1 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
err = mapExceptionCodeToError(res.payload[0])
|
|
|
|
default:
|
|
err = ErrProtocolError
|
|
mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads and returns quantity registers of type regType, as bytes.
|
|
func (mc *ModbusClient) readRegisters(addr uint16, quantity uint16, regType RegType) (bytes []byte, err error) {
|
|
var req *pdu
|
|
var res *pdu
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
// create and fill in the request object
|
|
req = &pdu{
|
|
unitId: mc.unitId,
|
|
}
|
|
|
|
switch regType {
|
|
case HOLDING_REGISTER:
|
|
req.functionCode = fcReadHoldingRegisters
|
|
case INPUT_REGISTER:
|
|
req.functionCode = fcReadInputRegisters
|
|
default:
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Errorf("unexpected register type (%v)", regType)
|
|
return
|
|
}
|
|
|
|
if quantity == 0 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of registers is 0")
|
|
return
|
|
}
|
|
|
|
if quantity > 125 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of registers exceeds 125")
|
|
return
|
|
}
|
|
|
|
if uint32(addr)+uint32(quantity)-1 > 0xffff {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("end register address is past 0xffff")
|
|
return
|
|
}
|
|
|
|
// start address
|
|
req.payload = uint16ToBytes(BIG_ENDIAN, addr)
|
|
// quantity
|
|
req.payload = append(req.payload, uint16ToBytes(BIG_ENDIAN, quantity)...)
|
|
|
|
// run the request across the transport and wait for a response
|
|
res, err = mc.executeRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// validate the response code
|
|
switch {
|
|
case res.functionCode == req.functionCode:
|
|
// make sure the payload length is what we expect
|
|
// (1 byte of length + 2 bytes per register)
|
|
if len(res.payload) != 1+2*int(quantity) {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
// validate the byte count field
|
|
// (2 bytes per register * number of registers)
|
|
if uint(res.payload[0]) != 2*uint(quantity) {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
// remove the byte count field from the returned slice
|
|
bytes = res.payload[1:]
|
|
|
|
case res.functionCode == (req.functionCode | 0x80):
|
|
if len(res.payload) != 1 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
err = mapExceptionCodeToError(res.payload[0])
|
|
|
|
default:
|
|
err = ErrProtocolError
|
|
mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Reads and returns quantity registers of type regType, as bytes.
|
|
func (mc *ModbusClient) readRegistersWithFunctionCode(addr uint16, quantity uint16, functionCode uint8) (bytes []byte, err error) {
|
|
var req *pdu
|
|
var res *pdu
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
// create and fill in the request object
|
|
req = &pdu{
|
|
unitId: mc.unitId,
|
|
}
|
|
|
|
if functionCode != fcCustomize {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Errorf("unexpected function code (%d)", functionCode)
|
|
return
|
|
}
|
|
|
|
if functionCode == 0 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Errorf("unexpected register type (%v)", functionCode)
|
|
return
|
|
}
|
|
|
|
req.functionCode = functionCode
|
|
|
|
if quantity == 0 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of registers is 0")
|
|
return
|
|
}
|
|
|
|
if quantity > 1024 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of registers exceeds 1024")
|
|
return
|
|
}
|
|
|
|
if uint32(addr)+uint32(quantity)-1 > 0xffff {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("end register address is past 0xffff")
|
|
return
|
|
}
|
|
|
|
// start address
|
|
req.payload = uint16ToBytes(BIG_ENDIAN, addr)
|
|
// quantity
|
|
req.payload = append(req.payload, uint16ToBytes(BIG_ENDIAN, quantity)...)
|
|
|
|
// run the request across the transport and wait for a response
|
|
res, err = mc.executeRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// validate the response code
|
|
switch {
|
|
case res.functionCode == req.functionCode:
|
|
// make sure the payload length is what we expect
|
|
// (1 byte of length + 2 bytes per register)
|
|
if len(res.payload) != 1+2*int(quantity) {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
// validate the byte count field
|
|
// (2 bytes per register * number of registers)
|
|
if uint(res.payload[0]) != 2*uint(quantity) {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
// remove the byte count field from the returned slice
|
|
bytes = res.payload[1:]
|
|
|
|
case res.functionCode == (req.functionCode | 0x80):
|
|
if len(res.payload) != 1 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
err = mapExceptionCodeToError(res.payload[0])
|
|
|
|
default:
|
|
err = ErrProtocolError
|
|
mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// Writes multiple registers starting from base address addr.
|
|
// Register values are passed as bytes, each value being exactly 2 bytes.
|
|
func (mc *ModbusClient) writeRegisters(addr uint16, values []byte) (err error) {
|
|
var req *pdu
|
|
var res *pdu
|
|
var payloadLength uint16
|
|
var quantity uint16
|
|
|
|
mc.lock.Lock()
|
|
defer mc.lock.Unlock()
|
|
|
|
payloadLength = uint16(len(values))
|
|
quantity = payloadLength / 2
|
|
|
|
if quantity == 0 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of registers is 0")
|
|
return
|
|
}
|
|
|
|
if quantity > 123 {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("quantity of registers exceeds 123")
|
|
return
|
|
}
|
|
|
|
if uint32(addr)+uint32(quantity)-1 > 0xffff {
|
|
err = ErrUnexpectedParameters
|
|
mc.logger.Error("end register address is past 0xffff")
|
|
return
|
|
}
|
|
|
|
// create and fill in the request object
|
|
req = &pdu{
|
|
unitId: mc.unitId,
|
|
functionCode: fcWriteMultipleRegisters,
|
|
}
|
|
|
|
// base address
|
|
req.payload = uint16ToBytes(BIG_ENDIAN, addr)
|
|
// quantity of registers (2 bytes per register)
|
|
req.payload = append(req.payload, uint16ToBytes(BIG_ENDIAN, quantity)...)
|
|
// byte count
|
|
req.payload = append(req.payload, byte(payloadLength))
|
|
// registers value
|
|
req.payload = append(req.payload, values...)
|
|
|
|
// run the request across the transport and wait for a response
|
|
res, err = mc.executeRequest(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// validate the response code
|
|
switch {
|
|
case res.functionCode == req.functionCode:
|
|
// expect 4 bytes (2 byte of address + 2 bytes of quantity)
|
|
if len(res.payload) != 4 ||
|
|
// bytes 1-2 should be the base register address
|
|
bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr ||
|
|
// bytes 3-4 should be the quantity of registers (2 bytes per register)
|
|
bytesToUint16(BIG_ENDIAN, res.payload[2:4]) != quantity {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
case res.functionCode == (req.functionCode | 0x80):
|
|
if len(res.payload) != 1 {
|
|
err = ErrProtocolError
|
|
return
|
|
}
|
|
|
|
err = mapExceptionCodeToError(res.payload[0])
|
|
|
|
default:
|
|
err = ErrProtocolError
|
|
mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (mc *ModbusClient) executeRequest(req *pdu) (res *pdu, err error) {
|
|
// send the request over the wire, wait for and decode the response
|
|
res, err = mc.transport.ExecuteRequest(req)
|
|
if err != nil {
|
|
// map i/o timeouts to ErrRequestTimedOut
|
|
if os.IsTimeout(err) {
|
|
err = ErrRequestTimedOut
|
|
}
|
|
return
|
|
}
|
|
|
|
// make sure the source unit id matches that of the request
|
|
if (res.functionCode&0x80) == 0x00 && res.unitId != req.unitId {
|
|
err = ErrBadUnitId
|
|
return
|
|
}
|
|
// accept errors from gateway devices (using special unit id #255)
|
|
if (res.functionCode&0x80) == 0x80 &&
|
|
(res.unitId != req.unitId && res.unitId != 0xff) {
|
|
err = ErrBadUnitId
|
|
return
|
|
}
|
|
|
|
return
|
|
}
|