Skip to content

Commit

Permalink
Parsing requests returns errors that can be sent as bytes
Browse files Browse the repository at this point in the history
  • Loading branch information
aldas committed Aug 29, 2021
1 parent cb7b0c3 commit c41cbf4
Show file tree
Hide file tree
Showing 12 changed files with 367 additions and 134 deletions.
179 changes: 127 additions & 52 deletions packet/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,74 +5,121 @@ import (
"fmt"
)

// ErrCode is enumeration for response error codes
type ErrCode uint8

const (
// ErrUnknown is catchall error code
ErrUnknown = 0
// ErrIllegalFunction is The function code received in the query is not an allowable action for the server.
// This may be because the function code is only applicable to newer devices, and was not implemented in the
// unit selected. It could also indicate that the server is in the wrong state to process a request of this
// type, for example because it is unconfigured and is being asked to return register values.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
ErrIllegalFunction = 1
// ErrIllegalDataAddress is The data address received in the query is not an allowable address for the server.
// More specifically, the combination of reference number and transfer length is invalid. For a controller with 100
// registers, the PDU addresses the first register as 0, and the last one as 99. If a request is submitted with a
// starting register address of 96 and a quantity of registers of 4, then this request will successfully
// operate (address-wise at least) on registers 96, 97, 98, 99. If a request is submitted with a starting
// register address of 96 and a quantity of registers of 5, then this request will fail with Exception
// Code 0x02 “Illegal Data Address” since it attempts to operate on registers 96, 97, 98, 99 and 100, and
// there is no register with address 100.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
ErrIllegalDataAddress = 2
// ErrIllegalDataValue is A value contained in the query data field is not an allowable value for server.
// This indicates a fault in the structure of the remainder of a complex request, such as that the implied length
// is incorrect. It specifically does NOT mean that a data item submitted for storage in a register has a value
// outside the expectation of the application program, since the MODBUS protocol is unaware of the significance of
// any particular value of any particular register.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
ErrIllegalDataValue = 3
// ErrServerFailure is An unrecoverable error occurred while the server was attempting to perform the requested action.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
ErrServerFailure = 4
// ErrAcknowledge is Specialized use in conjunction with programming commands. The server has accepted the request
// and is processing it, but a long duration of time will be required to do so. This response is returned to prevent
// a timeout error from occurring in the client. The client can next issue a Poll Program Complete message to
// determine if processing is completed.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
ErrAcknowledge = 5
// ErrServerBusy is Specialized use in conjunction with programming commands. The server is engaged in processing a
// long duration program command. The client should retransmit the message later when the server is free.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
ErrServerBusy = 6
// ErrMemoryParityError is Specialized use in conjunction with function codes 20 and 21 and reference type 6, to
// indicate that the extended file area failed to pass a consistency check.
// The server attempted to read record file, but detected a parity error in the memory. The client can retry
// the request, but service may be required on the server device.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
ErrMemoryParityError = 8
// ErrGatewayPathUnavailable is Specialized use in conjunction with gateways, indicates that the gateway was unable
// to allocate an internal communication path from the input port to the output port for processing the request.
// Usually means that the gateway is misconfigured or overloaded.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 49
ErrGatewayPathUnavailable = 10
// ErrGatewayTargetedDeviceResponse is Specialized use in conjunction with gateways, indicates that no response was
// obtained from the target device. Usually means that the device is not present on the network.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 49
ErrGatewayTargetedDeviceResponse = 11
)

