Skip to content

Commit d804aff

Browse files
kcttanxiaofengandig
authored andcommitted
Add RTU/ASCII Over TCP support
1 parent 867898f commit d804aff

5 files changed

Lines changed: 295 additions & 0 deletions

File tree

ascii_over_tcp_client.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
// Copyright 2018 xft. All rights reserved.
2+
// This software may be modified and distributed under the terms
3+
// of the BSD license. See the LICENSE file for details.
4+
5+
package modbus
6+
7+
import (
8+
"time"
9+
)
10+
11+
// ASCIIOverTCPClientHandler implements Packager and Transporter interface.
12+
type ASCIIOverTCPClientHandler struct {
13+
asciiPackager
14+
asciiTCPTransporter
15+
}
16+
17+
// NewASCIIOverTCPClientHandler allocates and initializes a ASCIIOverTCPClientHandler.
18+
func NewASCIIOverTCPClientHandler(address string) *ASCIIOverTCPClientHandler {
19+
handler := &ASCIIOverTCPClientHandler{}
20+
handler.Address = address
21+
handler.Timeout = tcpTimeout
22+
handler.IdleTimeout = tcpIdleTimeout
23+
return handler
24+
}
25+
26+
// ASCIIOverTCPClient creates ASCII over TCP client with default handler and given connect string.
27+
func ASCIIOverTCPClient(address string) Client {
28+
handler := NewASCIIOverTCPClientHandler(address)
29+
return NewClient(handler)
30+
}
31+
32+
// asciiTCPTransporter implements Transporter interface.
33+
type asciiTCPTransporter struct {
34+
tcpTransporter
35+
}
36+
37+
func (mb *asciiTCPTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) {
38+
mb.tcpTransporter.mu.Lock()
39+
defer mb.tcpTransporter.mu.Unlock()
40+
41+
// Make sure port is connected
42+
if err = mb.tcpTransporter.connect(); err != nil {
43+
return
44+
}
45+
// Start the timer to close when idle
46+
mb.tcpTransporter.lastActivity = time.Now()
47+
mb.tcpTransporter.startCloseTimer()
48+
// Set write and read timeout
49+
var timeout time.Time
50+
if mb.Timeout > 0 {
51+
timeout = mb.lastActivity.Add(mb.Timeout)
52+
}
53+
if err = mb.conn.SetDeadline(timeout); err != nil {
54+
return
55+
}
56+
57+
// Send the request
58+
mb.tcpTransporter.logf("modbus: sending %q\n", aduRequest)
59+
if _, err = mb.conn.Write(aduRequest); err != nil {
60+
return
61+
}
62+
// Get the response
63+
var n int
64+
var data [asciiMaxSize]byte
65+
length := 0
66+
for {
67+
if n, err = mb.conn.Read(data[length:]); err != nil {
68+
return
69+
}
70+
length += n
71+
if length >= asciiMaxSize || n == 0 {
72+
break
73+
}
74+
// Expect end of frame in the data received
75+
if length > asciiMinSize {
76+
if string(data[length-len(asciiEnd):length]) == asciiEnd {
77+
break
78+
}
79+
}
80+
}
81+
aduResponse = data[:length]
82+
mb.tcpTransporter.logf("modbus: received %q\n", aduResponse)
83+
return
84+
}

