Skip to content

Commit

Permalink
* Added packet.IsLikeModbusTCP() to check if given bytes are possib…
Browse files Browse the repository at this point in the history
…ly TCP packet or start of packet.

* Added `Parse*Request*` for every function type to help implement Modbus servers.
  • Loading branch information
aldas committed Aug 29, 2021
1 parent cb1e647 commit cb7b0c3
Show file tree
Hide file tree
Showing 29 changed files with 2,441 additions and 50 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ jobs:
test:
strategy:
matrix:
go-version: [ 1.16 ]
go-version: [ 1.16, 1.17 ]
platform: [ ubuntu-latest ]
runs-on: ${{ matrix.platform }}
steps:
Expand All @@ -23,7 +23,7 @@ jobs:
go test -race --coverprofile=coverage.coverprofile --covermode=atomic ./...
- name: Upload coverage to Codecov
if: success() && matrix.go-version == 1.16 && matrix.platform == 'ubuntu-latest'
if: success() && matrix.go-version == 1.17 && matrix.platform == 'ubuntu-latest'
uses: codecov/codecov-action@v1
with:
token: ${{ secrets.CODECOV_TOKEN }}
Expand Down
14 changes: 11 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.2] - unreleased

### Added

* Added `packet.IsLikeModbusTCP()` 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.

## [0.0.1] - unreleased
## [0.0.1] - 2021-04-11

### Added

