Skip to content

Commit

Permalink
refactor Field to ease Builder usage.
Browse files Browse the repository at this point in the history
  • Loading branch information
aldas committed Mar 22, 2021
1 parent 5876b39 commit 922276e
Show file tree
Hide file tree
Showing 12 changed files with 784 additions and 339 deletions.
422 changes: 265 additions & 157 deletions builder.go

Large diffs are not rendered by default.

483 changes: 387 additions & 96 deletions builder_test.go

Large diffs are not rendered by default.

7 changes: 0 additions & 7 deletions packet/packet.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,6 @@ const (
FunctionReadWriteMultipleRegisters = uint8(23)
)

// Modbus ASCII ??

// https://rakyll.org/style-packages/

// https://www.simplymodbus.ca/FAQ.htm
// https://en.wikipedia.org/wiki/Modbus

// 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 Down
12 changes: 6 additions & 6 deletions packet/readcoilsrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ type ReadCoilsRequest struct {

// NewReadCoilsRequestTCP creates new instance of Read Coils TCP request
func NewReadCoilsRequestTCP(unitID uint8, startAddress uint16, quantity uint16) (*ReadCoilsRequestTCP, error) {
if quantity == 0 || quantity > 2048 {
// 2048 coils is due that in response data size field is 1 byte so max 256*8=2048 coils can be returned
return nil, fmt.Errorf("quantity is out of range (1-2048): %v", quantity)
if quantity == 0 || quantity > 2000 {
// 2000 coils is due that in response data size field is 1 byte so max 250*8=2000 coils can be returned
return nil, fmt.Errorf("quantity is out of range (1-2000): %v", quantity)
}

return &ReadCoilsRequestTCP{
Expand Down Expand Up @@ -78,9 +78,9 @@ func (r ReadCoilsRequestTCP) ExpectedResponseLength() int {

// NewReadCoilsRequestRTU creates new instance of Read Coils RTU request
func NewReadCoilsRequestRTU(unitID uint8, startAddress uint16, quantity uint16) (*ReadCoilsRequestRTU, error) {
if quantity == 0 || quantity > 2048 {
// 2048 coils is due that in response data size field is 1 byte so max 256*8=2048 coils can be returned
return nil, fmt.Errorf("quantity is out of range (1-2048): %v", quantity)
if quantity == 0 || quantity > 2000 {
// 2000 coils is due that in response data size field is 1 byte so max 250*8=2000 coils can be returned
return nil, fmt.Errorf("quantity is out of range (1-2000): %v", quantity)
}

return &ReadCoilsRequestRTU{
Expand Down
8 changes: 4 additions & 4 deletions packet/readcoilsrequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ func TestNewReadCoilsRequestTCP(t *testing.T) {
name: "nok, quantity too big",
whenUnitID: 1,
whenStartAddress: 200,
whenQuantity: 2048 + 1,
whenQuantity: 2000 + 1,
expect: nil,
expectError: "quantity is out of range (1-2048): 2049",
expectError: "quantity is out of range (1-2000): 2001",
},
}

Expand Down Expand Up @@ -186,9 +186,9 @@ func TestNewReadCoilsRequestRTU(t *testing.T) {
name: "nok, quantity too big",
whenUnitID: 1,
whenStartAddress: 200,
whenQuantity: 2048 + 1,
whenQuantity: 2000 + 1,
expect: nil,
expectError: "quantity is out of range (1-2048): 2049",
expectError: "quantity is out of range (1-2000): 2001",
},
}

Expand Down
12 changes: 6 additions & 6 deletions packet/readdiscreteinputsrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ type ReadDiscreteInputsRequest struct {

// NewReadDiscreteInputsRequestTCP creates new instance of Read Discrete Inputs TCP request
func NewReadDiscreteInputsRequestTCP(unitID uint8, startAddress uint16, quantity uint16) (*ReadDiscreteInputsRequestTCP, error) {
if quantity == 0 || quantity > 2048 {
// 2048 coils is due that in response data size field is 1 byte so max 256*8=2048 coils can be returned
return nil, fmt.Errorf("quantity is out of range (1-2048): %v", quantity)
if quantity == 0 || quantity > 2000 {
// 2000 coils is due that in response data size field is 1 byte so max 250*8=2000 coils can be returned
return nil, fmt.Errorf("quantity is out of range (1-2000): %v", quantity)
}

return &ReadDiscreteInputsRequestTCP{
Expand Down Expand Up @@ -78,9 +78,9 @@ func (r ReadDiscreteInputsRequestTCP) ExpectedResponseLength() int {

// NewReadDiscreteInputsRequestRTU creates new instance of Read Discrete Inputs RTU request
func NewReadDiscreteInputsRequestRTU(unitID uint8, startAddress uint16, quantity uint16) (*ReadDiscreteInputsRequestRTU, error) {
if quantity == 0 || quantity > 2048 {
// 2048 coils is due that in response data size field is 1 byte so max 256*8=2048 coils can be returned
return nil, fmt.Errorf("quantity is out of range (1-2048): %v", quantity)
if quantity == 0 || quantity > 2000 {
// 2000 coils is due that in response data size field is 1 byte so max 250*8=2000 coils can be returned
return nil, fmt.Errorf("quantity is out of range (1-2000): %v", quantity)
}

return &ReadDiscreteInputsRequestRTU{
Expand Down
8 changes: 4 additions & 4 deletions packet/readdiscreteinputsrequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ func TestNewReadDiscreteInputsRequestTCP(t *testing.T) {
name: "nok, quantity too big",
whenUnitID: 1,
whenStartAddress: 200,
whenQuantity: 2048 + 1,
whenQuantity: 2000 + 1,
expect: nil,
expectError: "quantity is out of range (1-2048): 2049",
expectError: "quantity is out of range (1-2000): 2001",
},
}

Expand Down Expand Up @@ -186,9 +186,9 @@ func TestNewReadDiscreteInputsRequestRTU(t *testing.T) {
name: "nok, quantity too big",
whenUnitID: 1,
whenStartAddress: 200,
whenQuantity: 2048 + 1,
whenQuantity: 2000 + 1,
expect: nil,
expectError: "quantity is out of range (1-2048): 2049",
expectError: "quantity is out of range (1-2000): 2001",
},
}

Expand Down
21 changes: 21 additions & 0 deletions packet/registers.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,27 @@ import (
//
// Background info: https://www.digi.com/wiki/developer/index.php/Modbus_Floating_Points (about floats but 32bit int is also double word)
//
// For example, if the number 2923517522 (hex: AE 41 56 52) was to be sent as a 32 bit unsigned integen then bytes that
// are send over the wire depend on 2 factors - byte order and/or register/word order.
//
// Some devices store the 32bits in 2 registers/words in following order:
// a) AE41 5652 - higher (leftmost) 16 bits (high word) in the first register and the remaining low word in the second (AE41 before 5652)
// b) 5652 AE41 - low word in the first register and high word in the second (5652 before AE41)
//
// Ordered in memory (vertical table):
// | Memory | Big E | Little E | BE Low Word First | LE Low Word First |
// | byte 0 | AE | 52 | 56 | 41 |
// | byte 1 | 41 | 56 | 52 | AE |
// | byte 2 | 56 | 41 | AE | 52 |
// | byte 3 | 52 | AE | 41 | 56 |
//
// Ordered in memory (horizontal table):
// | 0 1 2 3 | Byte order | Word order | Name |
// | AE41 5652 | high byte first | high word first | big endian (high word first) |
// | 5652 AE41 | high byte first | low word first | big endian (low word first) |
// | 41AE 5256 | low byte first | high word first | little endian (low word first) |
// | 5256 41AE | low byte first | low word first | little endian (high word first) |
//
// Example:
// Our PLC (modbus serving) controller/computer is using little endian
//
Expand Down
12 changes: 6 additions & 6 deletions packet/writemultiplecoilsrequest.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ type WriteMultipleCoilsRequest struct {
// NewWriteMultipleCoilsRequestTCP creates new instance of Write Multiple Coils TCP request
func NewWriteMultipleCoilsRequestTCP(unitID uint8, startAddress uint16, coils []bool) (*WriteMultipleCoilsRequestTCP, error) {
coilsCount := len(coils)
if coilsCount == 0 || coilsCount > 2048 {
// 2048 coils is due that coils byte len size field is 1 byte so max 256*8=2048 coils can be sent
return nil, fmt.Errorf("coils count is out of range (1-2048): %v", coilsCount)
if coilsCount == 0 || coilsCount > 1968 {
// 1968 coils is due that coils byte len size field is 1 byte so max 246*8=1968 coils can be sent
return nil, fmt.Errorf("coils count is out of range (1-1968): %v", coilsCount)
}

coilsBytes := CoilsToBytes(coils)
Expand Down Expand Up @@ -87,9 +87,9 @@ func (r WriteMultipleCoilsRequestTCP) ExpectedResponseLength() int {
// NewWriteMultipleCoilsRequestRTU creates new instance of Write Multiple Coils RTU request
func NewWriteMultipleCoilsRequestRTU(unitID uint8, startAddress uint16, coils []bool) (*WriteMultipleCoilsRequestRTU, error) {
coilsCount := len(coils)
if coilsCount == 0 || coilsCount > 2048 {
// 2048 coils is due that coils byte len size field is 1 byte so max 256*8=2048 coils can be sent
return nil, fmt.Errorf("coils count is out of range (1-2048): %v", coilsCount)
if coilsCount == 0 || coilsCount > 1968 {
// 1968 coils is due that coils byte len size field is 1 byte so max 246*8=1968 coils can be sent
return nil, fmt.Errorf("coils count is out of range (1-1968): %v", coilsCount)
}

coilsBytes := CoilsToBytes(coils)
Expand Down
4 changes: 2 additions & 2 deletions packet/writemultiplecoilsrequest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ func TestNewWriteMultipleCoilsRequestTCP(t *testing.T) {
whenStartAddress: 200,
whenCoils: []bool{},
expect: nil,
expectError: "coils count is out of range (1-2048): 0",
expectError: "coils count is out of range (1-1968): 0",
},
}

Expand Down Expand Up @@ -212,7 +212,7 @@ func TestNewWriteMultipleCoilsRequestRTU(t *testing.T) {
whenStartAddress: 200,
whenCoils: []bool{},
expect: nil,
expectError: "coils count is out of range (1-2048): 0",
expectError: "coils count is out of range (1-1968): 0",
},
}

Expand Down
56 changes: 35 additions & 21 deletions splitter.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import (
"sort"
)

// split groups (by host:port+unitID+ "optimized" max amount of fields for max quantity) fields into packets
func split(fields []*Field, funcType string) ([]RegisterRequest, error) {
byAddressUnitID := groupByAddressUnitID(fields)
batches := batchGrouped(byAddressUnitID)
// split groups (by host:port+UnitID, "optimized" max amount of fields for max quantity) fields into packets
func split(fields []Field, funcType string) ([]RegisterRequest, error) {
connectionGroup, err := groupForSingleConnection(fields)
if err != nil {
return nil, err
}
batches := batchToRequests(connectionGroup)

result := make([]RegisterRequest, len(batches))
for i, b := range batches {
Expand All @@ -29,30 +32,37 @@ func split(fields []*Field, funcType string) ([]RegisterRequest, error) {
return nil, err
}
result[i] = RegisterRequest{
startAddress: b.StartAddress,
Request: req,
fields: b.fields,
Request: req,

serverAddress: b.Address,
unitID: b.UnitID,
startAddress: b.StartAddress,
fields: b.fields,
}
}
return result, nil
}

func groupByAddressUnitID(fields []*Field) map[string]map[uint16]registerSlot {
// groupForSingleConnection groups fields into groups what can be requested within same/single connection/request
func groupForSingleConnection(fields []Field) (map[string]map[uint16]registerSlot, error) {
result := map[string]map[uint16]registerSlot{}
for _, f := range fields {
// create groups by modbus address + unitID ... and on second level by register address
gID := fmt.Sprintf("%v_%v", f.address, f.unitID)
if err := f.Validate(); err != nil {
return nil, err
}
// create groups by modbus Address + unitID ... and on second level by register Address
gID := fmt.Sprintf("%v_%v", f.ServerAddress, f.UnitID)
group, ok := result[gID]
if !ok {
group = map[uint16]registerSlot{}
result[gID] = group
}

registerSize := f.registerSize()
slot, ok := group[f.registerAddress]
slot, ok := group[f.RegisterAddress]
if !ok {
slot = registerSlot{
registerAddress: f.registerAddress,
registerAddress: f.RegisterAddress,
size: registerSize,
fields: Fields{},
}
Expand All @@ -61,26 +71,27 @@ func groupByAddressUnitID(fields []*Field) map[string]map[uint16]registerSlot {
slot.size = registerSize
}
slot.fields = append(slot.fields, f)
group[f.registerAddress] = slot
group[f.RegisterAddress] = slot
}
return result
return result, nil
}

func batchGrouped(grouped map[string]map[uint16]registerSlot) []requestBatch {
func batchToRequests(connectionGroup map[string]map[uint16]registerSlot) []requestBatch {
// NB: is batching/grouping algorithm is very naive. It just sorts fields by register and creates N number
// of requests of them by limiting quantity to MaxRegistersInReadResponse. It does not try to optimise long caps
// between fields
// assumes that UnitID is same for all fields within group

var result = make([]requestBatch, 0)
for _, group := range grouped {
for _, group := range connectionGroup {
groupByAddress := slotsSorter{}
for _, slot := range group {
groupByAddress = append(groupByAddress, slot)
}
sort.Sort(groupByAddress)

address := groupByAddress[0].fields[0].address
unitID := groupByAddress[0].fields[0].unitID
address := groupByAddress[0].fields[0].ServerAddress
unitID := groupByAddress[0].fields[0].UnitID

batch := requestBatch{}
isFirstSeen := false
Expand All @@ -96,8 +107,9 @@ func batchGrouped(grouped map[string]map[uint16]registerSlot) []requestBatch {
batch.UnitID = unitID
}

addressDiff := (registerAddress + slot.size) - firstAddress
if addressDiff >= packet.MaxRegistersInReadResponse {
slotEndRegister := registerAddress + slot.size
addressDiff := slotEndRegister - firstAddress
if addressDiff > packet.MaxRegistersInReadResponse {
result = append(result, batch)

batch = requestBatch{
Expand All @@ -108,7 +120,9 @@ func batchGrouped(grouped map[string]map[uint16]registerSlot) []requestBatch {
firstAddress = registerAddress
addressDiff = slot.size
}
batch.Quantity = addressDiff
if batch.Quantity < addressDiff {
batch.Quantity = addressDiff
}

batch.fields = append(batch.fields, slot.fields...)
}
Expand Down
Loading

0 comments on commit 922276e

Please sign in to comment.