first init
This commit is contained in:
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
|
||||
}
|
||||
92
examples/tls_client.go
Normal file
92
examples/tls_client.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/simonvetter/modbus"
|
||||
)
|
||||
|
||||
/*
|
||||
* Modbus client with TLS example.
|
||||
*
|
||||
* This file is intended to be a demo of the modbus client in TCP+TLS
|
||||
* mode. It shows how to load certificates from files and how to
|
||||
* configure the client to use them.
|
||||
*/
|
||||
|
||||
func main() {
|
||||
var client *modbus.ModbusClient
|
||||
var err error
|
||||
var clientKeyPair tls.Certificate
|
||||
var serverCertPool *x509.CertPool
|
||||
var regs []uint16
|
||||
|
||||
// load the client certificate and its associated private key, which
|
||||
// are used to authenticate the client to the server
|
||||
clientKeyPair, err = tls.LoadX509KeyPair(
|
||||
"certs/client.cert.pem", "certs/client.key.pem")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load client key pair: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load either the server certificate or the certificate of the CA
|
||||
// (Certificate Authority) which signed the server certificate
|
||||
serverCertPool, err = modbus.LoadCertPool("certs/server.cert.pem")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load server certificate/CA: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// create a client targetting host secure-plc on port 802 using
|
||||
// modbus TCP over TLS (MBAPS)
|
||||
client, err = modbus.NewClient(&modbus.ClientConfiguration{
|
||||
// tcp+tls is the moniker for MBAPS (modbus/tcp encapsulated in
|
||||
// TLS),
|
||||
// 802/tcp is the IANA-registered port for MBAPS.
|
||||
URL: "tcp+tls://secure-plc:802",
|
||||
// set the client-side cert and key
|
||||
TLSClientCert: &clientKeyPair,
|
||||
// set the server/CA certificate
|
||||
TLSRootCAs: serverCertPool,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create modbus client: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// now that the client is created and configured, attempt to connect
|
||||
err = client.Open()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to connect: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
// read two 16-bit holding registers at address 0x4000
|
||||
regs, err = client.ReadRegisters(0x4000, 2, modbus.HOLDING_REGISTER)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to read registers 0x4000 and 0x4001: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("register 0x4000: 0x%04x\n", regs[0])
|
||||
fmt.Printf("register 0x4001: 0x%04x\n", regs[1])
|
||||
}
|
||||
|
||||
// set register 0x4002 to 500
|
||||
err = client.WriteRegister(0x4002, 500)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to write to register 0x4002: %v\n", err)
|
||||
} else {
|
||||
fmt.Printf("set register 0x4002 to 500\n")
|
||||
}
|
||||
|
||||
// close the connection
|
||||
err = client.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to close connection: %v\n", err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
}
|
||||
255
examples/tls_server.go
Normal file
255
examples/tls_server.go
Normal file
@@ -0,0 +1,255 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/simonvetter/modbus"
|
||||
)
|
||||
|
||||
/* Modbus TCP+TLS (MBAPS or Modbus Security) server example.
|
||||
*
|
||||
* This file is intended to be a demo of the modbus server in a tcp+tls
|
||||
* configuration.
|
||||
* It shows how to configure and start a server, as well as how to use
|
||||
* client roles to perform authorization in the handler.
|
||||
* Feel free to use it as boilerplate for simple servers.
|
||||
*
|
||||
* This server simulates a simple wall clock device, exposing a 32-bit unix
|
||||
* timestamp in holding registers #0 and 1.
|
||||
* The timestamp is incremented every second by the main loop.
|
||||
*
|
||||
* Access control is done by way of Modbus Roles, which are encoded in the
|
||||
* client certificate as an X509 extension:
|
||||
* - any client can read the clock regardless of their role, provided that their
|
||||
* certificate is accepted by the server,
|
||||
* - only clients with the "operator" role specified in their certificate can
|
||||
* set the time.
|
||||
*
|
||||
* Certificates with no, invalid or multiple Modbus Role extensions will have
|
||||
* their role set to an empty string (req.ClientRole == "").
|
||||
*
|
||||
* Requests from clients with certificates not passing TLS verification are
|
||||
* rejected at the TLS layer (i.e. before reaching the Modbus layer).
|
||||
*
|
||||
*
|
||||
* The following commands can be used to create self-signed server and client
|
||||
* certificates:
|
||||
* $ mkdir certs
|
||||
*
|
||||
* create the server key pair:
|
||||
* $ openssl req -x509 -newkey rsa:4096 -sha256 -days 360 -nodes \
|
||||
* -keyout certs/server.key.pem -out certs/server.cert.pem \
|
||||
* -subj "/CN=TEST SERVER CERT DO NOT USE/" -addext "subjectAltName=DNS:localhost" \
|
||||
* -addext "keyUsage=keyCertSign,digitalSignature,keyEncipherment" \
|
||||
* -addext "extendedKeyUsage=critical,serverAuth"
|
||||
*
|
||||
* create a client certificate with the "user" role:
|
||||
* $ openssl req -x509 -newkey rsa:4096 -sha256 -days 360 -nodes \
|
||||
* -keyout certs/user-client.key.pem -out certs/user-client.cert.pem \
|
||||
* -subj "/CN=TEST CLIENT CERT DO NOT USE/" \
|
||||
* -addext "keyUsage=keyCertSign,digitalSignature,keyEncipherment" \
|
||||
* -addext "extendedKeyUsage=critical,clientAuth" \
|
||||
* -addext "1.3.6.1.4.1.50316.802.1=ASN1:UTF8String:user"
|
||||
*
|
||||
* create another client certificate with the "operator" role:
|
||||
* $ openssl req -x509 -newkey rsa:4096 -sha256 -days 360 -nodes \
|
||||
* -keyout certs/operator-client.key.pem -out certs/operator-client.cert.pem \
|
||||
* -subj "/CN=TEST CLIENT CERT DO NOT USE/" \
|
||||
* -addext "keyUsage=keyCertSign,digitalSignature,keyEncipherment" \
|
||||
* -addext "extendedKeyUsage=critical,clientAuth" \
|
||||
* -addext "1.3.6.1.4.1.50316.802.1=ASN1:UTF8String:operator"
|
||||
*
|
||||
* create a file containing both client certificates (for use by the server as an
|
||||
* 'allowed client list'):
|
||||
* $ cat certs/user-client.cert.pem certs/operator-client.cert.pem >certs/clients.cert.pem
|
||||
*
|
||||
* start the server:
|
||||
* $ go run examples/tls_server.go
|
||||
*
|
||||
* in another shell, read the clock with modbus-cli as the 'user' role:
|
||||
* $ go run cmd/modbus-cli.go --target tcp+tls://localhost:5802 --cert certs/user-client.cert.pem \
|
||||
* --key certs/user-client.key.pem --ca certs/server.cert.pem rh:uint32:0
|
||||
*
|
||||
* attempting to set the clock as 'user' should fail with Illegal Function:
|
||||
* $ go run cmd/modbus-cli.go --target tcp+tls://localhost:5802 --cert certs/user-client.cert.pem \
|
||||
* --key certs/user-client.key.pem --ca certs/server.cert.pem wr:uint32:0:1598692358
|
||||
*
|
||||
* setting the clock as 'operator' should succeed:
|
||||
* $ go run cmd/modbus-cli.go --target tcp+tls://localhost:5802 --cert certs/operator-client.cert.pem \
|
||||
* --key certs/operator-client.key.pem --ca certs/server.cert.pem wr:uint32:0:1598692358
|
||||
*
|
||||
* reading the cock as 'operator' should also work:
|
||||
* $ go run cmd/modbus-cli.go --target tcp+tls://localhost:5802 --cert certs/operator-client.cert.pem \
|
||||
* --key certs/operator-client.key.pem --ca certs/server.cert.pem rh:uint32:0
|
||||
*/
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
var eh *exampleHandler
|
||||
var server *modbus.ModbusServer
|
||||
var serverKeyPair tls.Certificate
|
||||
var clientCertPool *x509.CertPool
|
||||
var ticker *time.Ticker
|
||||
|
||||
// create the handler object
|
||||
eh = &exampleHandler{}
|
||||
|
||||
// load the server certificate and its associated private key, which
|
||||
// are used to authenticate the server to the client.
|
||||
// note that a tls.Certificate object can contain both the cert and its key,
|
||||
// which is the case here.
|
||||
serverKeyPair, err = tls.LoadX509KeyPair(
|
||||
"certs/server.cert.pem", "certs/server.key.pem")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load server key pair: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// load TLS client authentication material, which could either be:
|
||||
// - the CA (Certificate Authority) certificate(s) used to sign client certs,
|
||||
// - the list of allowed client certs, if client certificates are self-signed or
|
||||
// if client certificate pinning is required.
|
||||
clientCertPool, err = modbus.LoadCertPool("certs/clients.cert.pem")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load CA/client certificates: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// create the server object
|
||||
server, err = modbus.NewServer(&modbus.ServerConfiguration{
|
||||
// listen on localhost port 5802
|
||||
URL: "tcp+tls://localhost:5802",
|
||||
// accept 10 concurrent connections max.
|
||||
MaxClients: 10,
|
||||
// close idle connections after 1min of inactivity
|
||||
Timeout: 60 * time.Second,
|
||||
// use serverKeyPair as server certificate + server private key
|
||||
TLSServerCert: &serverKeyPair,
|
||||
// use the client cert/CA pool to verify client certificates
|
||||
TLSClientCAs: clientCertPool,
|
||||
}, 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)
|
||||
}
|
||||
fmt.Println("server started")
|
||||
|
||||
ticker = time.NewTicker(1 * time.Second)
|
||||
for {
|
||||
<-ticker.C
|
||||
|
||||
// increment the clock every second.
|
||||
// lock the handler object while updating the clock register to avoid
|
||||
// concurrency issues as each client is served from a dedicated goroutine.
|
||||
eh.lock.Lock()
|
||||
eh.clock++
|
||||
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
|
||||
|
||||
// unix timestamp register, incremented in the main() function above and exposed
|
||||
// as a 32-bit holding register (2 consecutive 16-bit modbus registers).
|
||||
clock uint32
|
||||
}
|
||||
|
||||
// Holding register handler method.
|
||||
// This method gets called whenever a valid modbus request asking for a holding register
|
||||
// operation is received by the server.
|
||||
func (eh *exampleHandler) HandleHoldingRegisters(req *modbus.HoldingRegistersRequest) (res []uint16, err error) {
|
||||
var regAddr uint16
|
||||
|
||||
// require the "operator" role for write operations (i.e. set the clock).
|
||||
if req.IsWrite && req.ClientRole != "operator" {
|
||||
fmt.Printf("write access denied: client %s missing the 'operator' role (role: '%s')\n",
|
||||
req.ClientAddr, req.ClientRole)
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// since we're manipulating variables accessed from 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 16 most-significant bits of the clock in register #0
|
||||
case 0:
|
||||
if req.IsWrite {
|
||||
eh.clock =
|
||||
((uint32(req.Args[i]) << 16) & 0xffff0000 |
|
||||
(eh.clock & 0x0000ffff))
|
||||
}
|
||||
res = append(res, uint16((eh.clock >> 16) & 0x0000ffff))
|
||||
|
||||
// expose the 16 least-significant bits of the clock in register #1
|
||||
case 1:
|
||||
if req.IsWrite {
|
||||
eh.clock =
|
||||
(uint32(req.Args[i]) & 0x0000ffff |
|
||||
(eh.clock & 0xffff0000))
|
||||
}
|
||||
res = append(res, uint16(eh.clock & 0x0000ffff))
|
||||
|
||||
// any other address is unknown
|
||||
default:
|
||||
err = modbus.ErrIllegalDataAddress
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// input registers are not used by this server.
|
||||
func (eh *exampleHandler) HandleInputRegisters(req *modbus.InputRegistersRequest) (res []uint16, err error) {
|
||||
// this is the equivalent of saying
|
||||
// "input registers are not supported by this device"
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// coils are not used by this server.
|
||||
func (eh *exampleHandler) HandleCoils(req *modbus.CoilsRequest) (res []bool, err error) {
|
||||
// this is the equivalent of saying
|
||||
// "coils are not supported by this device"
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
|
||||
// discrete inputs are not used by this server.
|
||||
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"
|
||||
err = modbus.ErrIllegalFunction
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user