Skip to content

Commit

Permalink
Modbus server examples
Browse files Browse the repository at this point in the history
  • Loading branch information
aldas committed Jul 25, 2023
1 parent 41ad2d9 commit c30cd6a
Show file tree
Hide file tree
Showing 19 changed files with 747 additions and 296 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ permissions:
contents: read # to fetch code (actions/checkout)

env:
# run coverage and benchmarks only with the latest Go version
# run coverage only with the latest Go version
LATEST_GO_VERSION: "1.20"


Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

* Added `packet.IsLikeModbusTCP()` to check if given bytes are possibly TCP packet or start of packet.
* Added `packet.LooksLikeModbusTCP()` to check if given bytes are possibly TCP packet or start of packet.
* Added `Parse*Request*` for every function type to help implement Modbus servers.
* Added `Server` package to implement your own modbus server

## [0.0.1] - 2021-04-11

Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,18 @@ for _, req := range requests {
assert.Equal(t, "alarm_do_1", fields[1].Field.Name)
}
```

### RTU over serial port

RTU examples to interact with serial port can be found from [serial.md](serial.md)

### Low level packets

```go
client := modbus.NewTCPClient(modbus.WithTimeouts(10*time.Second, 10*time.Second))
client := modbus.NewTCPClientWithConfig(modbus.ClientConfig{
WriteTimeout: 2 * time.Second,
ReadTimeout: 2 * time.Second,
})
if err := client.Connect(context.Background(), "localhost:5020"); err != nil {
return err
}
Expand All @@ -103,6 +107,7 @@ uint32Var, err := registers.Uint32(17) // extract uint32 value from register 17
```

To create single TCP packet use following methods. Use `RTU` suffix to create RTU packets.

```go
import "github.com/aldas/go-modbus-client/packet"

Expand Down
5 changes: 1 addition & 4 deletions builder_external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,11 @@ func TestExternalUsage(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, reqs, 1)

client := modbus.NewClient()
client := modbus.NewTCPClient()
if err := client.Connect(context.Background(), addr); err != nil {
return
}

//for _, req := range reqs {
//
//}
req := reqs[0] // skip looping as we always have 1 request in this example
resp, err := client.Do(context.Background(), req)

