cfy-feate-dev #1
21
LICENSE.txt
Normal file
21
LICENSE.txt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2019 Simon Vetter
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
199
README.md
199
README.md
@@ -0,0 +1,199 @@
|
|||||||
|
## Go modbus stack
|
||||||
|
|
||||||
|
### Description
|
||||||
|
This package is a go implementation of the modbus protocol.
|
||||||
|
It aims to provide a simple-to-use, high-level API to interact with modbus
|
||||||
|
devices using native Go types.
|
||||||
|
|
||||||
|
Both client and server components are available.
|
||||||
|
|
||||||
|
The client supports the following modes:
|
||||||
|
- modbus RTU (serial, over both RS-232 and RS-485),
|
||||||
|
- modbus TCP (a.k.a. MBAP),
|
||||||
|
- modbus TCP over TLS (a.k.a. MBAPS or Modbus Security),
|
||||||
|
- modbus TCP over UDP (a.k.a. MBAP over UDP),
|
||||||
|
- modbus RTU over TCP (RTU tunneled in TCP for use with e.g. remote serial
|
||||||
|
ports or cheap TCP to serial bridges),
|
||||||
|
- modbus RTU over UDP (RTU tunneled in UDP).
|
||||||
|
|
||||||
|
Please note that UDP transports are not part of the Modbus specification.
|
||||||
|
Some devices expect MBAP (modbus TCP) framing in UDP packets while others
|
||||||
|
use RTU frames instead. The client support both so if unsure, try with
|
||||||
|
both udp:// and rtuoverudp:// schemes.
|
||||||
|
|
||||||
|
The server supports:
|
||||||
|
- modbus TCP (a.k.a. MBAP),
|
||||||
|
- modbus TCP over TLS (a.k.a. MBAPS or Modbus Security).
|
||||||
|
|
||||||
|
A CLI client is available in cmd/modbus-cli.go and can be built with
|
||||||
|
```bash
|
||||||
|
$ go build -o modbus-cli cmd/modbus-cli.go
|
||||||
|
$ ./modbus-cli --help
|
||||||
|
```
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
```bash
|
||||||
|
$ go get github.com/simonvetter/modbus
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the client
|
||||||
|
|
||||||
|
```golang
|
||||||
|
import (
|
||||||
|
"github.com/simonvetter/modbus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var client *modbus.ModbusClient
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// for a TCP endpoint
|
||||||
|
// (see examples/tls_client.go for TLS usage and options)
|
||||||
|
client, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||||
|
URL: "tcp://hostname-or-ip-address:502",
|
||||||
|
Timeout: 1 * time.Second,
|
||||||
|
})
|
||||||
|
// note: use udp:// for modbus TCP over UDP
|
||||||
|
|
||||||
|
// for an RTU (serial) device/bus
|
||||||
|
client, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||||
|
URL: "rtu:///dev/ttyUSB0",
|
||||||
|
Speed: 19200, // default
|
||||||
|
DataBits: 8, // default, optional
|
||||||
|
Parity: modbus.PARITY_NONE, // default, optional
|
||||||
|
StopBits: 2, // default if no parity, optional
|
||||||
|
Timeout: 300 * time.Millisecond,
|
||||||
|
})
|
||||||
|
|
||||||
|
// for an RTU over TCP device/bus (remote serial port or
|
||||||
|
// simple TCP-to-serial bridge)
|
||||||
|
client, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||||
|
URL: "rtuovertcp://hostname-or-ip-address:502",
|
||||||
|
Speed: 19200, // serial link speed
|
||||||
|
Timeout: 1 * time.Second,
|
||||||
|
})
|
||||||
|
// note: use rtuoverudp:// for modbus RTU over UDP
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
// error out if client creation failed
|
||||||
|
}
|
||||||
|
|
||||||
|
// now that the client is created and configured, attempt to connect
|
||||||
|
err = client.Open()
|
||||||
|
if err != nil {
|
||||||
|
// error out if we failed to connect/open the device
|
||||||
|
// note: multiple Open() attempts can be made on the same client until
|
||||||
|
// the connection succeeds (i.e. err == nil), calling the constructor again
|
||||||
|
// is unnecessary.
|
||||||
|
// likewise, a client can be opened and closed as many times as needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// read a single 16-bit holding register at address 100
|
||||||
|
var reg16 uint16
|
||||||
|
reg16, err = client.ReadRegister(100, modbus.HOLDING_REGISTER)
|
||||||
|
if err != nil {
|
||||||
|
// error out
|
||||||
|
} else {
|
||||||
|
// use value
|
||||||
|
fmt.Printf("value: %v", reg16) // as unsigned integer
|
||||||
|
fmt.Printf("value: %v", int16(reg16)) // as signed integer
|
||||||
|
}
|
||||||
|
|
||||||
|
// read 4 consecutive 16-bit input registers starting at address 100
|
||||||
|
var reg16s []uint16
|
||||||
|
reg16s, err = client.ReadRegisters(100, 4, modbus.INPUT_REGISTER)
|
||||||
|
|
||||||
|
// read the same 4 consecutive 16-bit input registers as 2 32-bit integers
|
||||||
|
var reg32s []uint32
|
||||||
|
reg32s, err = client.ReadUint32s(100, 2, modbus.INPUT_REGISTER)
|
||||||
|
|
||||||
|
// read the same 4 consecutive 16-bit registers as a single 64-bit integer
|
||||||
|
var reg64 uint64
|
||||||
|
reg64, err = client.ReadUint64(100, modbus.INPUT_REGISTER)
|
||||||
|
|
||||||
|
// read the same 4 consecutive 16-bit registers as a slice of bytes
|
||||||
|
var regBs []byte
|
||||||
|
regBs, err = client.ReadBytes(100, 8, modbus.INPUT_REGISTER)
|
||||||
|
|
||||||
|
// by default, 16-bit integers are decoded as big-endian and 32/64-bit values as
|
||||||
|
// big-endian with the high word first.
|
||||||
|
// change the byte/word ordering of subsequent requests to little endian, with
|
||||||
|
// the low word first (note that the second argument only affects 32/64-bit values)
|
||||||
|
client.SetEncoding(modbus.LITTLE_ENDIAN, modbus.LOW_WORD_FIRST)
|
||||||
|
|
||||||
|
// read the same 4 consecutive 16-bit input registers as 2 32-bit floats
|
||||||
|
var fl32s []float32
|
||||||
|
fl32s, err = client.ReadFloat32s(100, 2, modbus.INPUT_REGISTER)
|
||||||
|
|
||||||
|
// write -200 to 16-bit (holding) register 100, as a signed integer
|
||||||
|
var s int16 = -200
|
||||||
|
err = client.WriteRegister(100, uint16(s))
|
||||||
|
|
||||||
|
// Switch to unit ID (a.k.a. slave ID) #4
|
||||||
|
client.SetUnitId(4)
|
||||||
|
|
||||||
|
// write 3 floats to registers 100 to 105
|
||||||
|
err = client.WriteFloat32s(100, []float32{
|
||||||
|
3.14,
|
||||||
|
1.1,
|
||||||
|
-783.22,
|
||||||
|
})
|
||||||
|
|
||||||
|
// write 0x0102030405060708 to 16-bit (holding) registers 10 through 13
|
||||||
|
// (8 bytes i.e. 4 consecutive modbus registers)
|
||||||
|
err = client.WriteBytes(10, []byte{
|
||||||
|
0x01, 0x02, 0x03, 0x04,
|
||||||
|
0x05, 0x06, 0x07, 0x08,
|
||||||
|
})
|
||||||
|
|
||||||
|
// close the TCP connection/serial port
|
||||||
|
client.Close()
|
||||||
|
}
|
||||||
|
```
|
||||||
|
### Using the server component
|
||||||
|
See:
|
||||||
|
* [examples/tcp_server.go](examples/tcp_server.go) for a modbus TCP example
|
||||||
|
* [examples/tls_server.go](examples/tls_server.go) for TLS and Modbus Security features
|
||||||
|
|
||||||
|
### Supported function codes, golang object types and endianness/word ordering
|
||||||
|
Function codes:
|
||||||
|
* Read coils (0x01)
|
||||||
|
* Read discrete inputs (0x02)
|
||||||
|
* Read holding registers (0x03)
|
||||||
|
* Read input registers (0x04)
|
||||||
|
* Write single coil (0x05)
|
||||||
|
* Write single register (0x06)
|
||||||
|
* Write multiple coils (0x0f)
|
||||||
|
* Write multiple registers (0x10)
|
||||||
|
|
||||||
|
Go object types:
|
||||||
|
* Booleans (coils and discrete inputs)
|
||||||
|
* Bytes (input and holding registers)
|
||||||
|
* Signed/Unisgned 16-bit integers (input and holding registers)
|
||||||
|
* Signed/Unsigned 32-bit integers (input and holding registers)
|
||||||
|
* 32-bit floating point numbers (input and holding registers)
|
||||||
|
* Signed/Unsigned 64-bit integers (input and holding registers)
|
||||||
|
* 64-bit floating point numbers (input and holding registers)
|
||||||
|
|
||||||
|
Byte encoding/endianness/word ordering:
|
||||||
|
* Little and Big endian for byte slices and 16-bit integers
|
||||||
|
* Little and Big endian, with and without word swap for 32 and 64-bit
|
||||||
|
integers and floating point numbers.
|
||||||
|
|
||||||
|
### Logging ###
|
||||||
|
Both client and server objects will log to stdout by default.
|
||||||
|
This behavior can be overriden by passing a log.Logger object
|
||||||
|
through the Logger property of ClientConfiguration/ServerConfiguration.
|
||||||
|
|
||||||
|
### TODO (in no particular order)
|
||||||
|
* Add RTU (serial) support to the server
|
||||||
|
* Add more tests
|
||||||
|
* Add diagnostics register support
|
||||||
|
* Add fifo register support
|
||||||
|
* Add file register support
|
||||||
|
|
||||||
|
### Dependencies
|
||||||
|
* [github.com/goburrow/serial](https://github.com/goburrow/serial) for access to the serial port (thanks!)
|
||||||
|
|
||||||
|
### License
|
||||||
|
MIT.
|
||||||
|
|||||||
584
client_tls_test.go
Normal file
584
client_tls_test.go
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
package modbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"io"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// note: these certs and associated keys are self-signed
|
||||||
|
// and only meant to be used with this test.
|
||||||
|
// PLEASE DO NOT USE THEM FOR ANYTHING ELSE, EVER, as they
|
||||||
|
// do not provide any kind of security.
|
||||||
|
serverCert string = `-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----`
|
||||||
|
|
||||||
|
serverKey string = `-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
yAqUh6zMzzcee67KOWWZMTuPQuu1n/m1
|
||||||
|
-----END PRIVATE KEY-----`
|
||||||
|
|
||||||
|
clientCert string = `
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
`
|
||||||
|
clientKey string = `-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
iaKCVjivzjP1s/q6adzOOZVlVwm7Xw==
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestTCPOVerTLSClient tests the TLS layer of the modbus client.
|
||||||
|
func TestTCPoverTLSClient(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
var client *ModbusClient
|
||||||
|
var serverKeyPair tls.Certificate
|
||||||
|
var clientKeyPair tls.Certificate
|
||||||
|
var clientCp *x509.CertPool
|
||||||
|
var serverCp *x509.CertPool
|
||||||
|
var serverHostPort string
|
||||||
|
var serverChan chan string
|
||||||
|
var regs []uint16
|
||||||
|
|
||||||
|
serverChan = make(chan string)
|
||||||
|
|
||||||
|
// load server and client keypairs
|
||||||
|
serverKeyPair, err = tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load test server key pair: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientKeyPair, err = tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load test client key pair: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// start with an empty client cert pool initially to reject the server
|
||||||
|
// certificate
|
||||||
|
clientCp = x509.NewCertPool()
|
||||||
|
|
||||||
|
// start with an empty server cert pool initially to reject the client
|
||||||
|
// certificate
|
||||||
|
serverCp = x509.NewCertPool()
|
||||||
|
|
||||||
|
// start a mock modbus TLS server
|
||||||
|
go runMockTLSServer(t, serverKeyPair, serverCp, serverChan)
|
||||||
|
|
||||||
|
// wait for the test server goroutine to signal its readiness
|
||||||
|
// and network location
|
||||||
|
serverHostPort = <-serverChan
|
||||||
|
|
||||||
|
// attempt to create a client without specifying any TLS configuration
|
||||||
|
// parameter: should fail
|
||||||
|
client, err = NewClient(&ClientConfiguration{
|
||||||
|
URL: fmt.Sprintf("tcp+tls://%s", serverHostPort),
|
||||||
|
})
|
||||||
|
if err != ErrConfigurationError {
|
||||||
|
t.Errorf("NewClient() should have failed with %v, got: %v",
|
||||||
|
ErrConfigurationError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to create a client without specifying any TLS server
|
||||||
|
// cert/CA: should fail
|
||||||
|
client, err = NewClient(&ClientConfiguration{
|
||||||
|
URL: fmt.Sprintf("tcp+tls://%s", serverHostPort),
|
||||||
|
TLSClientCert: &clientKeyPair,
|
||||||
|
})
|
||||||
|
if err != ErrConfigurationError {
|
||||||
|
t.Errorf("NewClient() should have failed with %v, got: %v",
|
||||||
|
ErrConfigurationError, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to create a client with both client cert+key and server
|
||||||
|
// cert/CA: should succeed
|
||||||
|
client, err = NewClient(&ClientConfiguration{
|
||||||
|
URL: fmt.Sprintf("tcp+tls://%s", serverHostPort),
|
||||||
|
TLSClientCert: &clientKeyPair,
|
||||||
|
TLSRootCAs: clientCp,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("NewClient() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to the server: should fail with a TLS error as the server cert
|
||||||
|
// is not yet trusted by the client
|
||||||
|
err = client.Open()
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Open() should have failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// now load the server certificate into the client's trusted cert pool
|
||||||
|
// to get the client to accept the server's certificate
|
||||||
|
if !clientCp.AppendCertsFromPEM([]byte(serverCert)) {
|
||||||
|
t.Errorf("failed to load test server cert into cert pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to the server: should succeed
|
||||||
|
// note: client certificates are verified after the handshake procedure
|
||||||
|
// has completed, so Open() won't fail even though the client cert
|
||||||
|
// is rejected by the server.
|
||||||
|
// (see RFC 8446 section 4.6.2 Post Handshake Authentication)
|
||||||
|
err = client.Open()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Open() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to read two registers: since the client cert won't pass
|
||||||
|
// the validation step yet (no cert in server cert pool),
|
||||||
|
// expect a tls error
|
||||||
|
regs, err = client.ReadRegisters(0x1000, 2, INPUT_REGISTER)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ReadRegisters() should have failed")
|
||||||
|
}
|
||||||
|
client.Close()
|
||||||
|
|
||||||
|
// now place the client cert in the server's authorized client list
|
||||||
|
// to get the client cert past the validation procedure
|
||||||
|
if !serverCp.AppendCertsFromPEM([]byte(clientCert)) {
|
||||||
|
t.Errorf("failed to load test client cert into cert pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to the server: should succeed
|
||||||
|
err = client.Open()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Open() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to read two registers: should succeed
|
||||||
|
regs, err = client.ReadRegisters(0x1000, 2, INPUT_REGISTER)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ReadRegisters() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
if regs[0] != 0x1234 {
|
||||||
|
t.Errorf("expected 0x1234 in 1st reg, saw: 0x%04x", regs[0])
|
||||||
|
}
|
||||||
|
if regs[1] != 0x5678 {
|
||||||
|
t.Errorf("expected 0x5678 in 2nd reg, saw: 0x%04x", regs[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to read another: should succeed
|
||||||
|
regs, err = client.ReadRegisters(0x1002, 1, HOLDING_REGISTER)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ReadRegisters() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
if regs[0] != 0xaabb {
|
||||||
|
t.Errorf("expected 0xaabb in 1st reg, saw: 0x%04x", regs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
// close the connection: should succeed
|
||||||
|
err = client.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Close() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTLSClientOnServerTimeout(t *testing.T) {
|
||||||
|
var err error
|
||||||
|
var client *ModbusClient
|
||||||
|
var server *ModbusServer
|
||||||
|
var serverKeyPair tls.Certificate
|
||||||
|
var clientKeyPair tls.Certificate
|
||||||
|
var clientCp *x509.CertPool
|
||||||
|
var serverCp *x509.CertPool
|
||||||
|
var th *tlsTestHandler
|
||||||
|
var reg uint16
|
||||||
|
|
||||||
|
th = &tlsTestHandler{}
|
||||||
|
// load server and client keypairs
|
||||||
|
serverKeyPair, err = tls.X509KeyPair([]byte(serverCert), []byte(serverKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load test server key pair: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientKeyPair, err = tls.X509KeyPair([]byte(clientCert), []byte(clientKey))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load test client key pair: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// add those keypairs to their corresponding cert pool
|
||||||
|
clientCp = x509.NewCertPool()
|
||||||
|
if !clientCp.AppendCertsFromPEM([]byte(serverCert)) {
|
||||||
|
t.Errorf("failed to load test server cert into cert pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCp = x509.NewCertPool()
|
||||||
|
if !serverCp.AppendCertsFromPEM([]byte(clientCert)) {
|
||||||
|
t.Errorf("failed to load client cert into cert pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// load the server cert into the client CA cert pool to get the server cert
|
||||||
|
// accepted by clients
|
||||||
|
clientCp = x509.NewCertPool()
|
||||||
|
if !clientCp.AppendCertsFromPEM([]byte(serverCert)) {
|
||||||
|
t.Errorf("failed to load test server cert into cert pool")
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err = NewServer(&ServerConfiguration{
|
||||||
|
URL: "tcp+tls://[::1]:5802",
|
||||||
|
MaxClients: 10,
|
||||||
|
TLSServerCert: &serverKeyPair,
|
||||||
|
TLSClientCAs: serverCp,
|
||||||
|
// disconnect idle clients after 500ms
|
||||||
|
Timeout: 500 * time.Millisecond,
|
||||||
|
}, th)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = server.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to start server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// create the modbus client
|
||||||
|
client, err = NewClient(&ClientConfiguration{
|
||||||
|
URL: "tcp+tls://localhost:5802",
|
||||||
|
TLSClientCert: &clientKeyPair,
|
||||||
|
TLSRootCAs: clientCp,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// connect to the server: should succeed
|
||||||
|
err = client.Open()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Open() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// write a value to register #3: should succeed
|
||||||
|
err = client.WriteRegister(3, 0x0199)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Write() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to read the value back: should succeed
|
||||||
|
reg, err = client.ReadRegister(3, HOLDING_REGISTER)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("ReadRegisters() should have succeeded, got: %v", err)
|
||||||
|
}
|
||||||
|
if reg != 0x0199 {
|
||||||
|
t.Errorf("expected 0x0199 in reg #3, saw: 0x%04x", reg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// pause for longer than the server's configured timeout to end up with
|
||||||
|
// an open client with a closed underlying TCP socket
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// attempt a read: should fail
|
||||||
|
_, err = client.ReadRegister(3, INPUT_REGISTER)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("ReadRegister() should have failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup
|
||||||
|
client.Close()
|
||||||
|
server.Stop()
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// runMockTLSServer spins a test TLS server for use with TestTCPoverTLSClient.
|
||||||
|
func runMockTLSServer(t *testing.T, serverKeyPair tls.Certificate,
|
||||||
|
serverCp *x509.CertPool, serverChan chan string) {
|
||||||
|
var err error
|
||||||
|
var listener net.Listener
|
||||||
|
var sock net.Conn
|
||||||
|
var reqCount uint
|
||||||
|
var clientCount uint
|
||||||
|
var buf []byte
|
||||||
|
|
||||||
|
// let the OS pick an available port on the loopback interface
|
||||||
|
listener, err = tls.Listen("tcp", "localhost:0", &tls.Config{
|
||||||
|
// the server will use serverKeyPair (key+cert) to
|
||||||
|
// authenticate to the client
|
||||||
|
Certificates: []tls.Certificate{serverKeyPair},
|
||||||
|
// the server will use the certpool to authenticate the
|
||||||
|
// client-side cert
|
||||||
|
ClientCAs: serverCp,
|
||||||
|
// request client-side authentication and client cert validation
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to start test server listener: %v", err)
|
||||||
|
}
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
// let the main test goroutine know which port the OS picked
|
||||||
|
serverChan <- listener.Addr().String()
|
||||||
|
|
||||||
|
for err == nil {
|
||||||
|
// accept client connections
|
||||||
|
sock, err = listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to accept client conn: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// only proceed with clients passing the tls handshake
|
||||||
|
// note: this will reject any client whose cert does not pass the
|
||||||
|
// verification step
|
||||||
|
err = sock.(*tls.Conn).Handshake()
|
||||||
|
if err != nil {
|
||||||
|
sock.Close()
|
||||||
|
err = nil
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
clientCount++
|
||||||
|
if clientCount > 2 {
|
||||||
|
t.Errorf("expected 2 client conns, saw: %v", clientCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// expect MBAP (modbus/tcp) messages inside the TLS tunnel
|
||||||
|
for {
|
||||||
|
// expect 12 bytes per request
|
||||||
|
buf = make([]byte, 12)
|
||||||
|
|
||||||
|
_, err = sock.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
// ignore EOF errors (clients disconnecting)
|
||||||
|
if err != io.EOF {
|
||||||
|
t.Errorf("failed to read client request: %v", err)
|
||||||
|
}
|
||||||
|
sock.Close()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
reqCount++
|
||||||
|
switch reqCount {
|
||||||
|
case 1:
|
||||||
|
for i, b := range []byte{
|
||||||
|
0x00, 0x01, // txn id
|
||||||
|
0x00, 0x00, // protocol id
|
||||||
|
0x00, 0x06, // length
|
||||||
|
0x01, 0x04, // unit id + function code
|
||||||
|
0x10, 0x00, // start address
|
||||||
|
0x00, 0x02, // quantity
|
||||||
|
} {
|
||||||
|
if b != buf[i] {
|
||||||
|
t.Errorf("expected 0x%02x at pos %v, saw 0x%02x",
|
||||||
|
b, i, buf[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a reply
|
||||||
|
_, err = sock.Write([]byte{
|
||||||
|
0x00, 0x01, // txn id
|
||||||
|
0x00, 0x00, // protocol id
|
||||||
|
0x00, 0x07, // length
|
||||||
|
0x01, 0x04, // unit id + function code
|
||||||
|
0x04, // byte count
|
||||||
|
0x12, 0x34, // reg #0
|
||||||
|
0x56, 0x78, // reg #1
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to write reply: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
for i, b := range []byte{
|
||||||
|
0x00, 0x02, // txn id
|
||||||
|
0x00, 0x00, // protocol id
|
||||||
|
0x00, 0x06, // length
|
||||||
|
0x01, 0x03, // unit id + function code
|
||||||
|
0x10, 0x02, // start address
|
||||||
|
0x00, 0x01, // quantity
|
||||||
|
} {
|
||||||
|
if b != buf[i] {
|
||||||
|
t.Errorf("expected 0x%02x at pos %v, saw 0x%02x",
|
||||||
|
b, i, buf[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send a reply
|
||||||
|
_, err = sock.Write([]byte{
|
||||||
|
0x00, 0x02, // txn id
|
||||||
|
0x00, 0x00, // protocol id
|
||||||
|
0x00, 0x05, // length
|
||||||
|
0x01, 0x03, // unit id + function code
|
||||||
|
0x02, // byte count
|
||||||
|
0xaa, 0xbb, // reg #0
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to write reply: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop the server after the 2nd request
|
||||||
|
listener.Close()
|
||||||
|
|
||||||
|
default:
|
||||||
|
t.Errorf("unexpected request id %v", reqCount)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1288
cmd/modbus-cli.go
Normal file
1288
cmd/modbus-cli.go
Normal file
File diff suppressed because it is too large
Load Diff
73
crc.go
Normal file
73
crc.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package modbus
|
||||||
|
|
||||||
|
var crcTable [256]uint16 = [256]uint16{
|
||||||
|
0x0000, 0xc0c1, 0xc181, 0x0140, 0xc301, 0x03c0, 0x0280, 0xc241,
|
||||||
|
0xc601, 0x06c0, 0x0780, 0xc741, 0x0500, 0xc5c1, 0xc481, 0x0440,
|
||||||
|
0xcc01, 0x0cc0, 0x0d80, 0xcd41, 0x0f00, 0xcfc1, 0xce81, 0x0e40,
|
||||||
|
0x0a00, 0xcac1, 0xcb81, 0x0b40, 0xc901, 0x09c0, 0x0880, 0xc841,
|
||||||
|
0xd801, 0x18c0, 0x1980, 0xd941, 0x1b00, 0xdbc1, 0xda81, 0x1a40,
|
||||||
|
0x1e00, 0xdec1, 0xdf81, 0x1f40, 0xdd01, 0x1dc0, 0x1c80, 0xdc41,
|
||||||
|
0x1400, 0xd4c1, 0xd581, 0x1540, 0xd701, 0x17c0, 0x1680, 0xd641,
|
||||||
|
0xd201, 0x12c0, 0x1380, 0xd341, 0x1100, 0xd1c1, 0xd081, 0x1040,
|
||||||
|
0xf001, 0x30c0, 0x3180, 0xf141, 0x3300, 0xf3c1, 0xf281, 0x3240,
|
||||||
|
0x3600, 0xf6c1, 0xf781, 0x3740, 0xf501, 0x35c0, 0x3480, 0xf441,
|
||||||
|
0x3c00, 0xfcc1, 0xfd81, 0x3d40, 0xff01, 0x3fc0, 0x3e80, 0xfe41,
|
||||||
|
0xfa01, 0x3ac0, 0x3b80, 0xfb41, 0x3900, 0xf9c1, 0xf881, 0x3840,
|
||||||
|
0x2800, 0xe8c1, 0xe981, 0x2940, 0xeb01, 0x2bc0, 0x2a80, 0xea41,
|
||||||
|
0xee01, 0x2ec0, 0x2f80, 0xef41, 0x2d00, 0xedc1, 0xec81, 0x2c40,
|
||||||
|
0xe401, 0x24c0, 0x2580, 0xe541, 0x2700, 0xe7c1, 0xe681, 0x2640,
|
||||||
|
0x2200, 0xe2c1, 0xe381, 0x2340, 0xe101, 0x21c0, 0x2080, 0xe041,
|
||||||
|
0xa001, 0x60c0, 0x6180, 0xa141, 0x6300, 0xa3c1, 0xa281, 0x6240,
|
||||||
|
0x6600, 0xa6c1, 0xa781, 0x6740, 0xa501, 0x65c0, 0x6480, 0xa441,
|
||||||
|
0x6c00, 0xacc1, 0xad81, 0x6d40, 0xaf01, 0x6fc0, 0x6e80, 0xae41,
|
||||||
|
0xaa01, 0x6ac0, 0x6b80, 0xab41, 0x6900, 0xa9c1, 0xa881, 0x6840,
|
||||||
|
0x7800, 0xb8c1, 0xb981, 0x7940, 0xbb01, 0x7bc0, 0x7a80, 0xba41,
|
||||||
|
0xbe01, 0x7ec0, 0x7f80, 0xbf41, 0x7d00, 0xbdc1, 0xbc81, 0x7c40,
|
||||||
|
0xb401, 0x74c0, 0x7580, 0xb541, 0x7700, 0xb7c1, 0xb681, 0x7640,
|
||||||
|
0x7200, 0xb2c1, 0xb381, 0x7340, 0xb101, 0x71c0, 0x7080, 0xb041,
|
||||||
|
0x5000, 0x90c1, 0x9181, 0x5140, 0x9301, 0x53c0, 0x5280, 0x9241,
|
||||||
|
0x9601, 0x56c0, 0x5780, 0x9741, 0x5500, 0x95c1, 0x9481, 0x5440,
|
||||||
|
0x9c01, 0x5cc0, 0x5d80, 0x9d41, 0x5f00, 0x9fc1, 0x9e81, 0x5e40,
|
||||||
|
0x5a00, 0x9ac1, 0x9b81, 0x5b40, 0x9901, 0x59c0, 0x5880, 0x9841,
|
||||||
|
0x8801, 0x48c0, 0x4980, 0x8941, 0x4b00, 0x8bc1, 0x8a81, 0x4a40,
|
||||||
|
0x4e00, 0x8ec1, 0x8f81, 0x4f40, 0x8d01, 0x4dc0, 0x4c80, 0x8c41,
|
||||||
|
0x4400, 0x84c1, 0x8581, 0x4540, 0x8701, 0x47c0, 0x4680, 0x8641,
|
||||||
|
0x8201, 0x42c0, 0x4380, 0x8341, 0x4100, 0x81c1, 0x8081, 0x4040,
|
||||||
|
}
|
||||||
|
|
||||||
|
type crc struct {
|
||||||
|
crc uint16
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepares the CRC generator for use.
|
||||||
|
func (c *crc) init() {
|
||||||
|
c.crc = 0xffff
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adds the given bytes to the CRC.
|
||||||
|
func (c *crc) add(in []byte) {
|
||||||
|
var index byte
|
||||||
|
|
||||||
|
for _, b := range in {
|
||||||
|
index = b ^ byte(c.crc & 0xff)
|
||||||
|
c.crc >>= 8
|
||||||
|
c.crc ^= crcTable[index]
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the CRC as two bytes, swapped.
|
||||||
|
func (c *crc) value() (value []byte) {
|
||||||
|
value = uint16ToBytes(LITTLE_ENDIAN, c.crc)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *crc) isEqual(low byte, high byte) (yes bool) {
|
||||||
|
yes = (bytesToUint16(LITTLE_ENDIAN, []byte{low, high}) == c.crc)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
106
crc_test.go
Normal file
106
crc_test.go
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
package modbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCRC(t *testing.T) {
|
||||||
|
var c crc
|
||||||
|
var out []byte
|
||||||
|
|
||||||
|
// initialize the CRC object and make sure we get 0xffff as init value
|
||||||
|
c.init()
|
||||||
|
if c.crc != 0xffff {
|
||||||
|
t.Errorf("expected 0xffff, saw 0x%04x", c.crc)
|
||||||
|
}
|
||||||
|
|
||||||
|
out = c.value()
|
||||||
|
if len(out) != 2 {
|
||||||
|
t.Errorf("value() should have returned 2 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0xff || out[1] != 0xff {
|
||||||
|
t.Errorf("expected {0xff, 0xff} got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// add a few bytes, check the output
|
||||||
|
c.add([]byte{0x01, 0x02, 0x03, 0x04, 0x05})
|
||||||
|
if c.crc != 0xbb2a {
|
||||||
|
t.Errorf("expected 0xbb2a, saw 0x%04x", c.crc)
|
||||||
|
}
|
||||||
|
|
||||||
|
out = c.value()
|
||||||
|
if len(out) != 2 {
|
||||||
|
t.Errorf("value() should have returned 2 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x2a || out[1] != 0xbb {
|
||||||
|
t.Errorf("expected {0x2a, 0xbb} got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// add one extra byte, test the output again
|
||||||
|
c.add([]byte{0x06})
|
||||||
|
if c.crc != 0xddba {
|
||||||
|
t.Errorf("expected 0xddba, saw 0x%04x", c.crc)
|
||||||
|
}
|
||||||
|
|
||||||
|
out = c.value()
|
||||||
|
if len(out) != 2 {
|
||||||
|
t.Errorf("value() should have returned 2 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0xba || out[1] != 0xdd {
|
||||||
|
t.Errorf("expected {0xba, 0xdd} got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
// init the CRC once again: the output bytes should be back to 0xffff
|
||||||
|
c.init()
|
||||||
|
if c.crc != 0xffff {
|
||||||
|
t.Errorf("expected 0xffff, saw 0x%04x", c.crc)
|
||||||
|
}
|
||||||
|
|
||||||
|
out = c.value()
|
||||||
|
if len(out) != 2 {
|
||||||
|
t.Errorf("value() should have returned 2 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0xff || out[1] != 0xff {
|
||||||
|
t.Errorf("expected {0xff, 0xff} got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCRCIsEqual(t *testing.T) {
|
||||||
|
var c crc
|
||||||
|
var out []byte
|
||||||
|
|
||||||
|
// initialize the CRC object and feed it a few bytes
|
||||||
|
c.init()
|
||||||
|
c.add([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
|
||||||
|
|
||||||
|
// make sure the register value is what it should be
|
||||||
|
if c.crc != 0xddba {
|
||||||
|
t.Errorf("expected 0xddba, saw 0x%04x", c.crc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// positive test
|
||||||
|
if !c.isEqual(0xba, 0xdd) {
|
||||||
|
t.Error("isEqual() should have returned true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// negative test
|
||||||
|
if c.isEqual(0xdd, 0xba) {
|
||||||
|
t.Error("isEqual() should have returned false")
|
||||||
|
}
|
||||||
|
|
||||||
|
// loopback test
|
||||||
|
out = c.value()
|
||||||
|
if !c.isEqual(out[0], out[1]) {
|
||||||
|
t.Error("isEqual() should have returned true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// an empty payload should have a CRC of 0xffff
|
||||||
|
c.init()
|
||||||
|
if !c.isEqual(0xff, 0xff) {
|
||||||
|
t.Error("isEqual() should have returned true")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
209
encoding.go
Normal file
209
encoding.go
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
package modbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
func uint16ToBytes(endianness Endianness, in uint16) (out []byte) {
|
||||||
|
out = make([]byte, 2)
|
||||||
|
switch endianness {
|
||||||
|
case BIG_ENDIAN: binary.BigEndian.PutUint16(out, in)
|
||||||
|
case LITTLE_ENDIAN: binary.LittleEndian.PutUint16(out, in)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint16sToBytes(endianness Endianness, in []uint16) (out []byte) {
|
||||||
|
for i := range in {
|
||||||
|
out = append(out, uint16ToBytes(endianness, in[i])...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToUint16(endianness Endianness, in []byte) (out uint16) {
|
||||||
|
switch endianness {
|
||||||
|
case BIG_ENDIAN: out = binary.BigEndian.Uint16(in)
|
||||||
|
case LITTLE_ENDIAN: out = binary.LittleEndian.Uint16(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToUint16s(endianness Endianness, in []byte) (out []uint16) {
|
||||||
|
for i := 0; i < len(in); i += 2 {
|
||||||
|
out = append(out, bytesToUint16(endianness, in[i:i+2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToUint32s(endianness Endianness, wordOrder WordOrder, in []byte) (out []uint32) {
|
||||||
|
var u32 uint32
|
||||||
|
|
||||||
|
for i := 0; i < len(in); i += 4 {
|
||||||
|
switch endianness {
|
||||||
|
case BIG_ENDIAN:
|
||||||
|
if wordOrder == HIGH_WORD_FIRST {
|
||||||
|
u32 = binary.BigEndian.Uint32(in[i:i+4])
|
||||||
|
} else {
|
||||||
|
u32 = binary.BigEndian.Uint32(
|
||||||
|
[]byte{in[i+2], in[i+3], in[i+0], in[i+1]})
|
||||||
|
}
|
||||||
|
case LITTLE_ENDIAN:
|
||||||
|
if wordOrder == LOW_WORD_FIRST {
|
||||||
|
u32 = binary.LittleEndian.Uint32(in[i:i+4])
|
||||||
|
} else {
|
||||||
|
u32 = binary.LittleEndian.Uint32(
|
||||||
|
[]byte{in[i+2], in[i+3], in[i+0], in[i+1]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, u32)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint32ToBytes(endianness Endianness, wordOrder WordOrder, in uint32) (out []byte) {
|
||||||
|
out = make([]byte, 4)
|
||||||
|
|
||||||
|
switch endianness {
|
||||||
|
case BIG_ENDIAN:
|
||||||
|
binary.BigEndian.PutUint32(out, in)
|
||||||
|
|
||||||
|
// swap words if needed
|
||||||
|
if wordOrder == LOW_WORD_FIRST {
|
||||||
|
out[0], out[1], out[2], out[3] = out[2], out[3], out[0], out[1]
|
||||||
|
}
|
||||||
|
case LITTLE_ENDIAN:
|
||||||
|
binary.LittleEndian.PutUint32(out, in)
|
||||||
|
|
||||||
|
// swap words if needed
|
||||||
|
if wordOrder == HIGH_WORD_FIRST {
|
||||||
|
out[0], out[1], out[2], out[3] = out[2], out[3], out[0], out[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToFloat32s(endianness Endianness, wordOrder WordOrder, in []byte) (out []float32) {
|
||||||
|
var u32s []uint32
|
||||||
|
|
||||||
|
u32s = bytesToUint32s(endianness, wordOrder, in)
|
||||||
|
|
||||||
|
for _, u32 := range u32s {
|
||||||
|
out = append(out, math.Float32frombits(u32))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func float32ToBytes(endianness Endianness, wordOrder WordOrder, in float32) (out []byte) {
|
||||||
|
out = uint32ToBytes(endianness, wordOrder, math.Float32bits(in))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToUint64s(endianness Endianness, wordOrder WordOrder, in []byte) (out []uint64) {
|
||||||
|
var u64 uint64
|
||||||
|
|
||||||
|
for i := 0; i < len(in); i += 8 {
|
||||||
|
switch endianness {
|
||||||
|
case BIG_ENDIAN:
|
||||||
|
if wordOrder == HIGH_WORD_FIRST {
|
||||||
|
u64 = binary.BigEndian.Uint64(in[i:i+8])
|
||||||
|
} else {
|
||||||
|
u64 = binary.BigEndian.Uint64(
|
||||||
|
[]byte{in[i+6], in[i+7], in[i+4], in[i+5],
|
||||||
|
in[i+2], in[i+3], in[i+0], in[i+1]})
|
||||||
|
}
|
||||||
|
case LITTLE_ENDIAN:
|
||||||
|
if wordOrder == LOW_WORD_FIRST {
|
||||||
|
u64 = binary.LittleEndian.Uint64(in[i:i+8])
|
||||||
|
} else {
|
||||||
|
u64 = binary.LittleEndian.Uint64(
|
||||||
|
[]byte{in[i+6], in[i+7], in[i+4], in[i+5],
|
||||||
|
in[i+2], in[i+3], in[i+0], in[i+1]})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out = append(out, u64)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func uint64ToBytes(endianness Endianness, wordOrder WordOrder, in uint64) (out []byte) {
|
||||||
|
out = make([]byte, 8)
|
||||||
|
|
||||||
|
switch endianness {
|
||||||
|
case BIG_ENDIAN:
|
||||||
|
binary.BigEndian.PutUint64(out, in)
|
||||||
|
|
||||||
|
// swap words if needed
|
||||||
|
if wordOrder == LOW_WORD_FIRST {
|
||||||
|
out[0], out[1], out[2], out[3],out[4], out[5], out[6], out[7] =
|
||||||
|
out[6], out[7], out[4], out[5], out[2], out[3], out[0], out[1]
|
||||||
|
}
|
||||||
|
case LITTLE_ENDIAN:
|
||||||
|
binary.LittleEndian.PutUint64(out, in)
|
||||||
|
|
||||||
|
// swap words if needed
|
||||||
|
if wordOrder == HIGH_WORD_FIRST {
|
||||||
|
out[0], out[1], out[2], out[3],out[4], out[5], out[6], out[7] =
|
||||||
|
out[6], out[7], out[4], out[5], out[2], out[3], out[0], out[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bytesToFloat64s(endianness Endianness, wordOrder WordOrder, in []byte) (out []float64) {
|
||||||
|
var u64s []uint64
|
||||||
|
|
||||||
|
u64s = bytesToUint64s(endianness, wordOrder, in)
|
||||||
|
|
||||||
|
for _, u64 := range u64s {
|
||||||
|
out = append(out, math.Float64frombits(u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func float64ToBytes(endianness Endianness, wordOrder WordOrder, in float64) (out []byte) {
|
||||||
|
out = uint64ToBytes(endianness, wordOrder, math.Float64bits(in))
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeBools(in []bool) (out []byte) {
|
||||||
|
var byteCount uint
|
||||||
|
var i uint
|
||||||
|
|
||||||
|
byteCount = uint(len(in)) / 8
|
||||||
|
if len(in) % 8 != 0 {
|
||||||
|
byteCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
out = make([]byte, byteCount)
|
||||||
|
for i = 0; i < uint(len(in)); i++ {
|
||||||
|
if in[i] {
|
||||||
|
out[i/8] |= (0x01 << (i % 8))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeBools(quantity uint16, in []byte) (out []bool) {
|
||||||
|
var i uint
|
||||||
|
for i = 0; i < uint(quantity); i++ {
|
||||||
|
out = append(out, (((in[i/8] >> (i % 8)) & 0x01) == 0x01))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
610
encoding_test.go
Normal file
610
encoding_test.go
Normal file
@@ -0,0 +1,610 @@
|
|||||||
|
package modbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUint16ToBytes(t *testing.T) {
|
||||||
|
var out []byte
|
||||||
|
|
||||||
|
out = uint16ToBytes(BIG_ENDIAN, 0x4321)
|
||||||
|
if len(out) != 2 {
|
||||||
|
t.Errorf("expected 2 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x43 || out[1] != 0x21 {
|
||||||
|
t.Errorf("expected {0x43, 0x21}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = uint16ToBytes(LITTLE_ENDIAN, 0x4321)
|
||||||
|
if len(out) != 2 {
|
||||||
|
t.Errorf("expected 2 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x21 || out[1] != 0x43 {
|
||||||
|
t.Errorf("expected {0x21, 0x43}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUint16sToBytes(t *testing.T) {
|
||||||
|
var out []byte
|
||||||
|
|
||||||
|
out = uint16sToBytes(BIG_ENDIAN, []uint16{0x4321, 0x8765, 0xcba9})
|
||||||
|
if len(out) != 6 {
|
||||||
|
t.Errorf("expected 6 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x43 || out[1] != 0x21 {
|
||||||
|
t.Errorf("expected {0x43, 0x21}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
if out[2] != 0x87 || out[3] != 0x65 {
|
||||||
|
t.Errorf("expected {0x87, 0x65}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
if out[4] != 0xcb || out[5] != 0xa9 {
|
||||||
|
t.Errorf("expected {0xcb, 0xa9}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = uint16sToBytes(LITTLE_ENDIAN, []uint16{0x4321, 0x8765, 0xcba9})
|
||||||
|
if len(out) != 6 {
|
||||||
|
t.Errorf("expected 6 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x21 || out[1] != 0x43 {
|
||||||
|
t.Errorf("expected {0x21, 0x43}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
if out[2] != 0x65 || out[3] != 0x87 {
|
||||||
|
t.Errorf("expected {0x65, 0x87}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
if out[4] != 0xa9 || out[5] != 0xcb {
|
||||||
|
t.Errorf("expected {0xa9, 0xcb}, got {0x%02x, 0x%02x}", out[0], out[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBytesToUint16(t *testing.T) {
|
||||||
|
var result uint16
|
||||||
|
|
||||||
|
result = bytesToUint16(BIG_ENDIAN, []byte{0x43, 0x21})
|
||||||
|
if result != 0x4321 {
|
||||||
|
t.Errorf("expected 0x4321, got 0x%04x", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
result = bytesToUint16(LITTLE_ENDIAN, []byte{0x43, 0x21})
|
||||||
|
if result != 0x2143 {
|
||||||
|
t.Errorf("expected 0x2143, got 0x%04x", result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBytesToUint16s(t *testing.T) {
|
||||||
|
var results []uint16
|
||||||
|
|
||||||
|
results = bytesToUint16s(BIG_ENDIAN, []byte{0x11, 0x22, 0x33, 0x44})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x1122 {
|
||||||
|
t.Errorf("expected 0x1122, got 0x%04x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x3344 {
|
||||||
|
t.Errorf("expected 0x3344, got 0x%04x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToUint16s(LITTLE_ENDIAN, []byte{0x11, 0x22, 0x33, 0x44})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x2211 {
|
||||||
|
t.Errorf("expected 0x2211, got 0x%04x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x4433 {
|
||||||
|
t.Errorf("expected 0x4433, got 0x%04x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUint32ToBytes(t *testing.T) {
|
||||||
|
var out []byte
|
||||||
|
|
||||||
|
out = uint32ToBytes(BIG_ENDIAN, HIGH_WORD_FIRST, 0x87654321)
|
||||||
|
if len(out) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x87 || out[1] != 0x65 || // first word
|
||||||
|
out[2] != 0x43 || out[3] != 0x21 { // second word
|
||||||
|
t.Errorf("expected {0x87, 0x65, 0x43, 0x21}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = uint32ToBytes(BIG_ENDIAN, LOW_WORD_FIRST, 0x87654321)
|
||||||
|
if len(out) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x43 || out[1] != 0x21 || out[2] != 0x87 || out[3] != 0x65 {
|
||||||
|
t.Errorf("expected {0x43, 0x21, 0x87, 0x65}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = uint32ToBytes(LITTLE_ENDIAN, LOW_WORD_FIRST, 0x87654321)
|
||||||
|
if len(out) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x21 || out[1] != 0x43 || out[2] != 0x65 || out[3] != 0x87 {
|
||||||
|
t.Errorf("expected {0x21, 0x43, 0x65, 0x87}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = uint32ToBytes(LITTLE_ENDIAN, HIGH_WORD_FIRST, 0x87654321)
|
||||||
|
if len(out) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x65 || out[1] != 0x87 || out[2] != 0x21 || out[3] != 0x43 {
|
||||||
|
t.Errorf("expected {0x65, 0x87, 0x21, 0x43}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBytesToUint32s(t *testing.T) {
|
||||||
|
var results []uint32
|
||||||
|
|
||||||
|
results = bytesToUint32s(BIG_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x00, 0x11, 0x22, 0x33,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x87654321 {
|
||||||
|
t.Errorf("expected 0x87654321, got 0x%08x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x00112233 {
|
||||||
|
t.Errorf("expected 0x00112233, got 0x%08x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToUint32s(BIG_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x00, 0x11, 0x22, 0x33,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x43218765 {
|
||||||
|
t.Errorf("expected 0x43218765, got 0x%08x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x22330011 {
|
||||||
|
t.Errorf("expected 0x22330011, got 0x%08x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToUint32s(LITTLE_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x00, 0x11, 0x22, 0x33,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x21436587 {
|
||||||
|
t.Errorf("expected 0x21436587, got 0x%08x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x33221100 {
|
||||||
|
t.Errorf("expected 0x33221100, got 0x%08x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToUint32s(LITTLE_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||||
|
0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x00, 0x11, 0x22, 0x33,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x65872143 {
|
||||||
|
t.Errorf("expected 0x65872143, got 0x%08x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x11003322 {
|
||||||
|
t.Errorf("expected 0x11003322, got 0x%08x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloat32ToBytes(t *testing.T) {
|
||||||
|
var out []byte
|
||||||
|
|
||||||
|
out = float32ToBytes(BIG_ENDIAN, HIGH_WORD_FIRST, 1.234)
|
||||||
|
if len(out) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x3f || out[1] != 0x9d || out[2] != 0xf3 || out[3] != 0xb6 {
|
||||||
|
t.Errorf("expected {0x3f, 0x9d, 0xf3, 0xb6}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = float32ToBytes(BIG_ENDIAN, LOW_WORD_FIRST, 1.234)
|
||||||
|
if len(out) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0xf3 || out[1] != 0xb6 || out[2] != 0x3f || out[3] != 0x9d {
|
||||||
|
t.Errorf("expected {0xf3, 0xb6, 0x3f, 0x9d}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = float32ToBytes(LITTLE_ENDIAN, LOW_WORD_FIRST, 1.234)
|
||||||
|
if len(out) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0xb6 || out[1] != 0xf3 || out[2] != 0x9d || out[3] != 0x3f {
|
||||||
|
t.Errorf("expected {0xb6, 0xf3, 0x9d, 0x3f}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = float32ToBytes(LITTLE_ENDIAN, HIGH_WORD_FIRST, 1.234)
|
||||||
|
if len(out) != 4 {
|
||||||
|
t.Errorf("expected 4 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x9d || out[1] != 0x3f || out[2] != 0xb6 || out[3] != 0xf3 {
|
||||||
|
t.Errorf("expected {0x9d, 0x3f, 0xb6, 0xf3}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBytesToFloat32s(t *testing.T) {
|
||||||
|
var results []float32
|
||||||
|
|
||||||
|
results = bytesToFloat32s(BIG_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||||
|
0x3f, 0x9d, 0xf3, 0xb6,
|
||||||
|
0x40, 0x49, 0x0f, 0xdb,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 1.234 {
|
||||||
|
t.Errorf("expected 1.234, got %.04f", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 3.14159274101 {
|
||||||
|
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToFloat32s(BIG_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||||
|
0xf3, 0xb6, 0x3f, 0x9d,
|
||||||
|
0x0f, 0xdb, 0x40, 0x49,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 1.234 {
|
||||||
|
t.Errorf("expected 1.234, got %.04f", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 3.14159274101 {
|
||||||
|
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToFloat32s(LITTLE_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||||
|
0xb6, 0xf3, 0x9d, 0x3f,
|
||||||
|
0xdb, 0x0f, 0x49, 0x40,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 1.234 {
|
||||||
|
t.Errorf("expected 1.234, got %.04f", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 3.14159274101 {
|
||||||
|
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToFloat32s(LITTLE_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||||
|
0x9d, 0x3f, 0xb6, 0xf3,
|
||||||
|
0x49, 0x40, 0xdb, 0x0f,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 1.234 {
|
||||||
|
t.Errorf("expected 1.234, got %.04f", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 3.14159274101 {
|
||||||
|
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUint64ToBytes(t *testing.T) {
|
||||||
|
var out []byte
|
||||||
|
|
||||||
|
out = uint64ToBytes(BIG_ENDIAN, HIGH_WORD_FIRST, 0x0fedcba987654321)
|
||||||
|
if len(out) != 8 {
|
||||||
|
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
if out[0] != 0x0f || out[1] != 0xed || // 1st word
|
||||||
|
out[2] != 0xcb || out[3] != 0xa9 || // 2nd word
|
||||||
|
out[4] != 0x87 || out[5] != 0x65 || // 3rd word
|
||||||
|
out[6] != 0x43 || out[7] != 0x21 { // 4th word
|
||||||
|
t.Errorf("expected {0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = uint64ToBytes(BIG_ENDIAN, LOW_WORD_FIRST, 0x0fedcba987654321)
|
||||||
|
if len(out) != 8 {
|
||||||
|
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x43 || out[1] != 0x21 || // 1st word
|
||||||
|
out[2] != 0x87 || out[3] != 0x65 || // 2nd word
|
||||||
|
out[4] != 0xcb || out[5] != 0xa9 || // 3rd word
|
||||||
|
out[6] != 0x0f || out[7] != 0xed { // 4th word
|
||||||
|
t.Errorf("expected {0x43, 0x21, 0x87, 0x65, 0xcb, 0xa9, 0x0f, 0xed}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = uint64ToBytes(LITTLE_ENDIAN, LOW_WORD_FIRST, 0x0fedcba987654321)
|
||||||
|
if len(out) != 8 {
|
||||||
|
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x21 || out[1] != 0x43 || // 1st word
|
||||||
|
out[2] != 0x65 || out[3] != 0x87 || // 2nd word
|
||||||
|
out[4] != 0xa9 || out[5] != 0xcb || // 3rd word
|
||||||
|
out[6] != 0xed || out[7] != 0x0f { // 4th word
|
||||||
|
t.Errorf("expected {0x21, 0x43, 0x65, 0x87, 0xa9, 0xcb, 0xed, 0x0f}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = uint64ToBytes(LITTLE_ENDIAN, HIGH_WORD_FIRST, 0x0fedcba987654321)
|
||||||
|
if len(out) != 8 {
|
||||||
|
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0xed || out[1] != 0x0f || // 1st word
|
||||||
|
out[2] != 0xa9 || out[3] != 0xcb || // 2nd word
|
||||||
|
out[4] != 0x65 || out[5] != 0x87 || // 3rd word
|
||||||
|
out[6] != 0x21 || out[7] != 0x43 { // 4th word
|
||||||
|
t.Errorf("expected {0xed, 0x0f, 0xa9, 0xcb, 0x65, 0x87, 0x21, 0x43}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBytesToUint64s(t *testing.T) {
|
||||||
|
var results []uint64
|
||||||
|
|
||||||
|
results = bytesToUint64s(BIG_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||||
|
0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x0fedcba987654321 {
|
||||||
|
t.Errorf("expected 0x0fedcba987654321, got 0x%016x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x0011223344556677 {
|
||||||
|
t.Errorf("expected 0x0011223344556677, got 0x%016x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToUint64s(BIG_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||||
|
0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[0] != 0x43218765cba90fed {
|
||||||
|
t.Errorf("expected 0x43218765cba90fed, got 0x%016x", results[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
if results[1] != 0x6677445522330011 {
|
||||||
|
t.Errorf("expected 0x6677445522330011, got 0x%016x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToUint64s(LITTLE_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||||
|
0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x21436587a9cbed0f {
|
||||||
|
t.Errorf("expected 0x21436587a9cbed0f, got 0x%016x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x7766554433221100 {
|
||||||
|
t.Errorf("expected 0x7766554433221100, got 0x%016x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToUint64s(LITTLE_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||||
|
0x0f, 0xed, 0xcb, 0xa9, 0x87, 0x65, 0x43, 0x21,
|
||||||
|
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0xed0fa9cb65872143 {
|
||||||
|
t.Errorf("expected 0xed0fa9cb65872143, got 0x%016x", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 0x1100332255447766 {
|
||||||
|
t.Errorf("expected 0x1100332255447766, got 0x%016x", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFloat64ToBytes(t *testing.T) {
|
||||||
|
var out []byte
|
||||||
|
|
||||||
|
out = float64ToBytes(BIG_ENDIAN, HIGH_WORD_FIRST, 1.2345678)
|
||||||
|
if len(out) != 8 {
|
||||||
|
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x3f || out[1] != 0xf3 || out[2] != 0xc0 || out[3] != 0xca ||
|
||||||
|
out[4] != 0x2a || out[5] != 0x5b || out[6] != 0x1d || out[7] != 0x5d {
|
||||||
|
t.Errorf("expected {0x3f, 0xf3, 0xc0, 0xca, 0x2a, 0x5b, 0x1d, 0x5d}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = float64ToBytes(BIG_ENDIAN, LOW_WORD_FIRST, 1.2345678)
|
||||||
|
if len(out) != 8 {
|
||||||
|
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0x1d || out[1] != 0x5d || out[2] != 0x2a || out[3] != 0x5b ||
|
||||||
|
out[4] != 0xc0 || out[5] != 0xca || out[6] != 0x3f || out[7] != 0xf3 {
|
||||||
|
t.Errorf("expected {0x1d, 0x5d, 0x2a, 0x5b, 0xc0, 0xca, 0x3f, 0xf3}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = float64ToBytes(LITTLE_ENDIAN, LOW_WORD_FIRST, 1.2345678)
|
||||||
|
if len(out) != 8 {
|
||||||
|
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
if out[0] != 0x5d || out[1] != 0x1d || out[2] != 0x5b || out[3] != 0x2a ||
|
||||||
|
out[4] != 0xca || out[5] != 0xc0 || out[6] != 0xf3 || out[7] != 0x3f {
|
||||||
|
t.Errorf("expected {0x5d, 0x1d, 0x5b, 0x2a, 0xca, 0xc0, 0xf3, 0x3f}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
out = float64ToBytes(LITTLE_ENDIAN, HIGH_WORD_FIRST, 1.2345678)
|
||||||
|
if len(out) != 8 {
|
||||||
|
t.Errorf("expected 8 bytes, got %v", len(out))
|
||||||
|
}
|
||||||
|
if out[0] != 0xf3 || out[1] != 0x3f || out[2] != 0xca || out[3] != 0xc0 ||
|
||||||
|
out[4] != 0x5b || out[5] != 0x2a || out[6] != 0x5d || out[7] != 0x1d {
|
||||||
|
t.Errorf("expected {0xf3, 0x3f, 0xca, 0xc0, 0x5b, 0x2a, 0x5d, 0x1d}, got {0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
out[0], out[1], out[2], out[3], out[4], out[5], out[6], out[7])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBytesToFloat64s(t *testing.T) {
|
||||||
|
var results []float64
|
||||||
|
|
||||||
|
results = bytesToFloat64s(BIG_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||||
|
0x3f, 0xf3, 0xc0, 0xca, 0x2a, 0x5b, 0x1d, 0x5d,
|
||||||
|
0x40, 0x09, 0x21, 0xfb, 0x5f, 0xff, 0xe9, 0x5e,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 1.2345678 {
|
||||||
|
t.Errorf("expected 1.2345678, got %.08f", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 3.14159274101 {
|
||||||
|
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToFloat64s(BIG_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||||
|
0x1d, 0x5d, 0x2a, 0x5b, 0xc0, 0xca, 0x3f, 0xf3,
|
||||||
|
0xe9, 0x5e, 0x5f, 0xff, 0x21, 0xfb, 0x40, 0x09,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 1.2345678 {
|
||||||
|
t.Errorf("expected 1.234, got %.08f", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 3.14159274101 {
|
||||||
|
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToFloat64s(LITTLE_ENDIAN, LOW_WORD_FIRST, []byte{
|
||||||
|
0x5d, 0x1d, 0x5b, 0x2a, 0xca, 0xc0, 0xf3, 0x3f,
|
||||||
|
0x5e, 0xe9, 0xff, 0x5f, 0xfb, 0x21, 0x09, 0x40,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 1.2345678 {
|
||||||
|
t.Errorf("expected 1.234, got %.08f", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 3.14159274101 {
|
||||||
|
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = bytesToFloat64s(LITTLE_ENDIAN, HIGH_WORD_FIRST, []byte{
|
||||||
|
0xf3, 0x3f, 0xca, 0xc0, 0x5b, 0x2a, 0x5d, 0x1d,
|
||||||
|
0x09, 0x40, 0xfb, 0x21, 0xff, 0x5f, 0x5e, 0xe9,
|
||||||
|
})
|
||||||
|
if len(results) != 2 {
|
||||||
|
t.Errorf("expected 2 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 1.2345678 {
|
||||||
|
t.Errorf("expected 1.234, got %.08f", results[0])
|
||||||
|
}
|
||||||
|
if results[1] != 3.14159274101 {
|
||||||
|
t.Errorf("expected 3.14159274101, got %.09f", results[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecodeBools(t *testing.T) {
|
||||||
|
var results []bool
|
||||||
|
|
||||||
|
results = decodeBools(1, []byte{0x01})
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Errorf("expected 1 value, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != true {
|
||||||
|
t.Errorf("expected true, got false")
|
||||||
|
}
|
||||||
|
|
||||||
|
results = decodeBools(1, []byte{0x0f})
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Errorf("expected 1 value, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != true {
|
||||||
|
t.Errorf("expected true, got false")
|
||||||
|
}
|
||||||
|
|
||||||
|
results = decodeBools(9, []byte{0x75, 0x03})
|
||||||
|
if len(results) != 9 {
|
||||||
|
t.Errorf("expected 9 values, got %v", len(results))
|
||||||
|
}
|
||||||
|
for i, b := range []bool{
|
||||||
|
true, false, true, false, // 0x05
|
||||||
|
true, true, true, false, // 0x07
|
||||||
|
true, } { // 0x01
|
||||||
|
if b != results[i] {
|
||||||
|
t.Errorf("expected %v at %v, got %v", b, i, results[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncodeBools(t *testing.T) {
|
||||||
|
var results []byte
|
||||||
|
|
||||||
|
results = encodeBools([]bool{false, true, false, true, })
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Errorf("expected 1 byte, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x0a {
|
||||||
|
t.Errorf("expected 0x0a, got 0x%02x", results[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = encodeBools([]bool{true, false, true, })
|
||||||
|
if len(results) != 1 {
|
||||||
|
t.Errorf("expected 1 byte, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x05 {
|
||||||
|
t.Errorf("expected 0x05, got 0x%02x", results[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
results = encodeBools([]bool{true, false, false, true, false, true, true, false,
|
||||||
|
true, true, true, false, true, true, true, false,
|
||||||
|
false, true})
|
||||||
|
if len(results) != 3 {
|
||||||
|
t.Errorf("expected 3 bytes, got %v", len(results))
|
||||||
|
}
|
||||||
|
if results[0] != 0x69 || results[1] != 0x77 || results[2] != 0x02 {
|
||||||
|
t.Errorf("expected {0x69, 0x77, 0x02}, got {0x%02x, 0x%02x, 0x%02x}",
|
||||||
|
results[0], results[1], results[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
340
examples/tcp_server.go
Normal file
340
examples/tcp_server.go
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/simonvetter/modbus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MINUS_ONE int16 = -1
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Simple modbus server example.
|
||||||
|
*
|
||||||
|
* This file is intended to be a demo of the modbus server.
|
||||||
|
* It shows how to create and start a server, as well as how
|
||||||
|
* to write a handler object.
|
||||||
|
* Feel free to use it as boilerplate for simple servers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// run this with go run examples/tcp_server.go
|
||||||
|
func main() {
|
||||||
|
var server *modbus.ModbusServer
|
||||||
|
var err error
|
||||||
|
var eh *exampleHandler
|
||||||
|
var ticker *time.Ticker
|
||||||
|
|
||||||
|
// create the handler object
|
||||||
|
eh = &exampleHandler{}
|
||||||
|
|
||||||
|
// create the server object
|
||||||
|
server, err = modbus.NewServer(&modbus.ServerConfiguration{
|
||||||
|
// listen on localhost port 5502
|
||||||
|
URL: "tcp://localhost:5502",
|
||||||
|
// close idle connections after 30s of inactivity
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
// accept 5 concurrent connections max.
|
||||||
|
MaxClients: 5,
|
||||||
|
}, eh)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to create server: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start accepting client connections
|
||||||
|
// note that Start() returns as soon as the server is started
|
||||||
|
err = server.Start()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("failed to start server: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// increment a 32-bit uptime counter every second.
|
||||||
|
// (this counter is exposed as input registers 200-201 for demo purposes)
|
||||||
|
ticker = time.NewTicker(1 * time.Second)
|
||||||
|
for {
|
||||||
|
<-ticker.C
|
||||||
|
|
||||||
|
// since the handler methods are called from multiple goroutines,
|
||||||
|
// use locking where appropriate to avoid concurrency issues.
|
||||||
|
eh.lock.Lock()
|
||||||
|
eh.uptime++
|
||||||
|
eh.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// never reached
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example handler object, passed to the NewServer() constructor above.
|
||||||
|
type exampleHandler struct {
|
||||||
|
// this lock is used to avoid concurrency issues between goroutines, as
|
||||||
|
// handler methods are called from different goroutines
|
||||||
|
// (1 goroutine per client)
|
||||||
|
lock sync.RWMutex
|
||||||
|
|
||||||
|
// simple uptime counter, incremented in the main() above and exposed
|
||||||
|
// as a 32-bit input register (2 consecutive 16-bit modbus registers).
|
||||||
|
uptime uint32
|
||||||
|
|
||||||
|
// these are here to hold client-provided (written) values, for both coils and
|
||||||
|
// holding registers
|
||||||
|
coils [100]bool
|
||||||
|
holdingReg1 uint16
|
||||||
|
holdingReg2 uint16
|
||||||
|
|
||||||
|
// this is a 16-bit signed integer
|
||||||
|
holdingReg3 int16
|
||||||
|
|
||||||
|
// this is a 32-bit unsigned integer
|
||||||
|
holdingReg4 uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coil handler method.
|
||||||
|
// This method gets called whenever a valid modbus request asking for a coil operation is
|
||||||
|
// received by the server.
|
||||||
|
// It exposes 100 read/writable coils at addresses 0-99, except address 80 which is
|
||||||
|
// read-only.
|
||||||
|
// (read them with ./modbus-cli --target tcp://localhost:5502 rc:0+99, write to register n
|
||||||
|
// with ./modbus-cli --target tcp://localhost:5502 wr:n:<true|false>)
|
||||||
|
func (eh *exampleHandler) HandleCoils(req *modbus.CoilsRequest) (res []bool, err error) {
|
||||||
|
if req.UnitId != 1 {
|
||||||
|
// only accept unit ID #1
|
||||||
|
// note: we're merely filtering here, but we could as well use the unit
|
||||||
|
// ID field to support multiple register maps in a single server.
|
||||||
|
err = modbus.ErrIllegalFunction
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// make sure that all registers covered by this request actually exist
|
||||||
|
if int(req.Addr) + int(req.Quantity) > len(eh.coils) {
|
||||||
|
err = modbus.ErrIllegalDataAddress
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// since we're manipulating variables shared between multiple goroutines,
|
||||||
|
// acquire a lock to avoid concurrency issues.
|
||||||
|
eh.lock.Lock()
|
||||||
|
// release the lock upon return
|
||||||
|
defer eh.lock.Unlock()
|
||||||
|
|
||||||
|
// loop through `req.Quantity` registers, from address `req.Addr` to
|
||||||
|
// `req.Addr + req.Quantity - 1`, which here is conveniently `req.Addr + i`
|
||||||
|
for i := 0; i < int(req.Quantity); i++ {
|
||||||
|
// ignore the write if the current register address is 80
|
||||||
|
if req.IsWrite && int(req.Addr) + i != 80 {
|
||||||
|
// assign the value
|
||||||
|
eh.coils[int(req.Addr) + i] = req.Args[i]
|
||||||
|
}
|
||||||
|
// append the value of the requested register to res so they can be
|
||||||
|
// sent back to the client
|
||||||
|
res = append(res, eh.coils[int(req.Addr) + i])
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discrete input handler method.
|
||||||
|
// Note that we're returning ErrIllegalFunction unconditionally.
|
||||||
|
// This will cause the client to receive "illegal function", which is the modbus way of
|
||||||
|
// reporting that this server does not support/implement the discrete input type.
|
||||||
|
func (eh *exampleHandler) HandleDiscreteInputs(req *modbus.DiscreteInputsRequest) (res []bool, err error) {
|
||||||
|
// this is the equivalent of saying
|
||||||
|
// "discrete inputs are not supported by this device"
|
||||||
|
// (try it with modbus-cli --target tcp://localhost:5502 rdi:1)
|
||||||
|
err = modbus.ErrIllegalFunction
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Holding register handler method.
|
||||||
|
// This method gets called whenever a valid modbus request asking for a holding register
|
||||||
|
// operation (either read or write) received by the server.
|
||||||
|
func (eh *exampleHandler) HandleHoldingRegisters(req *modbus.HoldingRegistersRequest) (res []uint16, err error) {
|
||||||
|
var regAddr uint16
|
||||||
|
|
||||||
|
if req.UnitId != 1 {
|
||||||
|
// only accept unit ID #1
|
||||||
|
err = modbus.ErrIllegalFunction
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// since we're manipulating variables shared between multiple goroutines,
|
||||||
|
// acquire a lock to avoid concurrency issues.
|
||||||
|
eh.lock.Lock()
|
||||||
|
// release the lock upon return
|
||||||
|
defer eh.lock.Unlock()
|
||||||
|
|
||||||
|
// loop through `quantity` registers
|
||||||
|
for i := 0; i < int(req.Quantity); i++ {
|
||||||
|
// compute the target register address
|
||||||
|
regAddr = req.Addr + uint16(i)
|
||||||
|
|
||||||
|
switch regAddr {
|
||||||
|
// expose the static, read-only value of 0xff00 in register 100
|
||||||
|
case 100:
|
||||||
|
res = append(res, 0xff00)
|
||||||
|
|
||||||
|
// expose holdingReg1 in register 101 (RW)
|
||||||
|
case 101:
|
||||||
|
if req.IsWrite {
|
||||||
|
eh.holdingReg1 = req.Args[i]
|
||||||
|
}
|
||||||
|
res = append(res, eh.holdingReg1)
|
||||||
|
|
||||||
|
// expose holdingReg2 in register 102 (RW)
|
||||||
|
case 102:
|
||||||
|
if req.IsWrite {
|
||||||
|
// only accept values 2 and 4
|
||||||
|
switch req.Args[i] {
|
||||||
|
case 2, 4:
|
||||||
|
eh.holdingReg2 = req.Args[i]
|
||||||
|
|
||||||
|
// make note of the change (e.g. for auditing purposes)
|
||||||
|
fmt.Printf("%s set reg#102 to %v\n", req.ClientAddr, eh.holdingReg2)
|
||||||
|
default:
|
||||||
|
// if the written value is neither 2 nor 4,
|
||||||
|
// return a modbus "illegal data value" to
|
||||||
|
// let the client know that the value is
|
||||||
|
// not acceptable.
|
||||||
|
err = modbus.ErrIllegalDataValue
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res = append(res, eh.holdingReg2)
|
||||||
|
|
||||||
|
// expose eh.holdingReg3 in register 103 (RW)
|
||||||
|
// note: eh.holdingReg3 is a signed 16-bit integer
|
||||||
|
case 103:
|
||||||
|
if req.IsWrite {
|
||||||
|
// cast the 16-bit unsigned integer passed by the server
|
||||||
|
// to a 16-bit signed integer when writing
|
||||||
|
eh.holdingReg3 = int16(req.Args[i])
|
||||||
|
}
|
||||||
|
// cast the 16-bit signed integer from the handler to a 16-bit unsigned
|
||||||
|
// integer so that we can append it to `res`.
|
||||||
|
res = append(res, uint16(eh.holdingReg3))
|
||||||
|
|
||||||
|
|
||||||
|
// expose the 16 most-significant bits of eh.holdingReg4 in register 200
|
||||||
|
case 200:
|
||||||
|
if req.IsWrite {
|
||||||
|
eh.holdingReg4 =
|
||||||
|
((uint32(req.Args[i]) << 16) & 0xffff0000 |
|
||||||
|
(eh.holdingReg4 & 0x0000ffff))
|
||||||
|
}
|
||||||
|
res = append(res, uint16((eh.holdingReg4 >> 16) & 0x0000ffff))
|
||||||
|
|
||||||
|
// expose the 16 least-significant bits of eh.holdingReg4 in register 201
|
||||||
|
case 201:
|
||||||
|
if req.IsWrite {
|
||||||
|
eh.holdingReg4 =
|
||||||
|
(uint32(req.Args[i]) & 0x0000ffff |
|
||||||
|
(eh.holdingReg4 & 0xffff0000))
|
||||||
|
}
|
||||||
|
res = append(res, uint16(eh.holdingReg4 & 0x0000ffff))
|
||||||
|
|
||||||
|
|
||||||
|
// any other address is unknown
|
||||||
|
default:
|
||||||
|
err = modbus.ErrIllegalDataAddress
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input register handler method.
|
||||||
|
// This method gets called whenever a valid modbus request asking for an input register
|
||||||
|
// operation is received by the server.
|
||||||
|
// Note that input registers are always read-only as per the modbus spec.
|
||||||
|
func (eh *exampleHandler) HandleInputRegisters(req *modbus.InputRegistersRequest) (res []uint16, err error) {
|
||||||
|
var unixTs_s uint32
|
||||||
|
var minusOne int16 = -1
|
||||||
|
|
||||||
|
if req.UnitId != 1 {
|
||||||
|
// only accept unit ID #1
|
||||||
|
err = modbus.ErrIllegalFunction
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the current unix timestamp, converted as a 32-bit unsigned integer for
|
||||||
|
// simplicity
|
||||||
|
unixTs_s = uint32(time.Now().Unix() & 0xffffffff)
|
||||||
|
|
||||||
|
// loop through all register addresses from req.addr to req.addr + req.Quantity - 1
|
||||||
|
for regAddr := req.Addr; regAddr < req.Addr + req.Quantity; regAddr++ {
|
||||||
|
switch regAddr {
|
||||||
|
case 100:
|
||||||
|
// return the static value 0x1111 at address 100, as an unsigned
|
||||||
|
// 16-bit integer
|
||||||
|
// (read it with modbus-cli --target tcp://localhost:5502 ri:uint16:100)
|
||||||
|
res = append(res, 0x1111)
|
||||||
|
|
||||||
|
case 101:
|
||||||
|
// return the static value -1 at address 101, as a signed 16-bit
|
||||||
|
// integer
|
||||||
|
// (read it with modbus-cli --target tcp://localhost:5502 ri:int16:101)
|
||||||
|
res = append(res, uint16(minusOne))
|
||||||
|
|
||||||
|
|
||||||
|
// expose our uptime counter, encoded as a 32-bit unsigned integer in
|
||||||
|
// input registers 200-201
|
||||||
|
// (read it with modbus-cli --target tcp://localhost:5502 ri:uint32:200)
|
||||||
|
case 200:
|
||||||
|
// return the 16 most significant bits of the uptime counter
|
||||||
|
// (using locking to avoid concurrency issues)
|
||||||
|
eh.lock.RLock()
|
||||||
|
res = append(res, uint16((eh.uptime >> 16) & 0xffff))
|
||||||
|
eh.lock.RUnlock()
|
||||||
|
|
||||||
|
case 201:
|
||||||
|
// return the 16 least significant bits of the uptime counter
|
||||||
|
// (again, using locking to avoid concurrency issues)
|
||||||
|
eh.lock.RLock()
|
||||||
|
res = append(res, uint16(eh.uptime & 0xffff))
|
||||||
|
eh.lock.RUnlock()
|
||||||
|
|
||||||
|
|
||||||
|
// expose the current unix timestamp, encoded as a 32-bit unsigned integer
|
||||||
|
// in input registers 202-203
|
||||||
|
// (read it with modbus-cli --target tcp://localhost:5502 ri:uint32:202)
|
||||||
|
case 202:
|
||||||
|
// return the 16 most significant bits of the current unix time
|
||||||
|
res = append(res, uint16((unixTs_s >> 16) & 0xffff))
|
||||||
|
|
||||||
|
case 203:
|
||||||
|
// return the 16 least significant bits of the current unix time
|
||||||
|
res = append(res, uint16(unixTs_s & 0xffff))
|
||||||
|
|
||||||
|
|
||||||
|
// return 3.1415, encoded as a 32-bit floating point number in input
|
||||||
|
// registers 300-301
|
||||||
|
// (read it with modbus-cli --target tcp://localhost:5502 ri:float32:300)
|
||||||
|
case 300:
|
||||||
|
// returh the 16 most significant bits of the number
|
||||||
|
res = append(res, uint16((math.Float32bits(3.1415) >> 16) & 0xffff))
|
||||||
|
|
||||||
|
case 301:
|
||||||
|
// returh the 16 least significant bits of the number
|
||||||
|
res = append(res, uint16((math.Float32bits(3.1415)) & 0xffff))
|
||||||
|
|
||||||
|
|
||||||
|
// attempting to access any input register address other than
|
||||||
|
// those defined above will result in an illegal data address
|
||||||
|
// exception client-side.
|
||||||
|
default:
|
||||||
|
err = modbus.ErrIllegalDataAddress
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||