Skip to content

Commit

Permalink
Add FC17 Read Server ID (0x11)
Browse files Browse the repository at this point in the history
  • Loading branch information
aldas committed Jan 7, 2024
1 parent c9fbd97 commit 12b8e62
Show file tree
Hide file tree
Showing 14 changed files with 989 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

### Added

* Added support for FC17 (0x11) Read Server ID.
* 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
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ vet: ## Vet the files
test: ## Run unittests
@go test -short ${PKG_LIST}

goversion ?= "1.20"
test_version: ## Run tests inside Docker with given version (defaults to 1.20). Example: make test_version goversion=1.20
goversion ?= "1.21"
test_version: ## Run tests inside Docker with given version (defaults to 1.21). Example: make test_version goversion=1.21
@docker run --rm -it -v $(shell pwd):/project golang:$(goversion) /bin/sh -c "cd /project && make init check"

race: ## Run data race detector
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ go get github.com/aldas/go-modbus-client
* FC6 - Write Single Register ([req](packet/writesingleregisterrequest.go)/[resp](packet/writesingleregisterresponse.go))
* FC15 - Write Multiple Coils ([req](packet/writemultiplecoilsrequest.go)/[resp](packet/writemultiplecoilsresponse.go))
* FC16 - Write Multiple Registers ([req](packet/writemultipleregistersrequest.go)/[resp](packet/writemultipleregistersresponse.go))
* FC17 - Read Server ID ([req](packet/readserveridrequest.go)/[resp](packet/readserveridresponse.go))
* FC23 - Read / Write Multiple Registers ([req](packet/readwritemultipleregistersrequest.go)/[resp](packet/readwritemultipleregistersresponse.go))

