Skip to content
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

Add FC17 Read Server ID (0x11) #11

Merged
merged 1 commit into from
Jan 7, 2024
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
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
Loading