rtu_over_tcp_client.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
// Copyright 2018 xft. All rights reserved.
2+
// This software may be modified and distributed under the terms
3+
// of the BSD license. See the LICENSE file for details.
4+
5+
package modbus
6+
7+
import (
8+
"io"
9+
"time"
10+
)
11+
12+
// RTUOverTCPClientHandler implements Packager and Transporter interface.
13+
type RTUOverTCPClientHandler struct {
14+
rtuPackager
15+
rtuTCPTransporter
16+
}
17+
18+
// NewRTUOverTCPClientHandler allocates and initializes a RTUOverTCPClientHandler.
19+
func NewRTUOverTCPClientHandler(address string) *RTUOverTCPClientHandler {
20+
handler := &RTUOverTCPClientHandler{}
21+
handler.Address = address
22+
handler.Timeout = tcpTimeout
23+
handler.IdleTimeout = tcpIdleTimeout
24+
return handler
25+
}
26+
27+
// RTUOverTCPClient creates RTU over TCP client with default handler and given connect string.
28+
func RTUOverTCPClient(address string) Client {
29+
handler := NewRTUOverTCPClientHandler(address)
30+
return NewClient(handler)
31+
}
32+
33+
// rtuTCPTransporter implements Transporter interface.
34+
type rtuTCPTransporter struct {
35+
tcpTransporter
36+
}
37+
38+
func (mb *rtuTCPTransporter) Send(aduRequest []byte) (aduResponse []byte, err error) {
39+
mb.tcpTransporter.mu.Lock()
40+
defer mb.tcpTransporter.mu.Unlock()
41+
42+
// Establish a new connection if not connected
43+
if err = mb.tcpTransporter.connect(); err != nil {
44+
return
45+
}
46+
// Set timer to close when idle
47+
mb.tcpTransporter.lastActivity = time.Now()
48+
mb.tcpTransporter.startCloseTimer()
49+
// Set write and read timeout
50+
var timeout time.Time
51+
if mb.Timeout > 0 {
52+
timeout = mb.lastActivity.Add(mb.Timeout)
53+
}
54+
if err = mb.conn.SetDeadline(timeout); err != nil {
55+
return
56+
}
57+
58+
// Send the request
59+
mb.tcpTransporter.logf("modbus: sending % x\n", aduRequest)
60+
if _, err = mb.conn.Write(aduRequest); err != nil {
61+
return
62+
}
63+
function := aduRequest[1]
64+
functionFail := aduRequest[1] & 0x80
65+
bytesToRead := calculateResponseLength(aduRequest)
66+
67+
var n int
68+
var n1 int
69+
var data [rtuMaxSize]byte
70+
//We first read the minimum length and then read either the full package
71+
//or the error package, depending on the error status (byte 2 of the response)
72+
n, err = io.ReadAtLeast(mb.conn, data[:], rtuMinSize)
73+
if err != nil {
74+
return
75+
}
76+
//if the function is correct
77+
if data[1] == function {
78+
//we read the rest of the bytes
79+
if n < bytesToRead {
80+
if bytesToRead > rtuMinSize && bytesToRead <= rtuMaxSize {
81+
if bytesToRead > n {
82+
n1, err = io.ReadFull(mb.conn, data[n:bytesToRead])
83+
n += n1
84+
}
85+
}
86+
}
87+
} else if data[1] == functionFail {
88+
//for error we need to read 5 bytes
89+
if n < rtuExceptionSize {
90+
n1, err = io.ReadFull(mb.conn, data[n:rtuExceptionSize])
91+
}
92+
n += n1
93+
}
94+
95+
if err != nil {
96+
return
97+
}
98+
aduResponse = data[:n]
99+
mb.logf("modbus: received % x\n", aduResponse)
100+
return
101+
}

test/README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Modbus simulator
99
# TCP
1010
$ diagslave -m tcp -p 5020
1111

12+
1213
# RTU/ASCII
1314
$ socat -d -d pty,raw,echo=0 pty,raw,echo=0
1415
2015/04/03 12:34:56 socat[2342] N PTY is /dev/pts/6
@@ -18,7 +19,20 @@ $ diagslave -m ascii /dev/pts/7
1819
# Or
1920
$ diagslave -m rtu /dev/pts/7
2021

