cfy-feate-dev #1

Merged
Fuyao merged 1 commits from cfy-feate-dev into main 2025-11-07 13:53:51 +08:00
30 changed files with 9185 additions and 0 deletions
Showing only changes of commit 9831b4b948 - Show all commits

21
LICENSE.txt Normal file
View 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
View File

@@ -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.

1320
client.go Normal file

File diff suppressed because it is too large Load Diff

584
client_tls_test.go Normal file
View 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-----
MIIFkzCCA3ugAwIBAgIUWnvnN1r9czWyX7TGS+AwTNICd4wwDQYJKoZIhvcNAQEL
BQAwKTEnMCUGA1UEAwwebG9jYWxob3N0IFRFU1QgQ0VSVCBETyBOT1QgVVNFMB4X
DTIwMDgyMTA5NTEyMVoXDTQwMDgxNjA5NTEyMVowKTEnMCUGA1UEAwwebG9jYWxo
b3N0IFRFU1QgQ0VSVCBETyBOT1QgVVNFMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
MIICCgKCAgEA4D+a4wqvwxhyMhN4Z6EG7pIU1TfL7hV2MH4Izx1sDHGaUu+318SE
Egn85Zn1PbYAvYqlN+Ti3pCH5/tSJJHD4XVGcXtp3Wswt5MTXX8Ny3f1v3ZeQggp
nTy2tyODTulCQBg5L+8FTgJM2mJR0D+dryswiWgDVBLxg5W9p7icff30n/LHtEGd
jTkVbkaG798iGaIIeI6YS1wjMfsPWGWpG9SVoC3bkHN2NL2apecCLsoZpb+DiKdT
1rBG2pNeDseGpSWKwF/2/HeJsw+tD4okbtfYA7uURmRyqv1rxAXmclZXHFHpUL8l
Vt69g+ER0sXmLavM2Jj3iss6RF2MP6ghVUAcaciPbuDCn6+vnxCE6L2Gyr9G6Aur
rOBl/nRj3BHK9agp0fLzhIKgfCKMzCU5mo/UFlJIKbKIRJcdF5LNF3A9wD0K3Rv/
2bIvaXdWwIgUQ+zX3V3cDuMatCs/F2jGE5FejGaNeA7ixfpdCtybBpzGewLB49NB
AIFBboJBdfW3QuqQBM32GFmbwM4cZpdxr97cZTDJh3Age7e8BSWPO195IJEKWNSn
bnWCDNG5J4G6MBf9AfC/ljJCrOIEN4wTXP6EF4vaMq/VWz674j7QvR9q9aKB1lNn
bdKd/LMH+jmgG8bGuy01Tj12/JBgzgG0KI72364wuJlTjneqkTpCncMCAwEAAaOB
sjCBrzAdBgNVHQ4EFgQUEFEWxaWofRhSTd2+ZWmaH14OKAswHwYDVR0jBBgwFoAU
EFEWxaWofRhSTd2+ZWmaH14OKAswDwYDVR0TAQH/BAUwAwEB/zAsBgNVHREEJTAj
gglsb2NhbGhvc3SHEAAAAAAAAAAAAAAAAAAAAAGHBH8AAAEwCwYDVR0PBAQDAgWg
MBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQEL
BQADggIBACYfWN0rK/OTXDZEQHLWLe6SDailqJSULVRhzxevo3i07MhsQCARofXH
A37b0q/GHhLsKUEsjB+TYip90eKaRV1XDgnMOdbQxVCKY5IVTmobnoj5ZLja1AgD
4I3CTxx3tqxLxRmV1Bre1tIqHnalA6kC6HJAKW+R0biPSccchIjW2ljB1SXBcS2T
RjAsIfbEl9sDhyLl8jaOaOBLPLS5PNgRs4XJ8/ps9dDCNyeOizGVzAsgaeTxXadC
Y505cxoWQR1atYerjqVr0XolCTkefOapsNJzH3YXF2mxKJCVxARv8Ns8e5WwIWw2
r1ESi6M1qca5CutEgbBdUp7NyF44HJ9O3EsG+CFO6XRn6aaUvmin6vufKk29usRm
L3RWqBH1vz3vQzVLfEzXnJwnxDwZWcBrGx3RjKAL+O+hWHc3Qh6+AfI0yRX4j0MR
7IMHESf2xkCtw58w1t+OA1GBZ7hBX4zRiAQ89hk8UzRMw45yQ3cPkAp9u+PhrY1i
9dcDqvPueaSDoRMl7VvHyQ+2SeQF7mc3Xx6iAm9HPBmuVWVpX32g9jbu0xfzWhng
DXf3U5zg6BsG3gR5omPwbApKBlGckRY+ZuarhxPeczBx6KVIOKgvafybKrCsbso2
oq2sBRSZveoEKZDOmZpsUP2jYrcgrybnurcoN6g1Chl28V5rNITd
-----END CERTIFICATE-----`
serverKey string = `-----BEGIN PRIVATE KEY-----
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
yAqUh6zMzzcee67KOWWZMTuPQuu1n/m1
-----END PRIVATE KEY-----`
clientCert string = `
-----BEGIN CERTIFICATE-----
MIIFXzCCA0egAwIBAgIUQzQeLPGsr6OmD5NtrsiYMFVwATwwDQYJKoZIhvcNAQEL
BQAwJjEkMCIGA1UEAwwbVEVTVCBDTElFTlQgQ0VSVCBETyBOT1QgVVNFMB4XDTIw
MDgyMTA5NTI0NVoXDTQwMDgxNjA5NTI0NVowJjEkMCIGA1UEAwwbVEVTVCBDTElF
TlQgQ0VSVCBETyBOT1QgVVNFMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKC
AgEA5yBIvheS8d2T8lBn9BOsdi1mMmhUHyqxdx9YFgwIV0NYb9s3/J83Jf/Focob
DM4fdy7iuSECX8/KUoymQmn26ivzmI4iLJ0LsBbUhTzMO9lo82Vg18E4Ab4GQVMz
LWcxnt1wt2EYJ5nq72c1h9K27pIecDDZ9DtBD1j0d3cuoo8HIVzsUfZRp80H9b5H
WuY2nMPZC3jp6HlsVSHCbkuscs/d7dDGK4tsanmyqVfBNmJhNKvo2GwMM9vf82li
dh6OwrqUNeMXkDU8vQ/xMGWfZ+Xpu0vXx3pQ5SX3WVDZFMuk7/mMBZwUhnXh+yjC
R1MVIRJvjijnkFzSSXctoysl/Mrc3QW5QmPmoa8KVWL8pSc5oaMMdX9bXo9omPf1
XlmjKMvEUu2IbUQcaDFtVAKzR0UGcYEFh/QCIu7WV0pA24DfiX3r0Lv1GHHB6X+3
+zUH7ZcaajzVQM/crCB/VLQiZODg6EgFtx1woll5hES/I6l9Me7UKySlxY4wTjIk
k/cwIN6R0dHGDny30fIkOl0vseo6SKtIkApYfe2tAASatbNdpkJQbMjUkm1IlcYj
ZZdn7yssjLjmAWn6pz19GbW2sAGQ/D4k/rN2hOpCc3NSP2FMIeWwp6TUHUG2ZFck
79j/Fo2bPNTPiFWxUW6h6gWHDgZg8UFz3FXvaeUM0URFOjsCAwEAAaOBhDCBgTAd
BgNVHQ4EFgQUZkTFhQZ4vaia2hiIQrZnSfx2nOAwHwYDVR0jBBgwFoAUZkTFhQZ4
vaia2hiIQrZnSfx2nOAwDwYDVR0TAQH/BAUwAwEB/zALBgNVHQ8EBAMCBaAwEwYD
VR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOC
AgEAhDVloT4TqZL66A/N8GSbiAALYVM4VoQlaiYiNrtwcNBAKrveN/RJlVxvYC9f
ybsvHz+wf/UAkWsQqd7pacKMlctNYoavpooO2ketQErpz+Ysmb+yGlL5u9DwQI36
bsw/sxZjA4uunEM3yySVZf2k5j97HzBhhv24dlgxCPyu5tOAj83bM2QLTc5H7/KZ
ZEhMcrXN0+QUI9np3WYPKAPMJNODSMGD8mMqpjRufxDH0jhPhX4R4qvhHT+/OrLE
CwLTwtgZ8BnRS2b16QEGpvT7bu5EWZda4vgXQEeuMpEgUmwPOm2JS9QZguXrhA6u
Jd/12gbNEowQCt0qig1K2/ouYc3YKvCq/GuDPZnVq0nXEgSom4+g4UpU92zHARSy
CjfEW+rD9ay0ipzl6wxV09ZoQOoFwztf/AO89gl2CDtcw1J+mB8KcP2Pme+lWZ9m
mj7+ed+lubE5kBIK/H2EojEUceGmdluqD/T6bUaAR6edLuS0z4MKFTNlbbZq9QiS
vb6vr137SqCw56gFvYzxxOS2037QHAHk9dZz4+ik6BLXOQmHY1s59y/iAV3CrWwf
wVi6BS05QtOQW1nzeUU4DyMz4aAuBs88iGqDlipzkMreyYTG/66WpKCp/nezSn5H
cufNpBGKcE0Ww/H/GgMvKe/nB7HEJQqoAxVDeq75WFiHQrs=
-----END CERTIFICATE-----
`
clientKey string = `-----BEGIN PRIVATE KEY-----
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
v8+pZi41bah3Pfqgp0Q9IkGcCk8JVnAbc0syYy0CggEATeNNedXh3DJmSG2ijOQX
Kbdb/asDErzFnWQd6RX/W6JG645KEfS1wo/9OBKEgIRANrP7wl3kXtxiu3EHZ5xD
obGAhSpHv6qdPvaNNIoBZvmf+I+0sNkQJ8BFTstZVslBZRsMv23D3vmNjgvUvKyr
Wa86tabN8H4ahnp4XYV4HtwTcdOqSy+Z72qcw83RWGj6owS3iOPDrCLEnihgibMd
9F726pWyyaU1Omnq4PjwEMUD67GFKBqeAQRtt2597LeNAAASB/HzGiXwPij71a2t
QijspXUDPzDwqAzI0D5tkSxT/+gNwL5ilpVQwx1bOdhOP6RoJVEnz83GYvsOBN+F
EQKCAQEAo9j9MG+VCz+loz4fUXIJjC63ypfuRfxTCAIBMn4HzohP8chEcQBlWLCH
t0WcguYnwsuxGR4Rhx02UZCx3qNxiroBZ9w1NqTk947ZjKuNzqI7IpIqvtJ18op6
QgQu8piNkf0/etAO0e6IjbZe4WfJCeKsAqE4vCV43baaSiHN/0pfYi6LLJ2YmTF/
+sYY43naHg3zQTL4JbL4c58ebe4ADj4wIdNJ+/H5JgQf6r14iNjpyc6BJOjFuPyx
EJHQKb6499HKFua3QuH/kA6Ogfm9o3Lnwx/VO1lPLFteTv1fBKK00C00SkmyIe1p
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

File diff suppressed because it is too large Load Diff

73
crc.go Normal file
View 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
View 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
View 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
View 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
View 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