## Goals
Expand Down Expand Up @@ -118,6 +119,7 @@ req, err := packet.NewReadInputRegistersRequestTCP(0, 10, 9)
req, err := packet.NewWriteSingleCoilRequestTCP(0, 10, true)
req, err := packet.NewWriteSingleRegisterRequestTCP(0, 10, []byte{0xCA, 0xFE})
req, err := packet.NewWriteMultipleCoilsRequestTCP(0, 10, []bool{true, false, true})
req, err := packet.NewReadServerIDRequestTCP(0)
req, err := packet.NewWriteMultipleRegistersRequestTCP(0, 10, []byte{0xCA, 0xFE, 0xBA, 0xBE})
```

Expand Down
13 changes: 8 additions & 5 deletions packet/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ const (
FunctionWriteMultipleCoils = uint8(15) // 0x0f
// FunctionWriteMultipleRegisters is function code for Write Multiple Registers (FC16)
FunctionWriteMultipleRegisters = uint8(16) // 0x10
// FunctionReadServerID is function code for Read Server ID (FC16)
FunctionReadServerID = uint8(17) // 0x11
// FunctionReadWriteMultipleRegisters is function code for Read / Write Multiple Registers (FC23)
FunctionReadWriteMultipleRegisters = uint8(23) // 0x17
)

var supportedFunctionCodes = [9]byte{
var supportedFunctionCodes = [10]byte{
FunctionReadCoils,
FunctionReadDiscreteInputs,
FunctionReadHoldingRegisters,
Expand All @@ -43,6 +45,7 @@ var supportedFunctionCodes = [9]byte{
FunctionWriteSingleRegister,
FunctionWriteMultipleCoils,
FunctionWriteMultipleRegisters,
FunctionReadServerID,
FunctionReadWriteMultipleRegisters,
}

Expand Down Expand Up @@ -105,12 +108,12 @@ func LooksLikeModbusTCP(data []byte, allowUnSupportedFunctionCodes bool) (expect
// 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)
// 0x00 0x02 - number of bytes in the message (PDU = ProtocolDataUnit) to follow (4,5)
// 0x10 - unit id (6)
// 0x01 - function code (7)
// 0x11 - function code (7)

// minimal amount is 9 bytes (header + unit id + function code + 1 byte of something ala error code)
if len(data) < 9 {
// minimal amount is 8 bytes (header + unit id + function code)
if len(data) < 8 {
return 0, ErrTCPDataTooShort
}
if !(data[2] == 0x0 && data[3] == 0x0) { // check protocol id
Expand Down
2 changes: 1 addition & 1 deletion packet/packet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func TestLooksLikeModbusTCP(t *testing.T) {
},
{
name: "nok, too few bytes",
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10, 0x01},
when: []byte{0x01, 0x02, 0x00, 0x00, 0x00, 0x06, 0x10},
expectLength: 0,
expectError: "data is too short to be a Modbus TCP packet",
},
Expand Down
145 changes: 145 additions & 0 deletions packet/readserveridrequest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package packet

import (
"math/rand"
)

// ReadServerIDRequestTCP is TCP Request for Read Server ID function (FC=17, 0x11)
//
// Example packet: 0x81 0x80 0x00 0x00 0x00 0x02 0x10 0x11
// 0x81 0x80 - transaction id (0,1)
// 0x00 0x00 - protocol id (2,3)
// 0x00 0x02 - number of bytes in the message (PDU = ProtocolDataUnit) to follow (4,5)
// 0x10 - unit id (6)
// 0x11 - function code (7)
type ReadServerIDRequestTCP struct {
MBAPHeader
ReadServerIDRequest
}

// ReadServerIDRequestRTU is RTU Request for Read Server ID function (FC=17, 0x11)
//
// Example packet: 0x10 0x11 0xcc 0x7c
// 0x10 - unit id (0)
// 0x11 - function code (1)
// 0xcc 0x7c - CRC16 (6,7)
type ReadServerIDRequestRTU struct {
ReadServerIDRequest
}

// ReadServerIDRequest is Request for Read Server ID function (FC=17, 0x11)
type ReadServerIDRequest struct {
UnitID uint8
}

// NewReadServerIDRequestTCP creates new instance of Read Server ID TCP request
func NewReadServerIDRequestTCP(unitID uint8) (*ReadServerIDRequestTCP, error) {
return &ReadServerIDRequestTCP{
MBAPHeader: MBAPHeader{
TransactionID: uint16(1 + rand.Intn(65534)),
ProtocolID: 0,
},
ReadServerIDRequest: ReadServerIDRequest{
UnitID: unitID,
},
}, nil
}

// Bytes returns ReadServerIDRequestTCP packet as bytes form
func (r ReadServerIDRequestTCP) Bytes() []byte {
length := uint16(2)
result := make([]byte, tcpMBAPHeaderLen+length)
r.MBAPHeader.bytes(result[0:6], length)
r.ReadServerIDRequest.bytes(result[6 : 6+length])
return result
}

// ExpectedResponseLength returns length of bytes that valid response to this request would be
func (r ReadServerIDRequestTCP) ExpectedResponseLength() int {
// response = 6 header len + 1 unitID + 1 fc
return 6 + 2
}

// ParseReadServerIDRequestTCP parses given bytes into ReadServerIDRequestTCP
func ParseReadServerIDRequestTCP(data []byte) (*ReadServerIDRequestTCP, error) {
header, err := ParseMBAPHeader(data)
if err != nil {
return nil, err
}
unitID := data[6]
if data[7] != FunctionReadServerID {
tmpErr := NewErrorParseTCP(ErrIllegalFunction, "received function code in packet is not 0x11")
tmpErr.Packet.TransactionID = header.TransactionID
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadServerID
return nil, tmpErr
}
return &ReadServerIDRequestTCP{
MBAPHeader: header,
ReadServerIDRequest: ReadServerIDRequest{
UnitID: unitID,
},
}, nil
}

// NewReadServerIDRequestRTU creates new instance of Read Server ID RTU request
func NewReadServerIDRequestRTU(unitID uint8) (*ReadServerIDRequestRTU, error) {
return &ReadServerIDRequestRTU{
ReadServerIDRequest: ReadServerIDRequest{
UnitID: unitID,
},
}, nil
}

// Bytes returns ReadServerIDRequestRTU packet as bytes form
func (r ReadServerIDRequestRTU) Bytes() []byte {
result := make([]byte, 2+2)
bytes := r.ReadServerIDRequest.bytes(result)
crc := CRC16(bytes[:2])
result[2] = uint8(crc)
result[3] = uint8(crc >> 8)
return result
}

// ExpectedResponseLength returns length of bytes that valid response to this request would be
func (r ReadServerIDRequestRTU) ExpectedResponseLength() int {
// response = 1 UnitID + 1 functionCode
return 2
}

// ParseReadServerIDRequestRTU parses given bytes into ReadServerIDRequestRTU
// Does not check CRC
func ParseReadServerIDRequestRTU(data []byte) (*ReadServerIDRequestRTU, error) {
dLen := len(data)
if dLen != 4 && dLen != 2 { // with or without CRC bytes
return nil, NewErrorParseRTU(ErrServerFailure, "invalid data length to be valid packet")
}
unitID := data[0]
if data[1] != FunctionReadServerID {
tmpErr := NewErrorParseRTU(ErrIllegalFunction, "received function code in packet is not 0x11")
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadServerID
return nil, tmpErr
}
return &ReadServerIDRequestRTU{
ReadServerIDRequest: ReadServerIDRequest{
UnitID: unitID,
},
}, nil
}

// FunctionCode returns function code of this request
func (r ReadServerIDRequest) FunctionCode() uint8 {
return FunctionReadServerID
}

// Bytes returns ReadServerIDRequest packet as bytes form
func (r ReadServerIDRequest) Bytes() []byte {
return r.bytes(make([]byte, 2))
}

func (r ReadServerIDRequest) bytes(bytes []byte) []byte {
bytes[0] = r.UnitID
bytes[1] = FunctionReadServerID
return bytes
}
Loading

0 comments on commit 12b8e62

Please sign in to comment.