Skip to content
Open
Prev Previous commit
Next Next commit
Changed to modern serial library and more logging
  • Loading branch information
munnik committed Oct 2, 2024
commit 6d03bac3321f30f590dea1d96ab0690408a26a77
130 changes: 89 additions & 41 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,15 @@ import (
"strings"
"sync"
"time"

"go.bug.st/serial"
)

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

Expand All @@ -39,13 +37,13 @@ type ClientConfiguration struct {
// <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
Speed int
// DataBits sets the number of bits per serial character (rtu only)
DataBits uint
DataBits int
// Parity sets the serial link parity mode (rtu only)
Parity uint
Parity serial.Parity
// StopBits sets the number of serial stop bits (rtu only)
StopBits uint
StopBits serial.StopBits
// Timeout sets the request timeout value
Timeout time.Duration
// TLSClientCert sets the client-side TLS key pair (tcp+tls only)
Expand Down Expand Up @@ -106,12 +104,10 @@ func NewClient(conf *ClientConfiguration) (mc *ModbusClient, err error) {
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.Parity == serial.NoParity {
mc.conf.StopBits = serial.TwoStopBits
} else {
mc.conf.StopBits = serial.OneStopBit
}

if mc.conf.Timeout == 0 {
Expand Down Expand Up @@ -594,21 +590,31 @@ func (mc *ModbusClient) WriteCoil(addr uint16, value bool) (err error) {
// 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 {
if len(res.payload) != 4 {
err = ErrProtocolError
mc.logger.Warningf("the length of the payload is not 4 but %d", len(res.payload))
return
}
if bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr {
err = ErrProtocolError
mc.logger.Warningf("bytes 1-2 are not the expected coil address %d, but %d", addr, bytesToUint16(BIG_ENDIAN, res.payload[0:2]))
return
}
if value && (res.payload[2] != 0xff || res.payload[3] != 0x00) {
err = ErrProtocolError
mc.logger.Warningf("expected % x but got % x", []byte{0xff, 0x00}, res.payload[2:4])
return
}
if !value && (res.payload[2] != 0x00 || res.payload[3] != 0x00) {
err = ErrProtocolError
mc.logger.Warningf("expected % x but got % x", []byte{0x00, 0x00}, res.payload[2:4])
return
}

case res.functionCode == (req.functionCode | 0x80):
if len(res.payload) != 1 {
err = ErrProtocolError
mc.logger.Warningf("expected the length of the payload to be 1, but the length is %d", len(res.payload))
return
}

Expand Down Expand Up @@ -677,19 +683,26 @@ func (mc *ModbusClient) WriteCoils(addr uint16, values []bool) (err error) {
// 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 {
if len(res.payload) != 4 {
err = ErrProtocolError
mc.logger.Warningf("the length of the payload is not 4 but %d", len(res.payload))
return
}
if bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr {
err = ErrProtocolError
mc.logger.Warningf("bytes 1-2 are not the expected coil address %d, but %d", addr, bytesToUint16(BIG_ENDIAN, res.payload[0:2]))
return
}
if bytesToUint16(BIG_ENDIAN, res.payload[2:4]) != quantity {
err = ErrProtocolError
mc.logger.Warningf("bytes 3-4 are not the expected quantity of coils %d, but %d", quantity, bytesToUint16(BIG_ENDIAN, res.payload[2:4]))
return
}

case res.functionCode == (req.functionCode | 0x80):
if len(res.payload) != 1 {
err = ErrProtocolError
mc.logger.Warningf("expected the length of the payload to be 1, but the length is %d", len(res.payload))
return
}

Expand Down Expand Up @@ -731,19 +744,26 @@ func (mc *ModbusClient) WriteRegister(addr uint16, value uint16) (err error) {
// 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 {
if len(res.payload) != 4 {
err = ErrProtocolError
mc.logger.Warningf("the length of the payload is not 4 but %d", len(res.payload))
return
}
if bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr {
err = ErrProtocolError
mc.logger.Warningf("bytes 1-2 are not the expected coil address %d, but %d", addr, bytesToUint16(BIG_ENDIAN, res.payload[0:2]))
return
}
if bytesToUint16(mc.endianness, res.payload[2:4]) != value {
err = ErrProtocolError
mc.logger.Warningf("bytes 3-4 are not the expected value %d, but %d", value, bytesToUint16(mc.endianness, res.payload[2:4]))
return
}

case res.functionCode == (req.functionCode | 0x80):
if len(res.payload) != 1 {
err = ErrProtocolError
mc.logger.Warningf("expected the length of the payload to be 1, but the length is %d", len(res.payload))
return
}

Expand Down Expand Up @@ -986,12 +1006,14 @@ func (mc *ModbusClient) readBools(addr uint16, quantity uint16, di bool) (values

if len(res.payload) != expectedLen {
err = ErrProtocolError
mc.logger.Warningf("expected the length of the payload to be %d, but the length is %d", expectedLen, len(res.payload))
return
}

// validate the byte count field
if int(res.payload[0])+1 != expectedLen {
err = ErrProtocolError
mc.logger.Warningf("expected %d in the byte count field, but got %d", expectedLen, int(res.payload[0])+1)
return
}

Expand All @@ -1001,6 +1023,7 @@ func (mc *ModbusClient) readBools(addr uint16, quantity uint16, di bool) (values
case res.functionCode == (req.functionCode | 0x80):
if len(res.payload) != 1 {
err = ErrProtocolError
mc.logger.Warningf("expected the length of the payload to be 1, but the length is %d", len(res.payload))
return
}

Expand Down Expand Up @@ -1074,13 +1097,15 @@ func (mc *ModbusClient) readRegisters(addr uint16, quantity uint16, regType RegT
// (1 byte of length + 2 bytes per register)
if len(res.payload) != 1+2*int(quantity) {
err = ErrProtocolError
mc.logger.Warningf("expected the length of the payload to be %d, but the length is %d", 1+2*int(quantity), len(res.payload))
return
}

// validate the byte count field
// (2 bytes per register * number of registers)
if uint(res.payload[0]) != 2*uint(quantity) {
err = ErrProtocolError
mc.logger.Warningf("expected %d in the byte count field, but got %d", 2*uint(quantity), int(res.payload[0]))
return
}

Expand All @@ -1090,13 +1115,29 @@ func (mc *ModbusClient) readRegisters(addr uint16, quantity uint16, regType RegT
case res.functionCode == (req.functionCode | 0x80):
if len(res.payload) != 1 {
err = ErrProtocolError
mc.logger.Warningf("expected the length of the payload to be 1, but the length is %d", len(res.payload))
return
}

err = mapExceptionCodeToError(res.payload[0])

default:
err = ErrProtocolError
if len(res.payload) != 4 {
err = ErrProtocolError
mc.logger.Warningf("the length of the payload is not 4 but %d", len(res.payload))
return
}
if bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr {
err = ErrProtocolError
mc.logger.Warningf("bytes 1-2 are not the expected coil address %d, but %d", addr, bytesToUint16(BIG_ENDIAN, res.payload[0:2]))
return
}
if bytesToUint16(BIG_ENDIAN, res.payload[2:4]) != quantity {
err = ErrProtocolError
mc.logger.Warningf("bytes 3-4 are not the expected quantity of coils %d, but %d", quantity, bytesToUint16(BIG_ENDIAN, res.payload[2:4]))
return
}

mc.logger.Warningf("unexpected response code (%v)", res.functionCode)
}

Expand Down Expand Up @@ -1159,19 +1200,26 @@ func (mc *ModbusClient) writeRegisters(addr uint16, values []byte) (err error) {
// 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 {
if len(res.payload) != 4 {
err = ErrProtocolError
mc.logger.Warningf("the length of the payload is not 4 but %d", len(res.payload))
return
}
if bytesToUint16(BIG_ENDIAN, res.payload[0:2]) != addr {
err = ErrProtocolError
mc.logger.Warningf("bytes 1-2 are not the expected coil address %d, but %d", addr, bytesToUint16(BIG_ENDIAN, res.payload[0:2]))
return
}
if bytesToUint16(BIG_ENDIAN, res.payload[2:4]) != quantity {
err = ErrProtocolError
mc.logger.Warningf("bytes 3-4 are not the expected value %d, but %d", quantity, bytesToUint16(mc.endianness, res.payload[2:4]))
return
}

case res.functionCode == (req.functionCode | 0x80):
if len(res.payload) != 1 {
err = ErrProtocolError
mc.logger.Warningf("expected the length of the payload to be 1, but the length is %d", len(res.payload))
return
}

Expand Down
32 changes: 22 additions & 10 deletions cmd/modbus-cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/simonvetter/modbus"
"go.bug.st/serial"
)

func main() {
Expand All @@ -24,10 +25,10 @@ func main() {
var certPath string // path to TLS client certificate
var keyPath string // path to TLS client key
var clientKeyPair tls.Certificate
var speed uint
var dataBits uint
var speed int
var dataBits int
var parity string
var stopBits uint
var stopBits string
var endianness string
var wordOrder string
var timeout string
Expand All @@ -37,10 +38,10 @@ func main() {
var runList []operation

flag.StringVar(&target, "target", "", "target device to connect to (e.g. tcp://somehost:502) [required]")
flag.UintVar(&speed, "speed", 19200, "serial bus speed in bps (rtu)")
flag.UintVar(&dataBits, "data-bits", 8, "number of bits per character on the serial bus (rtu)")
flag.IntVar(&speed, "speed", 19200, "serial bus speed in bps (rtu)")
flag.IntVar(&dataBits, "data-bits", 8, "number of bits per character on the serial bus (rtu)")
flag.StringVar(&parity, "parity", "none", "parity bit <none|even|odd> on the serial bus (rtu)")
flag.UintVar(&stopBits, "stop-bits", 2, "number of stop bits <0|1|2>) on the serial bus (rtu)")
flag.StringVar(&stopBits, "stop-bits", "2", "number of stop bits <0|1|1.5|2>) on the serial bus (rtu)")
flag.StringVar(&timeout, "timeout", "3s", "timeout value")
flag.StringVar(&endianness, "endianness", "big", "register endianness <little|big>")
flag.StringVar(&wordOrder, "word-order", "highfirst", "word ordering for 32-bit registers <highfirst|hf|lowfirst|lf>")
Expand All @@ -66,21 +67,32 @@ func main() {
URL: target,
Speed: speed,
DataBits: dataBits,
StopBits: stopBits,
}

switch parity {
case "none":
config.Parity = modbus.PARITY_NONE
config.Parity = serial.NoParity
case "odd":
config.Parity = modbus.PARITY_ODD
config.Parity = serial.OddParity
case "even":
config.Parity = modbus.PARITY_EVEN
config.Parity = serial.EvenParity
default:
fmt.Printf("unknown parity setting '%s' (should be one of none, odd or even)\n",
parity)
os.Exit(1)
}
switch stopBits {
case "1":
config.StopBits = serial.OneStopBit
case "1.5":
config.StopBits = serial.OnePointFiveStopBits
case "2":
config.StopBits = serial.TwoStopBits
default:
fmt.Printf("unknown stop-bits setting '%s' (should be one of 1, 1.5 or 2)\n",
stopBits)
os.Exit(1)
}

config.Timeout, err = time.ParseDuration(timeout)
if err != nil {
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ module github.com/simonvetter/modbus

go 1.16

require github.com/goburrow/serial v0.1.0
require (
go.bug.st/serial v1.6.2
golang.org/x/sys v0.25.0 // indirect
)
20 changes: 18 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,18 @@
github.com/goburrow/serial v0.1.0 h1:v2T1SQa/dlUqQiYIT8+Cu7YolfqAi3K96UmhwYyuSrA=
github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlVLZJ8haA=
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8=
go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 h1:v6hYoSR9T5oet+pMXwUWkbiVqx/63mlHjefrHmxwfeY=
golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Loading