22+
23+
# RTU/ASCII Over TCP
24+
$ socat -d -d pty,raw,echo=0 tcp-listen:5020,reuseaddr
25+
2018/12/25 15:57:52 socat[30337] N PTY is /dev/pts/6
26+
2018/12/25 15:57:52 socat[30337] N listening on AF=2 0.0.0.0:5020
27+
$ diagslave -m ascii /dev/pts/6
28+
29+
# Or
30+
$ diagslave -m rtu /dev/pts/6
31+
32+
2133
$ go test -v -run TCP
2234
$ go test -v -run RTU
2335
$ go test -v -run ASCII
36+
$ go test -v -run RTUOverTCP
37+
$ go test -v -run ASCIIOverTCP
2438
```

test/ascii_over_tcp_client_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2018 xft. All rights reserved.
2+
// This software may be modified and distributed under the terms
3+
// of the BSD license. See the LICENSE file for details.
4+
5+
package test
6+
7+
import (
8+
"log"
9+
"os"
10+
"testing"
11+
"time"
12+
13+
"github.com/goburrow/modbus"
14+
)
15+
16+
const (
17+
asciiOverTCPDevice = "localhost:5020"
18+
)
19+
20+
func TestASCIIOverTCPClient(t *testing.T) {
21+
// Diagslave does not support broadcast id.
22+
handler := modbus.NewASCIIOverTCPClientHandler(asciiOverTCPDevice)
23+
handler.SlaveId = 17
24+
ClientTestAll(t, modbus.NewClient(handler))
25+
}
26+
27+
func TestASCIIOverTCPClientAdvancedUsage(t *testing.T) {
28+
handler := modbus.NewASCIIOverTCPClientHandler(asciiOverTCPDevice)
29+
handler.Timeout = 5 * time.Second
30+
handler.SlaveId = 1
31+
handler.Logger = log.New(os.Stdout, "ascii over tcp: ", log.LstdFlags)
32+
handler.Connect()
33+
defer handler.Close()
34+
35+
client := modbus.NewClient(handler)
36+
results, err := client.ReadDiscreteInputs(15, 2)
37+
if err != nil || results == nil {
38+
t.Fatal(err, results)
39+
}
40+
results, err = client.WriteMultipleRegisters(1, 2, []byte{0, 3, 0, 4})
41+
if err != nil || results == nil {
42+
t.Fatal(err, results)
43+
}
44+
results, err = client.WriteMultipleCoils(5, 10, []byte{4, 3})
45+
if err != nil || results == nil {
46+
t.Fatal(err, results)
47+
}
48+
}

test/rtu_over_tcp_client_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
// Copyright 2018 xft. All rights reserved.
2+
// This software may be modified and distributed under the terms
3+
// of the BSD license. See the LICENSE file for details.
4+
5+
package test
6+
7+
import (
8+
"log"
9+
"os"
10+
"testing"
11+
"time"
12+
13+
"github.com/goburrow/modbus"
14+
)
15+
16+
const (
17+
rtuOverTCPDevice = "localhost:5020"
18+
)
19+
20+
func TestRTUOverTCPClient(t *testing.T) {
21+
// Diagslave does not support broadcast id.
22+
handler := modbus.NewRTUOverTCPClientHandler(rtuOverTCPDevice)
23+
handler.SlaveId = 17
24+
ClientTestAll(t, modbus.NewClient(handler))
25+
}
26+
27+
func TestRTUOverTCPClientAdvancedUsage(t *testing.T) {
28+
handler := modbus.NewRTUOverTCPClientHandler(rtuOverTCPDevice)
29+
handler.Timeout = 5 * time.Second
30+
handler.SlaveId = 1
31+
handler.Logger = log.New(os.Stdout, "rtu over tcp: ", log.LstdFlags)
32+
handler.Connect()
33+
defer handler.Close()
34+
35+
client := modbus.NewClient(handler)
36+
results, err := client.ReadDiscreteInputs(15, 2)
37+
if err != nil || results == nil {
38+
t.Fatal(err, results)
39+
}
40+
results, err = client.WriteMultipleRegisters(1, 2, []byte{0, 3, 0, 4})
41+
if err != nil || results == nil {
42+
t.Fatal(err, results)
43+
}
44+
results, err = client.WriteMultipleCoils(5, 10, []byte{4, 3})
45+
if err != nil || results == nil {
46+
t.Fatal(err, results)
47+
}
48+
}

0 commit comments

Comments
 (0)