func errorText(code uint8) string {
switch code {
case 1:
// The function code received in the query is not an allowable action for the server. This may be
// because the function code is only applicable to newer devices, and was not implemented in the
// unit selected. It could also indicate that the server is in the wrong state to process a request of this
// type, for example because it is unconfigured and is being asked to return register values.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
case ErrIllegalFunction:
return "Illegal function"
case 2:
// The data address received in the query is not an allowable address for the server. More specifically,
// the combination of reference number and transfer length is invalid. For a controller with 100 registers,
// the PDU addresses the first register as 0, and the last one as 99. If a request is submitted with a
// starting register address of 96 and a quantity of registers of 4, then this request will successfully
// operate (address-wise at least) on registers 96, 97, 98, 99. If a request is submitted with a starting
// register address of 96 and a quantity of registers of 5, then this request will fail with Exception
// Code 0x02 “Illegal Data Address” since it attempts to operate on registers 96, 97, 98, 99 and 100, and
// there is no register with address 100.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
case ErrIllegalDataAddress:
return "Illegal data address"
case 3:
// A value contained in the query data field is not an allowable value for server. This indicates a fault in
// the structure of the remainder of a complex request, such as that the implied length is incorrect.
// It specifically does NOT mean that a data item submitted for storage in a register has a value outside
// the expectation of the application program, since the MODBUS protocol is unaware of the significance of any
// particular value of any particular register.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
case ErrIllegalDataValue:
return "Illegal data value"
case 4:
// An unrecoverable error occurred while the server was attempting to perform the requested action.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
case ErrServerFailure:
return "Server failure"
case 5:
// Specialized use in conjunction with programming commands.
// The server has accepted the request and is processing it, but a long duration of time will be required to
// do so. This response is returned to prevent a timeout error from occurring in the client. The client can
// next issue a Poll Program Complete message to determine if processing is completed.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
case ErrAcknowledge:
return "Acknowledge"
case 6:
// Specialized use in conjunction with programming commands.
// The server is engaged in processing a long duration program command. The client should retransmit the
// message later when the server is free.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
case ErrServerBusy:
return "Server busy"
case 8:
// Specialized use in conjunction with function codes 20 and 21 and reference type 6, to indicate that the
// extended file area failed to pass a consistency check.
// The server attempted to read record file, but detected a parity error in the memory. The client can retry
// the request, but service may be required on the server device.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 48
case ErrMemoryParityError:
return "Memory parity error"
case 10:
// Specialized use in conjunction with gateways, indicates that the gateway was unable to allocate an internal
// communication path from the input port to the output port for processing the request. Usually means that
// the gateway is misconfigured or overloaded.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 49
case ErrGatewayPathUnavailable:
return "Gateway path unavailable"
case 11:
// Specialized use in conjunction with gateways, indicates that no response was obtained from the target device.
// Usually means that the device is not present on the network.
// Quote from: `MODBUS Application Protocol Specification V1.1b3`, page 49
case ErrGatewayTargetedDeviceResponse:
return "Gateway targeted device failed to respond"
case ErrUnknown:
fallthrough
default:
return fmt.Sprintf("Unknown error code: %v", code)
}
}

// NewErrorParseTCP creates new instance of parsing error that can be sent to the client
func NewErrorParseTCP(code uint8, message string) *ErrorParseTCP {
return &ErrorParseTCP{
Message: message,
Packet: ErrorResponseTCP{
TransactionID: 0,
UnitID: 0,
Function: 0,
Code: code,
},
}
}

// ErrorParseTCP is parsing error that can be sent to the client
type ErrorParseTCP struct {
Message string
Packet ErrorResponseTCP
}

// Error translates error code to error message.
func (e ErrorParseTCP) Error() string {
return e.Message
}

// Bytes returns ErrorParseTCP packet as bytes form
func (e ErrorParseTCP) Bytes() []byte {
return e.Packet.Bytes()
}

