From 4237d62997afaac06f24d81128585c82c714b361 Mon Sep 17 00:00:00 2001 From: Stefan Nienhuis Date: Sun, 18 Feb 2024 16:25:19 +0100 Subject: [PATCH 1/4] feat: Implement RTU over UDP --- cmd/modbus-cli/main.go | 6 ++ rtu_over_udp_client.go | 146 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 rtu_over_udp_client.go diff --git a/cmd/modbus-cli/main.go b/cmd/modbus-cli/main.go index 7a4e8d9..39c69a8 100644 --- a/cmd/modbus-cli/main.go +++ b/cmd/modbus-cli/main.go @@ -472,7 +472,13 @@ func newHandler(o option) (modbus.ClientHandler, error) { h.ProtocolRecoveryTimeout = o.tcp.protocolRecoveryTimeout h.Logger = o.logger return h, nil + case "udp": + h := modbus.NewRTUOverUDPClientHandler(u.Host) + h.SlaveID = byte(o.slaveID) + h.Logger = o.logger + return h, nil } + return nil, fmt.Errorf("unsupported scheme: %s", u.Scheme) } diff --git a/rtu_over_udp_client.go b/rtu_over_udp_client.go new file mode 100644 index 0000000..2d07838 --- /dev/null +++ b/rtu_over_udp_client.go @@ -0,0 +1,146 @@ +package modbus + +import ( + "io" + "net" + "sync" +) + +// RTUOverUDPClientHandler implements Packager and Transporter interface. +type RTUOverUDPClientHandler struct { + rtuPackager + rtuUDPTransporter +} + +// NewRTUOverUDPClientHandler allocates and initializes a RTUOverUDPClientHandler. +func NewRTUOverUDPClientHandler(address string) *RTUOverUDPClientHandler { + handler := &RTUOverUDPClientHandler{} + handler.Address = address + return handler +} + +// RTUOverUDPClient creates RTU over UDP client with default handler and given connect string. +func RTUOverUDPClient(address string) Client { + handler := NewRTUOverUDPClientHandler(address) + return NewClient(handler) +} + +// rtuUDPTransporter implements Transporter interface. +type rtuUDPTransporter struct { + // Connect string + Address string + // Transmission logger + Logger logger + + // UDP connection + mu sync.Mutex + conn net.Conn +} + +// Send sends data to server and ensures adequate response for request type +func (mb *rtuUDPTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) { + mb.mu.Lock() + defer mb.mu.Unlock() + + // Establish a new connection if not connected + if err = mb.connect(); err != nil { + return + } + + // Set write and read timeout + // var timeout time.Time + // if mb.Timeout > 0 { + // timeout = mb.lastActivity.Add(mb.Timeout) + // } + // if err = mb.conn.SetDeadline(timeout); err != nil { + // return + // } + + // Send the request + mb.logf("modbus: send % x\n", aduRequest) + if _, err = mb.conn.Write(aduRequest); err != nil { + return + } + function := aduRequest[1] + functionFail := aduRequest[1] & 0x80 + bytesToRead := calculateResponseLength(aduRequest) + + var n int + var n1 int + var data [rtuMaxSize]byte + //We first read the minimum length and then read either the full package + //or the error package, depending on the error status (byte 2 of the response) + n, err = io.ReadAtLeast(mb.conn, data[:], rtuMinSize) + if err != nil { + return + } + //if the function is correct + if data[1] == function { + //we read the rest of the bytes + if n < bytesToRead { + if bytesToRead > rtuMinSize && bytesToRead <= rtuMaxSize { + n1, err = io.ReadFull(mb.conn, data[n:bytesToRead]) + n += n1 + } + } + } else if data[1] == functionFail { + //for error we need to read 5 bytes + if n < rtuExceptionSize { + n1, err = io.ReadFull(mb.conn, data[n:rtuExceptionSize]) + } + n += n1 + } + + if err != nil { + return + } + aduResponse = data[:n] + mb.logf("modbus: recv % x\n", aduResponse) + return +} + +func (mb *rtuUDPTransporter) logf(format string, v ...interface{}) { + if mb.Logger != nil { + mb.Logger.Printf(format, v...) + } +} + +// Connect establishes a new connection to the address in Address. +func (mb *rtuUDPTransporter) Connect() error { + mb.mu.Lock() + defer mb.mu.Unlock() + + return mb.connect() +} + +// connect establishes a new connection to the address in Address. Caller must hold the mutex before calling this method. +// Since UDP is connectionless this does little more than setting up the connection object. +func (mb *rtuUDPTransporter) connect() error { + if mb.conn == nil { + dialer := net.Dialer{} + conn, err := dialer.Dial("udp", mb.Address) + if err != nil { + return err + } + mb.conn = conn + } + return nil +} + +// Close closes current connection. +func (mb *rtuUDPTransporter) Close() error { + mb.mu.Lock() + defer mb.mu.Unlock() + + return mb.close() +} + +// close closes current connection. Caller must hold the mutex before calling this method. +// Since UDP is connectionless this does little more than freeing up the connection object. +func (mb *rtuUDPTransporter) close() (err error) { + if mb.conn != nil { + err = mb.conn.Close() + mb.conn = nil + } + return +} From d0431e769eee1041e8da4524bccd5ccdc46a33d6 Mon Sep 17 00:00:00 2001 From: Stefan Nienhuis Date: Sun, 18 Feb 2024 16:29:44 +0100 Subject: [PATCH 2/4] docs: Document UDP protocol --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 6852e36..3ff3c00 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Bit access: # Supported formats - TCP - Serial (RTU, ASCII) +- UDP # Usage Basic usage: @@ -81,6 +82,12 @@ For Modbus RTU, replace the address field and use the `rtu-` arguments in order ```sh ./modbus-cli -address=rtu:///dev/ttyUSB0 -rtu-baudrate=57600 -rtu-stopbits=2 -rtu-parity=N -rtu-databits=8 ... ``` + +For Modbus UDP, replace the address field with a UDP address. +```sh +./modbus-cli --address=udp://127.0.0.1:502 ... +``` + ### Reading Registers Read 1 register and get raw result From 47a626895b8998530ec7cc3f080efe04890c7e21 Mon Sep 17 00:00:00 2001 From: Stefan Nienhuis Date: Fri, 23 Feb 2024 15:39:01 +0100 Subject: [PATCH 3/4] fmt: Clean up RTU over UDP comments --- rtu_over_tcp_client.go | 10 +++++----- rtu_over_udp_client.go | 19 +++++-------------- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/rtu_over_tcp_client.go b/rtu_over_tcp_client.go index d9ec19d..1a88080 100644 --- a/rtu_over_tcp_client.go +++ b/rtu_over_tcp_client.go @@ -68,15 +68,15 @@ func (mb *rtuTCPTransporter) Send(aduRequest []byte) (aduResponse []byte, err er var n int var n1 int var data [rtuMaxSize]byte - //We first read the minimum length and then read either the full package - //or the error package, depending on the error status (byte 2 of the response) + // We first read the minimum length and then read either the full package + // or the error package, depending on the error status (byte 2 of the response) n, err = io.ReadAtLeast(mb.conn, data[:], rtuMinSize) if err != nil { return } - //if the function is correct + // if the function is correct if data[1] == function { - //we read the rest of the bytes + // we read the rest of the bytes if n < bytesToRead { if bytesToRead > rtuMinSize && bytesToRead <= rtuMaxSize { n1, err = io.ReadFull(mb.conn, data[n:bytesToRead]) @@ -84,7 +84,7 @@ func (mb *rtuTCPTransporter) Send(aduRequest []byte) (aduResponse []byte, err er } } } else if data[1] == functionFail { - //for error we need to read 5 bytes + // for error we need to read 5 bytes if n < rtuExceptionSize { n1, err = io.ReadFull(mb.conn, data[n:rtuExceptionSize]) } diff --git a/rtu_over_udp_client.go b/rtu_over_udp_client.go index 2d07838..fa3f1b1 100644 --- a/rtu_over_udp_client.go +++ b/rtu_over_udp_client.go @@ -47,15 +47,6 @@ func (mb *rtuUDPTransporter) Send(aduRequest []byte) (aduResponse []byte, err er return } - // Set write and read timeout - // var timeout time.Time - // if mb.Timeout > 0 { - // timeout = mb.lastActivity.Add(mb.Timeout) - // } - // if err = mb.conn.SetDeadline(timeout); err != nil { - // return - // } - // Send the request mb.logf("modbus: send % x\n", aduRequest) if _, err = mb.conn.Write(aduRequest); err != nil { @@ -68,15 +59,15 @@ func (mb *rtuUDPTransporter) Send(aduRequest []byte) (aduResponse []byte, err er var n int var n1 int var data [rtuMaxSize]byte - //We first read the minimum length and then read either the full package - //or the error package, depending on the error status (byte 2 of the response) + // We first read the minimum length and then read either the full package + // or the error package, depending on the error status (byte 2 of the response) n, err = io.ReadAtLeast(mb.conn, data[:], rtuMinSize) if err != nil { return } - //if the function is correct + // if the function is correct if data[1] == function { - //we read the rest of the bytes + // we read the rest of the bytes if n < bytesToRead { if bytesToRead > rtuMinSize && bytesToRead <= rtuMaxSize { n1, err = io.ReadFull(mb.conn, data[n:bytesToRead]) @@ -84,7 +75,7 @@ func (mb *rtuUDPTransporter) Send(aduRequest []byte) (aduResponse []byte, err er } } } else if data[1] == functionFail { - //for error we need to read 5 bytes + // for error we need to read 5 bytes if n < rtuExceptionSize { n1, err = io.ReadFull(mb.conn, data[n:rtuExceptionSize]) } From 6e290f266aed865b73c46bb81cbebe59e25adf9f Mon Sep 17 00:00:00 2001 From: Stefan Nienhuis Date: Fri, 23 Feb 2024 15:48:01 +0100 Subject: [PATCH 4/4] fix: Add length checks to RTU over UDP --- rtu_over_udp_client.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/rtu_over_udp_client.go b/rtu_over_udp_client.go index fa3f1b1..6c5483e 100644 --- a/rtu_over_udp_client.go +++ b/rtu_over_udp_client.go @@ -1,11 +1,26 @@ package modbus import ( + "fmt" "io" "net" "sync" ) +// ErrADURequestLength informs about a wrong ADU request length. +type ErrADURequestLength int + +func (length ErrADURequestLength) Error() string { + return fmt.Sprintf("modbus: ADU request length '%d' must not be less than 2", length) +} + +// ErrADUResponseLength informs about a wrong ADU request length. +type ErrADUResponseLength int + +func (length ErrADUResponseLength) Error() string { + return fmt.Sprintf("modbus: ADU response length '%d' must not be less than 2", length) +} + // RTUOverUDPClientHandler implements Packager and Transporter interface. type RTUOverUDPClientHandler struct { rtuPackager @@ -42,6 +57,12 @@ func (mb *rtuUDPTransporter) Send(aduRequest []byte) (aduResponse []byte, err er mb.mu.Lock() defer mb.mu.Unlock() + // Check ADU request length + if len(aduRequest) < 2 { + err = ErrADURequestLength(len(aduRequest)) + return + } + // Establish a new connection if not connected if err = mb.connect(); err != nil { return @@ -65,6 +86,13 @@ func (mb *rtuUDPTransporter) Send(aduRequest []byte) (aduResponse []byte, err er if err != nil { return } + + // Check ADU response length + if len(data) < 2 { + err = ErrADUResponseLength(len(data)) + return + } + // if the function is correct if data[1] == function { // we read the rest of the bytes