Skip to content

Add ASCII/RTU over TCP transports #21

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 7, 2020
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
84 changes: 84 additions & 0 deletions ascii_over_tcp_client.go
Original file line number Diff line number Diff line change
@@ -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
}
100 changes: 100 additions & 0 deletions rtu_over_tcp_client.go
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 15 additions & 1 deletion test/README.md
Original file line number Diff line number Diff line change
@@ -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
----------------
Expand All @@ -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
Expand All @@ -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
```
48 changes: 48 additions & 0 deletions test/ascii_over_tcp_client_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
48 changes: 48 additions & 0 deletions test/rtu_over_tcp_client_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}