// ErrorResponseTCP is TCP error response send by server to client
type ErrorResponseTCP struct {
TransactionID uint16
Expand Down Expand Up @@ -105,6 +152,34 @@ func (re ErrorResponseTCP) FunctionCode() uint8 {
return re.Function
}

// NewErrorParseRTU creates new instance of parsing error that can be sent to the client
func NewErrorParseRTU(code uint8, message string) *ErrorParseRTU {
return &ErrorParseRTU{
Message: message,
Packet: ErrorResponseRTU{
UnitID: 0,
Function: 0,
Code: code,
},
}
}

// ErrorParseRTU is parsing error that can be sent to the client
type ErrorParseRTU struct {
Message string
Packet ErrorResponseRTU
}

// Error translates error code to error message.
func (e ErrorParseRTU) Error() string {
return e.Message
}

// Bytes returns ErrorParseRTU packet as bytes form
func (e ErrorParseRTU) Bytes() []byte {
return e.Packet.Bytes()
}

// ErrorResponseRTU is RTU error response send by server to client
type ErrorResponseRTU struct {
UnitID uint8
Expand Down
8 changes: 4 additions & 4 deletions packet/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,17 @@ func (h MBAPHeader) bytes(dst []byte, length uint16) {
// 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")
return MBAPHeader{}, NewErrorParseTCP(ErrServerFailure, "data to short to contain MBAPHeader")
}
if data[2] != 0x0 || data[3] != 0x00 {
return MBAPHeader{}, errors.New("invalid protocol id")
return MBAPHeader{}, NewErrorParseTCP(ErrServerFailure, "invalid protocol id")
}
pduLen := binary.BigEndian.Uint16(data[4:6])
if pduLen == 0 {
return MBAPHeader{}, errors.New("pdu length in header not be 0")
return MBAPHeader{}, NewErrorParseTCP(ErrServerFailure, "pdu length in header can not be 0")
}
if len(data) != 6+int(pduLen) {
return MBAPHeader{}, errors.New("packet length does not match length in header")
return MBAPHeader{}, NewErrorParseTCP(ErrServerFailure, "packet length does not match length in header")
}
return MBAPHeader{
TransactionID: binary.BigEndian.Uint16(data[0:2]),
Expand Down
4 changes: 2 additions & 2 deletions packet/packet_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ func TestParseMBAPHeader(t *testing.T) {
expectError: "invalid protocol id",
},
{
name: "nok, pdu length in header not be 0",
name: "nok, pdu length in header can not be 0",
when: []byte{0x81, 0x80, 0x00, 0x00, 0x00, 0x00},
expectError: "pdu length in header not be 0",
expectError: "pdu length in header can not be 0",
},
{
name: "nok, packet length does not match length in header",
Expand Down
31 changes: 23 additions & 8 deletions packet/readcoilsrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package packet

import (
"encoding/binary"
"errors"
"fmt"
"math"
"math/rand"
Expand Down Expand Up @@ -84,17 +83,26 @@ func ParseReadCoilsRequestTCP(data []byte) (*ReadCoilsRequestTCP, error) {
if err != nil {
return nil, err
}
unitID := data[6]
if data[7] != FunctionReadCoils {
return nil, errors.New("received function code in packet is not 0x01")
tmpErr := NewErrorParseTCP(ErrIllegalFunction, "received function code in packet is not 0x01")
tmpErr.Packet.TransactionID = header.TransactionID
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadCoils
return nil, tmpErr
}
quantity := binary.BigEndian.Uint16(data[10:12])
if !(quantity >= 1 && quantity <= 125) { // 0x0001 to 0x007D
return nil, errors.New("invalid quantity. valid range 1..125")
tmpErr := NewErrorParseTCP(ErrIllegalDataValue, "invalid quantity. valid range 1..125")
tmpErr.Packet.TransactionID = header.TransactionID
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadCoils
return nil, tmpErr
}
return &ReadCoilsRequestTCP{
MBAPHeader: header,
ReadCoilsRequest: ReadCoilsRequest{
UnitID: data[6],
UnitID: unitID,
// function code = data[7]
StartAddress: binary.BigEndian.Uint16(data[8:10]),
Quantity: quantity,
Expand Down Expand Up @@ -140,18 +148,25 @@ func (r ReadCoilsRequestRTU) ExpectedResponseLength() int {
func ParseReadCoilsRequestRTU(data []byte) (*ReadCoilsRequestRTU, error) {
dLen := len(data)
if dLen != 8 && dLen != 6 { // with or without CRC bytes
return nil, errors.New("invalid data length to be valid packet")
return nil, NewErrorParseRTU(ErrServerFailure, "invalid data length to be valid packet")
}
unitID := data[0]
if data[1] != FunctionReadCoils {
return nil, errors.New("received function code in packet is not 0x01")
tmpErr := NewErrorParseRTU(ErrIllegalFunction, "received function code in packet is not 0x01")
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadCoils
return nil, tmpErr
}
quantity := binary.BigEndian.Uint16(data[4:6])
if !(quantity >= 1 && quantity <= 125) { // 0x0001 to 0x007D
return nil, errors.New("invalid quantity. valid range 1..125")
tmpErr := NewErrorParseRTU(ErrIllegalDataValue, "invalid quantity. valid range 1..125")
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadCoils
return nil, tmpErr
}
return &ReadCoilsRequestRTU{
ReadCoilsRequest: ReadCoilsRequest{
UnitID: data[0],
UnitID: unitID,
// function code = data[1]
StartAddress: binary.BigEndian.Uint16(data[2:4]),
Quantity: quantity,
Expand Down
31 changes: 23 additions & 8 deletions packet/readdiscreteinputsrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package packet

import (
"encoding/binary"
"errors"
"fmt"
"math"
"math/rand"
Expand Down Expand Up @@ -84,17 +83,26 @@ func ParseReadDiscreteInputsRequestTCP(data []byte) (*ReadDiscreteInputsRequestT
if err != nil {
return nil, err
}
unitID := data[6]
if data[7] != FunctionReadDiscreteInputs {
return nil, errors.New("received function code in packet is not 0x02")
tmpErr := NewErrorParseTCP(ErrIllegalFunction, "received function code in packet is not 0x02")
tmpErr.Packet.TransactionID = header.TransactionID
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadDiscreteInputs
return nil, tmpErr
}
quantity := binary.BigEndian.Uint16(data[10:12])
if !(quantity >= 1 && quantity <= 125) { // 0x0001 to 0x007D
return nil, errors.New("invalid quantity. valid range 1..125")
tmpErr := NewErrorParseTCP(ErrIllegalDataValue, "invalid quantity. valid range 1..125")
tmpErr.Packet.TransactionID = header.TransactionID
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadDiscreteInputs
return nil, tmpErr
}
return &ReadDiscreteInputsRequestTCP{
MBAPHeader: header,
ReadDiscreteInputsRequest: ReadDiscreteInputsRequest{
UnitID: data[6],
UnitID: unitID,
// function code = data[7]
StartAddress: binary.BigEndian.Uint16(data[8:10]),
Quantity: quantity,
Expand Down Expand Up @@ -139,18 +147,25 @@ func (r ReadDiscreteInputsRequestRTU) ExpectedResponseLength() int {
func ParseReadDiscreteInputsRequestRTU(data []byte) (*ReadDiscreteInputsRequestRTU, error) {
dLen := len(data)
if dLen != 8 && dLen != 6 { // with or without CRC bytes
return nil, errors.New("invalid data length to be valid packet")
return nil, NewErrorParseRTU(ErrServerFailure, "invalid data length to be valid packet")
}
unitID := data[0]
if data[1] != FunctionReadDiscreteInputs {
return nil, errors.New("received function code in packet is not 0x02")
tmpErr := NewErrorParseRTU(ErrIllegalFunction, "received function code in packet is not 0x02")
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadDiscreteInputs
return nil, tmpErr
}
quantity := binary.BigEndian.Uint16(data[4:6])
if !(quantity >= 1 && quantity <= 125) { // 0x0001 to 0x007D
return nil, errors.New("invalid quantity. valid range 1..125")
tmpErr := NewErrorParseRTU(ErrIllegalDataValue, "invalid quantity. valid range 1..125")
tmpErr.Packet.UnitID = unitID
tmpErr.Packet.Function = FunctionReadDiscreteInputs
return nil, tmpErr
}
return &ReadDiscreteInputsRequestRTU{
ReadDiscreteInputsRequest: ReadDiscreteInputsRequest{
UnitID: data[0],
UnitID: unitID,
// function code = data[1]
StartAddress: binary.BigEndian.Uint16(data[2:4]),
Quantity: quantity,
Expand Down
Loading

0 comments on commit c41cbf4

Please sign in to comment.