Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Bit access:
# Supported formats
- TCP
- Serial (RTU, ASCII)
- UDP

# Usage
Basic usage:
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions cmd/modbus-cli/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
10 changes: 5 additions & 5 deletions rtu_over_tcp_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,23 +68,23 @@ 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])
n += n1
}
}
} 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])
}
Expand Down
165 changes: 165 additions & 0 deletions rtu_over_udp_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
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
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()

// 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
}

// 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
}

// 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
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
}