Expand Down
4 changes: 1 addition & 3 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,7 @@ func NewRequestBuilder(serverAddress string, unitID uint8) *Builder {

// AddAll adds field into Builder. AddAll does not set ServerAddress and UnitID values.
func (b *Builder) AddAll(fields Fields) *Builder {
for _, f := range fields {
b.fields = append(b.fields, f)
}
b.fields = append(b.fields, fields...)
return b
}

Expand Down
1 change: 1 addition & 0 deletions builder_external_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func TestExternalUsage(t *testing.T) {

fields, err := req.ExtractFields(resp.(modbus.RegistersResponse), true)
assert.NotNil(t, resp)
assert.NoError(t, err)
assert.Len(t, fields, 2)

assert.Equal(t, uint16(1), fields[0].Value)
Expand Down
18 changes: 6 additions & 12 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,8 @@ func defaultClient() *Client {
// NewTCPClient creates new instance of Modbus Client for Modbus TCP protocol
func NewTCPClient(opts ...ClientOptionFunc) *Client {
client := defaultClient()
if opts != nil {
for _, o := range opts {
o(client)
}
for _, o := range opts {
o(client)
}
return client
}
Expand All @@ -94,21 +92,17 @@ func NewRTUClient(opts ...ClientOptionFunc) *Client {
client.asProtocolErrorFunc = packet.AsRTUErrorPacket
client.parseResponseFunc = packet.ParseRTUResponseWithCRC

if opts != nil {
for _, o := range opts {
o(client)
}
for _, o := range opts {
o(client)
}
return client
}

// NewClient creates new instance of Modbus Client with given options
func NewClient(opts ...ClientOptionFunc) *Client {
client := defaultClient()
if opts != nil {
for _, o := range opts {
o(client)
}
for _, o := range opts {
o(client)
}
return client
}
Expand Down
File renamed without changes.
117 changes: 92 additions & 25 deletions packet/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,37 @@ const (

const (
// FunctionReadCoils is function code for Read Coils (FC01)
FunctionReadCoils = uint8(1)
FunctionReadCoils = uint8(1) // 0x01
// FunctionReadDiscreteInputs is function code for Read Discrete Inputs (FC02)
FunctionReadDiscreteInputs = uint8(2)
FunctionReadDiscreteInputs = uint8(2) // 0x02
// FunctionReadHoldingRegisters is function code for Read Holding Registers (FC03)
FunctionReadHoldingRegisters = uint8(3)
FunctionReadHoldingRegisters = uint8(3) // 0x03
// FunctionReadInputRegisters is function code for Read Input Registers (FC04)
FunctionReadInputRegisters = uint8(4)
FunctionReadInputRegisters = uint8(4) // 0x04
// FunctionWriteSingleCoil is function code for Write Single Coil (FC05)
FunctionWriteSingleCoil = uint8(5)
FunctionWriteSingleCoil = uint8(5) // 0x05
// FunctionWriteSingleRegister is function code for Write Single Register (FC06)
FunctionWriteSingleRegister = uint8(6)
FunctionWriteSingleRegister = uint8(6) // 0x06
// FunctionWriteMultipleCoils is function code for Write Multiple Coils (FC15)
FunctionWriteMultipleCoils = uint8(15)
FunctionWriteMultipleCoils = uint8(15) // 0x0f
// FunctionWriteMultipleRegisters is function code for Write Multiple Registers (FC16)
FunctionWriteMultipleRegisters = uint8(16)
FunctionWriteMultipleRegisters = uint8(16) // 0x10
// FunctionReadWriteMultipleRegisters is function code for Read / Write Multiple Registers (FC23)
FunctionReadWriteMultipleRegisters = uint8(23)
FunctionReadWriteMultipleRegisters = uint8(23) // 0x17
)

var supportedFunctionCodes = [9]byte{
FunctionReadCoils,
FunctionReadDiscreteInputs,
FunctionReadHoldingRegisters,
FunctionReadInputRegisters,
FunctionWriteSingleCoil,
FunctionWriteSingleRegister,
FunctionWriteMultipleCoils,
FunctionWriteMultipleRegisters,
FunctionReadWriteMultipleRegisters,
}

// MBAPHeader (Modbus Application Header) is header part of modbus TCP packet. NB: this library does pack unitID into header
type MBAPHeader struct {
TransactionID uint16
Expand All @@ -46,22 +58,77 @@ func (h MBAPHeader) bytes(dst []byte, length uint16) {
binary.BigEndian.PutUint16(dst[4:6], length)
}

//// IsCompleteTCPPacket checks if packet is complete valid modbus TCP packet
//func IsCompleteTCPPacket(data []byte) bool {
// // minimal amount is 9 bytes (header + function code + 1 byte of something ala error code)
// packetLen := len(data)
// if packetLen < 9 {
// return false
// }
// // modbus header 6 bytes are = transaction id + protocol id + length of PDU part.
// // so adding these number is what complete packet would be
// expectedLength := int(6 + binary.BigEndian.Uint16(data[4:5]))
// if packetLen > expectedLength {
// return true // this is error situation. we received more bytes than length in packet indicates
// }
//
// return packetLen == expectedLength
//}
// ParseMBAPHeader parses MBAPHeader from given bytes
func ParseMBAPHeader(data []byte) (MBAPHeader, error) {
if len(data) < 6 {
return MBAPHeader{}, errors.New("data to short to contain MBAPHeader")
}
if data[2] != 0x0 || data[3] != 0x00 {
return MBAPHeader{}, errors.New("invalid protocol id")
}
pduLen := binary.BigEndian.Uint16(data[4:6])
if pduLen == 0 {
return MBAPHeader{}, errors.New("pdu length in header not be 0")
}
if len(data) != 6+int(pduLen) {
return MBAPHeader{}, errors.New("packet length does not match length in header")
}
return MBAPHeader{
TransactionID: binary.BigEndian.Uint16(data[0:2]),
ProtocolID: 0,
}, nil
}

// LooksLikeType is enum for classifying what given slice of bytes could potentially could be parsed to
type LooksLikeType int

const (
// DataTooShort is case when slice of bytes is too short to determine result
DataTooShort = iota
// IsNotTPCPacket is case when slice of bytes can not be Modbus TCP packet
IsNotTPCPacket
// LooksLikeTCPPacket is case when slice of bytes looks like Modbus TCP packet with supported function code
LooksLikeTCPPacket
// UnsupportedFunctionCode is case when slice of bytes looks like Modbus TCP packet but function code value is not supported
UnsupportedFunctionCode
)

// IsLikeModbusTCP checks if given data starts with bytes that could be potentially parsed as Modbus TCP packet.
func IsLikeModbusTCP(data []byte, allowUnSupportedFunctionCodes bool) (expectedLen int, looksLike LooksLikeType) {
// Example of first 8 bytes
// 0x81 0x80 - transaction id (0,1)
// 0x00 0x00 - protocol id (2,3)
// 0x00 0x06 - number of bytes in the message (PDU = ProtocolDataUnit) to follow (4,5)
// 0x10 - unit id (6)
// 0x01 - function code (7)

// minimal amount is 9 bytes (header + unit id + function code + 1 byte of something ala error code)
if len(data) < 9 {
return 0, DataTooShort
}
if !(data[2] == 0x0 && data[3] == 0x0) { // check protocol id
return 0, IsNotTPCPacket
}
pduLen := binary.BigEndian.Uint16(data[4:6]) // number of bytes in the message to follow
if pduLen < 3 { // every request is more than 2 bytes of PDU
return 0, IsNotTPCPacket
}
functionCode := data[7] // function code
if functionCode == 0 {
return 0, IsNotTPCPacket
}
expectedLen = int(pduLen) + 6
if allowUnSupportedFunctionCodes {
return expectedLen, LooksLikeTCPPacket
}
for _, fc := range supportedFunctionCodes {
if fc == functionCode {
return expectedLen, LooksLikeTCPPacket
}
}
return expectedLen, UnsupportedFunctionCode
}

//
//// IsCompleteRTUPacket checks if packet is complete valid modbus RTU packet
//func IsCompleteRTUPacket(data []byte) bool {
Expand Down
149 changes: 149 additions & 0 deletions packet/packet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,155 @@ import (
"testing"
)

func TestParseMBAPHeader(t *testing.T) {
var testCases = []struct {
name string
when []byte
expect MBAPHeader
expectError string
}{
{
name: "ok, ErrorResponseTCP (code=3)",
when: []byte{0x81, 0x80, 0x0, 0x0, 0x0, 0x3, 0x1, 0x82, 0x3},
expect: MBAPHeader{
TransactionID: 33152,
ProtocolID: 0,
},
},
{
name: "ok, ReadCoilsRequestTCP (fc1)",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10, 0x01, 0x00, 0x6B, 0x00, 0x03},
expect: MBAPHeader{
TransactionID: 258,
ProtocolID: 0,
},
},
{
name: "ok, ReadWriteMultipleRegistersResponseTCP (fc23)",
when: []byte{0x81, 0x80, 0x00, 0x00, 0x00, 0x05, 0x03, 0x17, 0x02, 0xCD, 0x6B},
expect: MBAPHeader{
TransactionID: 33152,
ProtocolID: 0,
},
},
{
name: "nok, data to short to contain MBAPHeader",
when: []byte{0x81, 0x80, 0x00, 0x00, 0x00},
expectError: "data to short to contain MBAPHeader",
},
{
name: "nok, data to short to contain MBAPHeader",
when: []byte{0x81, 0x80, 0x00, 0x01, 0x00, 0x00},
expectError: "invalid protocol id",
},
{
name: "nok, pdu length in header not be 0",
when: []byte{0x81, 0x80, 0x00, 0x00, 0x00, 0x00},
expectError: "pdu length in header not be 0",
},
{
name: "nok, packet length does not match length in header",
when: []byte{0x81, 0x80, 0x00, 0x00, 0x00, 0x02, 0xff},
expectError: "packet length does not match length in header",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, err := ParseMBAPHeader(tc.when)

assert.Equal(t, tc.expect, result)
if tc.expectError != "" {
assert.EqualError(t, err, tc.expectError)
} else {
assert.NoError(t, err)
}
})
}
}

func TestIsLikeModbusTCP(t *testing.T) {
var testCases = []struct {
name string
when []byte
whenAllowUnsupportedFC bool
expectLength int
expectLooksLike LooksLikeType
}{
{
name: "ok, full packet",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10, 0x01, 0x00, 0x6B, 0x00, 0x03},
expectLength: 12,
expectLooksLike: LooksLikeTCPPacket,
},
{
name: "ok, fragment of packet",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10, 0x01, 0x00},
expectLength: 12,
expectLooksLike: LooksLikeTCPPacket,
},
{
name: "nok, ErrorResponseTCP (code=3)",
when: []byte{0x81, 0x80, 0x0, 0x0, 0x0, 0x3, 0x1, 0x82, 0x3},
expectLength: 9,
expectLooksLike: UnsupportedFunctionCode,
},
{
name: "nok, too few bytes",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10, 0x01},
expectLength: 0,
expectLooksLike: DataTooShort,
},
{
name: "nok, invalid packet id, 1",
when: []byte{0x01, 0x02, 0x01 /* 0x00 */, 0x00, 0x00, 0x06, 0x10, 0x01, 0x00, 0x6B, 0x00, 0x03},
expectLength: 0,
expectLooksLike: IsNotTPCPacket,
},
{
name: "nok, invalid packet id, 2",
when: []byte{0x01, 0x02, 0x00, 0x01 /* 0x00 */, 0x00, 0x06, 0x10, 0x01, 0x00, 0x6B, 0x00, 0x03},
expectLength: 0,
expectLooksLike: IsNotTPCPacket,
},
{
name: "nok, pdu too short",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x02 /* 0x04+ */, 0x10, 0x01, 0x00, 0x6B, 0x00, 0x03},
expectLength: 0,
expectLooksLike: IsNotTPCPacket,
},
{
name: "nok, function code = 0",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10, 0x00 /* 0x01 */, 0x00, 0x6B, 0x00, 0x03},
expectLength: 0,
expectLooksLike: IsNotTPCPacket,
},
{
name: "ok, allow unsupported function code = 1F",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10, 0x1f /* 0x01 */, 0x00, 0x6B, 0x00, 0x03},
whenAllowUnsupportedFC: true,
expectLength: 12,
expectLooksLike: LooksLikeTCPPacket,
},
{
name: "ok, unsupported function code = 1F",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10, 0x1f /* 0x01 */, 0x00, 0x6B, 0x00, 0x03},
whenAllowUnsupportedFC: false,
expectLength: 12,
expectLooksLike: UnsupportedFunctionCode,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expectedLen, looksLike := IsLikeModbusTCP(tc.when, tc.whenAllowUnsupportedFC)

assert.Equal(t, tc.expectLength, expectedLen)
assert.Equal(t, tc.expectLooksLike, looksLike)
})
}
}

func TestCRC16(t *testing.T) {
var testCases = []struct {
name string
Expand Down
Loading

0 comments on commit cb7b0c3

Please sign in to comment.