Files
modbus/client.go

1473 lines
37 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}