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 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_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 new file mode 100644 index 0000000..6c5483e --- /dev/null +++ b/rtu_over_udp_client.go @@ -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 +}