diff --git a/ascii_over_tcp_client.go b/ascii_over_tcp_client.go new file mode 100644 index 0000000..040de20 --- /dev/null +++ b/ascii_over_tcp_client.go @@ -0,0 +1,84 @@ +// Copyright 2018 xft. All rights reserved. +// This software may be modified and distributed under the terms +// of the BSD license. See the LICENSE file for details. + +package modbus + +import ( + "time" +) + +// ASCIIOverTCPClientHandler implements Packager and Transporter interface. +type ASCIIOverTCPClientHandler struct { + asciiPackager + asciiTCPTransporter +} + +// NewASCIIOverTCPClientHandler allocates and initializes a ASCIIOverTCPClientHandler. +func NewASCIIOverTCPClientHandler(address string) *ASCIIOverTCPClientHandler { + handler := &ASCIIOverTCPClientHandler{} + handler.Address = address + handler.Timeout = tcpTimeout + handler.IdleTimeout = tcpIdleTimeout + return handler +} + +// ASCIIOverTCPClient creates ASCII over TCP client with default handler and given connect string. +func ASCIIOverTCPClient(address string) Client { + handler := NewASCIIOverTCPClientHandler(address) + return NewClient(handler) +} + +// asciiTCPTransporter implements Transporter interface. +type asciiTCPTransporter struct { + tcpTransporter +} + +func (mb *asciiTCPTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) { + mb.tcpTransporter.mu.Lock() + defer mb.tcpTransporter.mu.Unlock() + + // Make sure port is connected + if err = mb.tcpTransporter.connect(); err != nil { + return + } + // Start the timer to close when idle + mb.tcpTransporter.lastActivity = time.Now() + mb.tcpTransporter.startCloseTimer() + // 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.tcpTransporter.logf("modbus: send %q\n", aduRequest) + if _, err = mb.conn.Write(aduRequest); err != nil { + return + } + // Get the response + var n int + var data [asciiMaxSize]byte + length := 0 + for { + if n, err = mb.conn.Read(data[length:]); err != nil { + return + } + length += n + if length >= asciiMaxSize || n == 0 { + break + } + // Expect end of frame in the data received + if length > asciiMinSize { + if string(data[length-len(asciiEnd):length]) == asciiEnd { + break + } + } + } + aduResponse = data[:length] + mb.tcpTransporter.logf("modbus: recv %q\n", aduResponse) + return +} diff --git a/rtu_over_tcp_client.go b/rtu_over_tcp_client.go new file mode 100644 index 0000000..d9ec19d --- /dev/null +++ b/rtu_over_tcp_client.go @@ -0,0 +1,100 @@ +// Copyright 2018 xft. All rights reserved. +// This software may be modified and distributed under the terms +// of the BSD license. See the LICENSE file for details. + +package modbus + +import ( + "io" + "time" +) + +// RTUOverTCPClientHandler implements Packager and Transporter interface. +type RTUOverTCPClientHandler struct { + rtuPackager + rtuTCPTransporter +} + +// NewRTUOverTCPClientHandler allocates and initializes a RTUOverTCPClientHandler. +func NewRTUOverTCPClientHandler(address string) *RTUOverTCPClientHandler { + handler := &RTUOverTCPClientHandler{} + handler.Address = address + handler.Timeout = tcpTimeout + handler.IdleTimeout = tcpIdleTimeout + return handler +} + +// RTUOverTCPClient creates RTU over TCP client with default handler and given connect string. +func RTUOverTCPClient(address string) Client { + handler := NewRTUOverTCPClientHandler(address) + return NewClient(handler) +} + +// rtuTCPTransporter implements Transporter interface. +type rtuTCPTransporter struct { + tcpTransporter +} + +// Send sends data to server and ensures adequate response for request type +func (mb *rtuTCPTransporter) 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 timer to close when idle + mb.lastActivity = time.Now() + mb.startCloseTimer() + // 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 +} diff --git a/test/README.md b/test/README.md index e3b5c94..3a06fb5 100644 --- a/test/README.md +++ b/test/README.md @@ -1,4 +1,4 @@ -System testing for [modbus library](https://github.com/goburrow/modbus) +System testing for [modbus library](https://github.com/grid-x/modbus) Modbus simulator ---------------- @@ -9,6 +9,7 @@ Modbus simulator # TCP $ diagslave -m tcp -p 5020 + # RTU/ASCII $ socat -d -d pty,raw,echo=0 pty,raw,echo=0 2015/04/03 12:34:56 socat[2342] N PTY is /dev/pts/6 @@ -18,7 +19,20 @@ $ diagslave -m ascii /dev/pts/7 # Or $ diagslave -m rtu /dev/pts/7 + +# RTU/ASCII Over TCP +$ socat -d -d pty,raw,echo=0 tcp-listen:5020,reuseaddr +2018/12/25 15:57:52 socat[30337] N PTY is /dev/pts/6 +2018/12/25 15:57:52 socat[30337] N listening on AF=2 0.0.0.0:5020 +$ diagslave -m ascii /dev/pts/6 + +# Or +$ diagslave -m rtu /dev/pts/6 + + $ go test -v -run TCP $ go test -v -run RTU $ go test -v -run ASCII +$ go test -v -run RTUOverTCP +$ go test -v -run ASCIIOverTCP ``` diff --git a/test/ascii_over_tcp_client_test.go b/test/ascii_over_tcp_client_test.go new file mode 100644 index 0000000..ad819c9 --- /dev/null +++ b/test/ascii_over_tcp_client_test.go @@ -0,0 +1,48 @@ +// Copyright 2018 xft. All rights reserved. +// This software may be modified and distributed under the terms +// of the BSD license. See the LICENSE file for details. + +package test + +import ( + "log" + "os" + "testing" + "time" + + "github.com/grid-x/modbus" +) + +const ( + asciiOverTCPDevice = "localhost:5020" +) + +func TestASCIIOverTCPClient(t *testing.T) { + // Diagslave does not support broadcast id. + handler := modbus.NewASCIIOverTCPClientHandler(asciiOverTCPDevice) + handler.SlaveId = 17 + ClientTestAll(t, modbus.NewClient(handler)) +} + +func TestASCIIOverTCPClientAdvancedUsage(t *testing.T) { + handler := modbus.NewASCIIOverTCPClientHandler(asciiOverTCPDevice) + handler.Timeout = 5 * time.Second + handler.SlaveId = 1 + handler.Logger = log.New(os.Stdout, "ascii over tcp: ", log.LstdFlags) + handler.Connect() + defer handler.Close() + + client := modbus.NewClient(handler) + results, err := client.ReadDiscreteInputs(15, 2) + if err != nil || results == nil { + t.Fatal(err, results) + } + results, err = client.WriteMultipleRegisters(1, 2, []byte{0, 3, 0, 4}) + if err != nil || results == nil { + t.Fatal(err, results) + } + results, err = client.WriteMultipleCoils(5, 10, []byte{4, 3}) + if err != nil || results == nil { + t.Fatal(err, results) + } +} diff --git a/test/rtu_over_tcp_client_test.go b/test/rtu_over_tcp_client_test.go new file mode 100644 index 0000000..bf8db56 --- /dev/null +++ b/test/rtu_over_tcp_client_test.go @@ -0,0 +1,48 @@ +// Copyright 2018 xft. All rights reserved. +// This software may be modified and distributed under the terms +// of the BSD license. See the LICENSE file for details. + +package test + +import ( + "log" + "os" + "testing" + "time" + + "github.com/grid-x/modbus" +) + +const ( + rtuOverTCPDevice = "localhost:5020" +) + +func TestRTUOverTCPClient(t *testing.T) { + // Diagslave does not support broadcast id. + handler := modbus.NewRTUOverTCPClientHandler(rtuOverTCPDevice) + handler.SlaveId = 17 + ClientTestAll(t, modbus.NewClient(handler)) +} + +func TestRTUOverTCPClientAdvancedUsage(t *testing.T) { + handler := modbus.NewRTUOverTCPClientHandler(rtuOverTCPDevice) + handler.Timeout = 5 * time.Second + handler.SlaveId = 1 + handler.Logger = log.New(os.Stdout, "rtu over tcp: ", log.LstdFlags) + handler.Connect() + defer handler.Close() + + client := modbus.NewClient(handler) + results, err := client.ReadDiscreteInputs(15, 2) + if err != nil || results == nil { + t.Fatal(err, results) + } + results, err = client.WriteMultipleRegisters(1, 2, []byte{0, 3, 0, 4}) + if err != nil || results == nil { + t.Fatal(err, results) + } + results, err = client.WriteMultipleCoils(5, 10, []byte{4, 3}) + if err != nil || results == nil { + t.Fatal(err, results) + } +}