1473 lines
37 KiB
Go
1473 lines
37 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.executeRequestWithRes(req)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// validate the response code
|
||
switch {
|
||
case res.functionCode == req.functionCode:
|
||
// expect at least 4 bytes (2 byte of address + 2 bytes of value)
|
||
// 后面可能还有自定义数据
|
||
if len(res.payload) < 4 {
|
||
err = ErrProtocolError
|
||
return
|
||
}
|
||
|
||
// bytes 1-2 should be the register address
|
||
if 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
|
||
}
|
||
|
||
// 返回自定义数据(第4字节之后的数据)
|
||
if len(res.payload) > 4 {
|
||
bytes = res.payload[4:]
|
||
} else {
|
||
bytes = []byte{} // 没有自定义数据,返回空切片
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
req.functionCode = functionCode
|
||
|
||
if quantity == 0 {
|
||
err = ErrUnexpectedParameters
|
||
mc.logger.Error("quantity of registers is 0")
|
||
return
|
||
}
|
||
|
||
// 16 * 16 * 40
|
||
if quantity > 10240 {
|
||
err = ErrUnexpectedParameters
|
||
mc.logger.Error("quantity of registers exceeds 10240")
|
||
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)
|
||
// }
|
||
|
||
switch {
|
||
case res.functionCode == req.functionCode:
|
||
// For custom function code 0x41, the payload format is:
|
||
// [起始地址(2字节)] [字节数(2字节)] [数据...]
|
||
if functionCode == fcCustomize {
|
||
// validate minimum payload length (start address 2 bytes + byte count 2 bytes)
|
||
if len(res.payload) < 4 {
|
||
err = ErrProtocolError
|
||
mc.logger.Errorf("payload too short for custom function code: %d bytes", len(res.payload))
|
||
return
|
||
}
|
||
|
||
// extract byte count from payload (bytes 2-3, big endian)
|
||
byteCount := bytesToUint16(BIG_ENDIAN, res.payload[2:4])
|
||
|
||
// validate payload length matches expected data length
|
||
expectedLength := 4 + int(byteCount) // start address (2) + byte count (2) + data
|
||
if len(res.payload) != expectedLength {
|
||
err = ErrProtocolError
|
||
mc.logger.Errorf("payload length mismatch: expected %d, got %d", expectedLength, len(res.payload))
|
||
return
|
||
}
|
||
|
||
// validate byte count matches requested quantity
|
||
if byteCount != 2*quantity {
|
||
err = ErrProtocolError
|
||
mc.logger.Errorf("byte count mismatch: expected %d, got %d", 2*quantity, byteCount)
|
||
return
|
||
}
|
||
|
||
// extract only the data part (skip start address and byte count)
|
||
bytes = res.payload[4:]
|
||
} else {
|
||
// standard modbus protocol handling
|
||
// 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
|
||
}
|
||
|
||
func (mc *ModbusClient) executeRequestWithRes(req *pdu) (res *pdu, err error) {
|
||
// send the request over the wire, wait for and decode the response
|
||
res, err = mc.transport.ExecuteRequestWithRes(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
|
||
}
|