Expand Down
32 changes: 14 additions & 18 deletions builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@ func TestBuilder_ReadHoldingRegistersTCP(t *testing.T) {

receivedChan := make(chan []byte, 1)
handler := func(received []byte, bytesRead int) (response []byte, closeConnection bool) {
if bytesRead == 0 {
return nil, false
}
receivedChan <- received
resp := packet.ReadHoldingRegistersResponseTCP{
MBAPHeader: packet.MBAPHeader{TransactionID: 123, ProtocolID: 0},
Expand All @@ -41,17 +38,25 @@ func TestBuilder_ReadHoldingRegistersTCP(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, reqs, 1)

client := NewClient()
err = client.Connect(context.Background(), addr)
ctxReq, cancelReq := context.WithTimeout(context.Background(), 2*time.Second)
defer cancelReq()

client := NewTCPClient()
err = client.Connect(ctxReq, addr)
assert.NoError(t, err)

request := reqs[0]
resp, err := client.Do(context.Background(), request)
resp, err := client.Do(ctxReq, request)
assert.NoError(t, err)
assert.NotNil(t, resp)

received := <-receivedChan
assert.Equal(t, []byte{0, 0, 0, 6, 0, 3, 0, 18, 0, 4}, received[2:]) // trim transaction ID
select {
case received := <-receivedChan:
assert.Equal(t, []byte{0, 0, 0, 6, 0, 3, 0, 18, 0, 4}, received[2:]) // trim transaction ID
default:
t.Errorf("nothing received")
}

}

func TestBuilder_ReadHoldingRegistersRTU(t *testing.T) {
Expand All @@ -60,9 +65,6 @@ func TestBuilder_ReadHoldingRegistersRTU(t *testing.T) {

receivedChan := make(chan []byte, 1)
handler := func(received []byte, bytesRead int) (response []byte, closeConnection bool) {
if bytesRead == 0 {
return nil, false
}
receivedChan <- received
resp := packet.ReadHoldingRegistersResponseRTU{
ReadHoldingRegistersResponse: packet.ReadHoldingRegistersResponse{
Expand Down Expand Up @@ -103,9 +105,6 @@ func TestBuilder_ReadInputRegistersTCP(t *testing.T) {

receivedChan := make(chan []byte, 1)
handler := func(received []byte, bytesRead int) (response []byte, closeConnection bool) {
if bytesRead == 0 {
return nil, false
}
receivedChan <- received
resp := packet.ReadInputRegistersResponseTCP{
MBAPHeader: packet.MBAPHeader{TransactionID: 123, ProtocolID: 0},
Expand All @@ -128,7 +127,7 @@ func TestBuilder_ReadInputRegistersTCP(t *testing.T) {
assert.NoError(t, err)
assert.Len(t, reqs, 1)

client := NewClient()
client := NewTCPClient()
err = client.Connect(context.Background(), addr)
assert.NoError(t, err)

Expand All @@ -147,9 +146,6 @@ func TestBuilder_ReadInputRegistersRTU(t *testing.T) {

receivedChan := make(chan []byte, 1)
handler := func(received []byte, bytesRead int) (response []byte, closeConnection bool) {
if bytesRead == 0 {
return nil, false
}
receivedChan <- received
resp := packet.ReadInputRegistersResponseRTU{
ReadInputRegistersResponse: packet.ReadInputRegistersResponse{
Expand Down
110 changes: 53 additions & 57 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,22 @@ type ClientHooks interface {
BeforeParse(received []byte)
}

func defaultClient() *Client {
return &Client{
// ClientConfig is configuration for Client
type ClientConfig struct {
// WriteTimeout is total amount of time writing the request can take after client returns error
WriteTimeout time.Duration
// ReadTimeout is total amount of time reading the response can take before client returns error
ReadTimeout time.Duration

DialContextFunc func(ctx context.Context, address string) (net.Conn, error)
AsProtocolErrorFunc func(data []byte) error
ParseResponseFunc func(data []byte) (packet.Response, error)

Hooks ClientHooks
}

func defaultClient(conf ClientConfig) *Client {
c := &Client{
timeNow: time.Now,
writeTimeout: defaultWriteTimeout,
readTimeout: defaultReadTimeout,
Expand All @@ -75,75 +89,57 @@ func defaultClient() *Client {
asProtocolErrorFunc: packet.AsTCPErrorPacket,
parseResponseFunc: packet.ParseTCPResponse,
}
}

// NewTCPClient creates new instance of Modbus Client for Modbus TCP protocol
func NewTCPClient(opts ...ClientOptionFunc) *Client {
client := defaultClient()
for _, o := range opts {
o(client)
if conf.WriteTimeout > 0 {
c.writeTimeout = conf.WriteTimeout
}
return client
}

// NewRTUClient creates new instance of Modbus Client for Modbus RTU protocol
func NewRTUClient(opts ...ClientOptionFunc) *Client {
client := defaultClient()
client.asProtocolErrorFunc = packet.AsRTUErrorPacket
client.parseResponseFunc = packet.ParseRTUResponseWithCRC

for _, o := range opts {
o(client)
if conf.ReadTimeout > 0 {
c.readTimeout = conf.ReadTimeout
}
return client
}

// NewClient creates new instance of Modbus Client with given options
func NewClient(opts ...ClientOptionFunc) *Client {
client := defaultClient()
for _, o := range opts {
o(client)
if conf.DialContextFunc != nil {
c.dialContextFunc = conf.DialContextFunc
}
return client
if conf.AsProtocolErrorFunc != nil {
c.asProtocolErrorFunc = conf.AsProtocolErrorFunc
}
if conf.ParseResponseFunc != nil {
c.parseResponseFunc = conf.ParseResponseFunc
}
if conf.Hooks != nil {
c.hooks = conf.Hooks
}
return c
}

// ClientOptionFunc is options type for NewClient function
type ClientOptionFunc func(c *Client)

// WithProtocolErrorFunc is option to provide custom function for parsing error packet
func WithProtocolErrorFunc(errorFunc func(data []byte) error) func(c *Client) {
return func(c *Client) {
c.asProtocolErrorFunc = errorFunc
}
// NewTCPClient creates new instance of Modbus Client for Modbus TCP protocol
func NewTCPClient() *Client {
return NewTCPClientWithConfig(ClientConfig{})
}

// WithParseResponseFunc is option to provide custom function for parsing protocol packet
func WithParseResponseFunc(parseFunc func(data []byte) (packet.Response, error)) func(c *Client) {
return func(c *Client) {
c.parseResponseFunc = parseFunc
}
// NewTCPClientWithConfig creates new instance of Modbus Client for Modbus TCP protocol with given configuration options
func NewTCPClientWithConfig(conf ClientConfig) *Client {
client := defaultClient(conf)
client.asProtocolErrorFunc = packet.AsTCPErrorPacket
client.parseResponseFunc = packet.ParseTCPResponse
return client
}

// WithDialContextFunc is option to provide custom function for creating new connection
func WithDialContextFunc(dialContextFunc func(ctx context.Context, address string) (net.Conn, error)) func(c *Client) {
return func(c *Client) {
c.dialContextFunc = dialContextFunc
}
// NewRTUClient creates new instance of Modbus Client for Modbus RTU protocol
func NewRTUClient() *Client {
return NewRTUClientWithConfig(ClientConfig{})
}

// WithTimeouts is option to for setting writing packet or reading packet timeouts
func WithTimeouts(writeTimeout time.Duration, readTimeout time.Duration) func(c *Client) {
return func(c *Client) {
c.writeTimeout = writeTimeout
c.readTimeout = readTimeout
}
// NewRTUClientWithConfig creates new instance of Modbus Client for Modbus RTU protocol with given configuration options
func NewRTUClientWithConfig(conf ClientConfig) *Client {
client := defaultClient(conf)
client.asProtocolErrorFunc = packet.AsRTUErrorPacket
client.parseResponseFunc = packet.ParseRTUResponseWithCRC
return client
}

// WithHooks is option to set hooks in client
func WithHooks(logger ClientHooks) func(c *Client) {
return func(c *Client) {
c.hooks = logger
}
// NewClient creates new instance of Modbus Client with given configuration options
func NewClient(conf ClientConfig) *Client {
return defaultClient(conf)
}

// Connect opens network connection to Client to server. Context lifetime is only meant for this call.
Expand Down
16 changes: 10 additions & 6 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,14 @@ func (l *mockLogger) BeforeParse(received []byte) {

func TestWithOptions(t *testing.T) {
client := NewClient(
WithProtocolErrorFunc(packet.AsRTUErrorPacket),
WithParseResponseFunc(packet.ParseRTUResponse),
WithTimeouts(99*time.Second, 98*time.Second),
WithHooks(new(mockLogger)),
ClientConfig{
WriteTimeout: 99 * time.Second,
ReadTimeout: 98 * time.Second,
DialContextFunc: nil,
AsProtocolErrorFunc: packet.AsRTUErrorPacket,
ParseResponseFunc: packet.ParseRTUResponse,
Hooks: new(mockLogger),
},
)
assert.NotNil(t, client.asProtocolErrorFunc)
assert.NotNil(t, client.parseResponseFunc)
Expand Down Expand Up @@ -146,7 +150,7 @@ func TestClient_Do_receivePacketWith1Read(t *testing.T) {
logger.On("AfterEachRead", []byte{0x12, 0x34, 0x0, 0x0, 0x0, 0x5, 0x1, 0x1, 0x2, 0x0, 0x1}, 11, nil).Once()
logger.On("BeforeParse", []byte{0x12, 0x34, 0x0, 0x0, 0x0, 0x5, 0x1, 0x1, 0x2, 0x0, 0x1}).Once()

client := NewTCPClient(WithHooks(logger))
client := NewTCPClientWithConfig(ClientConfig{Hooks: logger})
client.conn = conn
client.timeNow = func() time.Time {
return exampleNow
Expand Down Expand Up @@ -475,7 +479,7 @@ func TestClient_Do_ReadMoreBytesThanPacketCanBe(t *testing.T) {
conn.On("Read", mock.Anything).
Return(tcpPacketMaxLen+1, nil)

client := NewClient()
client := NewClient(ClientConfig{})
client.conn = conn
client.timeNow = func() time.Time {
return exampleNow
Expand Down
Loading

0 comments on commit c30cd6a

Please sign in to comment.