Modbus refactor (#9141)
This commit is contained in:
parent
58479fdb05
commit
2e7b232073
|
|
@ -71,8 +71,6 @@ following works:
|
||||||
- github.com/go-redis/redis [BSD 2-Clause "Simplified" License](https://github.com/go-redis/redis/blob/master/LICENSE)
|
- github.com/go-redis/redis [BSD 2-Clause "Simplified" License](https://github.com/go-redis/redis/blob/master/LICENSE)
|
||||||
- github.com/go-sql-driver/mysql [Mozilla Public License 2.0](https://github.com/go-sql-driver/mysql/blob/master/LICENSE)
|
- github.com/go-sql-driver/mysql [Mozilla Public License 2.0](https://github.com/go-sql-driver/mysql/blob/master/LICENSE)
|
||||||
- github.com/go-stack/stack [MIT License](https://github.com/go-stack/stack/blob/master/LICENSE.md)
|
- github.com/go-stack/stack [MIT License](https://github.com/go-stack/stack/blob/master/LICENSE.md)
|
||||||
- github.com/goburrow/modbus [BSD 3-Clause "New" or "Revised" License](https://github.com/goburrow/modbus/blob/master/LICENSE)
|
|
||||||
- github.com/goburrow/serial [MIT License](https://github.com/goburrow/serial/LICENSE)
|
|
||||||
- github.com/gobwas/glob [MIT License](https://github.com/gobwas/glob/blob/master/LICENSE)
|
- github.com/gobwas/glob [MIT License](https://github.com/gobwas/glob/blob/master/LICENSE)
|
||||||
- github.com/gofrs/uuid [MIT License](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
- github.com/gofrs/uuid [MIT License](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
||||||
- github.com/gogo/googleapis [Apache License 2.0](https://github.com/gogo/googleapis/blob/master/LICENSE)
|
- github.com/gogo/googleapis [Apache License 2.0](https://github.com/gogo/googleapis/blob/master/LICENSE)
|
||||||
|
|
@ -92,6 +90,8 @@ following works:
|
||||||
- github.com/gorilla/mux [BSD 3-Clause "New" or "Revised" License](https://github.com/gorilla/mux/blob/master/LICENSE)
|
- github.com/gorilla/mux [BSD 3-Clause "New" or "Revised" License](https://github.com/gorilla/mux/blob/master/LICENSE)
|
||||||
- github.com/gorilla/websocket [BSD 2-Clause "Simplified" License](https://github.com/gorilla/websocket/blob/master/LICENSE)
|
- github.com/gorilla/websocket [BSD 2-Clause "Simplified" License](https://github.com/gorilla/websocket/blob/master/LICENSE)
|
||||||
- github.com/gosnmp/gosnmp [BSD 2-Clause "Simplified" License](https://github.com/gosnmp/gosnmp/blob/master/LICENSE)
|
- github.com/gosnmp/gosnmp [BSD 2-Clause "Simplified" License](https://github.com/gosnmp/gosnmp/blob/master/LICENSE)
|
||||||
|
- github.com/grid-x/modbus [BSD 3-Clause "New" or "Revised" License](https://github.com/grid-x/modbus/blob/master/LICENSE)
|
||||||
|
- github.com/grid-x/serial [MIT License](https://github.com/grid-x/serial/blob/master/LICENSE)
|
||||||
- github.com/grpc-ecosystem/grpc-gateway [BSD 3-Clause "New" or "Revised" License](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt)
|
- github.com/grpc-ecosystem/grpc-gateway [BSD 3-Clause "New" or "Revised" License](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt)
|
||||||
- github.com/hailocab/go-hostpool [MIT License](https://github.com/hailocab/go-hostpool/blob/master/LICENSE)
|
- github.com/hailocab/go-hostpool [MIT License](https://github.com/hailocab/go-hostpool/blob/master/LICENSE)
|
||||||
- github.com/harlow/kinesis-consumer [MIT License](https://github.com/harlow/kinesis-consumer/blob/master/MIT-LICENSE)
|
- github.com/harlow/kinesis-consumer [MIT License](https://github.com/harlow/kinesis-consumer/blob/master/MIT-LICENSE)
|
||||||
|
|
|
||||||
3
go.mod
3
go.mod
|
|
@ -53,7 +53,7 @@ require (
|
||||||
github.com/go-ping/ping v0.0.0-20210201095549-52eed920f98c
|
github.com/go-ping/ping v0.0.0-20210201095549-52eed920f98c
|
||||||
github.com/go-redis/redis v6.15.9+incompatible
|
github.com/go-redis/redis v6.15.9+incompatible
|
||||||
github.com/go-sql-driver/mysql v1.5.0
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
github.com/goburrow/modbus v0.1.0
|
github.com/goburrow/modbus v0.1.0 // indirect
|
||||||
github.com/goburrow/serial v0.1.0 // indirect
|
github.com/goburrow/serial v0.1.0 // indirect
|
||||||
github.com/gobwas/glob v0.2.3
|
github.com/gobwas/glob v0.2.3
|
||||||
github.com/gofrs/uuid v3.3.0+incompatible
|
github.com/gofrs/uuid v3.3.0+incompatible
|
||||||
|
|
@ -66,6 +66,7 @@ require (
|
||||||
github.com/gopcua/opcua v0.1.13
|
github.com/gopcua/opcua v0.1.13
|
||||||
github.com/gorilla/mux v1.7.3
|
github.com/gorilla/mux v1.7.3
|
||||||
github.com/gosnmp/gosnmp v1.32.0
|
github.com/gosnmp/gosnmp v1.32.0
|
||||||
|
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
||||||
github.com/harlow/kinesis-consumer v0.3.1-0.20181230152818-2f58b136fee0
|
github.com/harlow/kinesis-consumer v0.3.1-0.20181230152818-2f58b136fee0
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -561,6 +561,10 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
||||||
github.com/gosnmp/gosnmp v1.32.0 h1:gctewmZx5qFI0oHMzRnjETqIZ093d9NgZy9TQr3V0iA=
|
github.com/gosnmp/gosnmp v1.32.0 h1:gctewmZx5qFI0oHMzRnjETqIZ093d9NgZy9TQr3V0iA=
|
||||||
github.com/gosnmp/gosnmp v1.32.0/go.mod h1:EIp+qkEpXoVsyZxXKy0AmXQx0mCHMMcIhXXvNDMpgF0=
|
github.com/gosnmp/gosnmp v1.32.0/go.mod h1:EIp+qkEpXoVsyZxXKy0AmXQx0mCHMMcIhXXvNDMpgF0=
|
||||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||||
|
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b h1:Y4xqzO0CDNoehCr3ncgie3IgFTO9AzV8PMMEWESFM5c=
|
||||||
|
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b/go.mod h1:YaK0rKJenZ74vZFcSSLlAQqtG74PMI68eDjpDCDDmTw=
|
||||||
|
github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08 h1:syBxnRYnSPUDdkdo5U4sy2roxBPQDjNiw4od7xlsABQ=
|
||||||
|
github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk=
|
||||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,20 @@ with N decimal places'.
|
||||||
(FLOAT32 is deprecated and should not be used any more. UFIXED provides the same conversion
|
(FLOAT32 is deprecated and should not be used any more. UFIXED provides the same conversion
|
||||||
from unsigned values).
|
from unsigned values).
|
||||||
|
|
||||||
|
### Trouble shooting
|
||||||
|
Modbus documentations are often a mess. People confuse memory-address (starts at one) and register address (starts at zero) or stay unclear about the used word-order. Furthermore, there are some non-standard implementations that also
|
||||||
|
swap the bytes within the register word (16-bit).
|
||||||
|
|
||||||
|
If you get an error or don't get the expected values from your device, you can try the following steps (assuming a 32-bit value).
|
||||||
|
|
||||||
|
In case are using a serial device and get an `permission denied` error, please check the permissions of your serial device and change accordingly.
|
||||||
|
|
||||||
|
In case you get an `exception '2' (illegal data address)` error you might try to offset your `address` entries by minus one as it is very likely that there is a confusion between memory and register addresses.
|
||||||
|
|
||||||
|
In case you see strange values, the `byte_order` might be off. You can either probe all combinations (`ABCD`, `CDBA`, `BADC` or `DCBA`) or you set `byte_order="ABCD" data_type="UINT32"` and use the resulting value(s) in an online converter like [this](https://www.scadacore.com/tools/programming-calculators/online-hex-converter/). This makes especially sense if you don't want to mess with the device, deal with 64-bit values and/or don't know the `data_type` of your register (e.g. fix-point floating values vs. IEEE floating point).
|
||||||
|
|
||||||
|
If nothing helps, please post your configuration, error message and/or the output of `byte_order="ABCD" data_type="UINT32"` to one of the telegraf support channels (forum, slack or as issue).
|
||||||
|
|
||||||
### Example Output
|
### Example Output
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
package modbus
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxQuantityDiscreteInput = uint16(2000)
|
||||||
|
maxQuantityCoils = uint16(2000)
|
||||||
|
maxQuantityInputRegisters = uint16(125)
|
||||||
|
maxQuantityHoldingRegisters = uint16(125)
|
||||||
|
)
|
||||||
|
|
||||||
|
type Configuration interface {
|
||||||
|
Check() error
|
||||||
|
Process() (map[byte]requestSet, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeDuplicates(elements []uint16) []uint16 {
|
||||||
|
encountered := map[uint16]bool{}
|
||||||
|
result := []uint16{}
|
||||||
|
|
||||||
|
for _, addr := range elements {
|
||||||
|
if !encountered[addr] {
|
||||||
|
encountered[addr] = true
|
||||||
|
result = append(result, addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeInputDatatype(dataType string) (string, error) {
|
||||||
|
switch dataType {
|
||||||
|
case "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64", "FLOAT32", "FLOAT64":
|
||||||
|
return dataType, nil
|
||||||
|
}
|
||||||
|
return "unknown", fmt.Errorf("unknown type %q", dataType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeOutputDatatype(dataType string) (string, error) {
|
||||||
|
switch dataType {
|
||||||
|
case "", "native":
|
||||||
|
return "native", nil
|
||||||
|
case "INT64", "UINT64", "FLOAT64":
|
||||||
|
return dataType, nil
|
||||||
|
}
|
||||||
|
return "unknown", fmt.Errorf("unknown type %q", dataType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeByteOrder(byteOrder string) (string, error) {
|
||||||
|
switch byteOrder {
|
||||||
|
case "ABCD", "MSW-BE", "MSW": // Big endian (Motorola)
|
||||||
|
return "ABCD", nil
|
||||||
|
case "BADC", "MSW-LE": // Big endian with bytes swapped
|
||||||
|
return "BADC", nil
|
||||||
|
case "CDAB", "LSW-BE": // Little endian with bytes swapped
|
||||||
|
return "CDAB", nil
|
||||||
|
case "DCBA", "LSW-LE", "LSW": // Little endian (Intel)
|
||||||
|
return "DCBA", nil
|
||||||
|
}
|
||||||
|
return "unknown", fmt.Errorf("unknown byte-order %q", byteOrder)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,246 @@
|
||||||
|
package modbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fieldDefinition struct {
|
||||||
|
Measurement string `toml:"measurement"`
|
||||||
|
Name string `toml:"name"`
|
||||||
|
ByteOrder string `toml:"byte_order"`
|
||||||
|
DataType string `toml:"data_type"`
|
||||||
|
Scale float64 `toml:"scale"`
|
||||||
|
Address []uint16 `toml:"address"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConfigurationOriginal struct {
|
||||||
|
SlaveID byte `toml:"slave_id"`
|
||||||
|
DiscreteInputs []fieldDefinition `toml:"discrete_inputs"`
|
||||||
|
Coils []fieldDefinition `toml:"coils"`
|
||||||
|
HoldingRegisters []fieldDefinition `toml:"holding_registers"`
|
||||||
|
InputRegisters []fieldDefinition `toml:"input_registers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) {
|
||||||
|
coil, err := c.initRequests(c.Coils, cCoils, maxQuantityCoils)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
discrete, err := c.initRequests(c.DiscreteInputs, cDiscreteInputs, maxQuantityDiscreteInput)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
holding, err := c.initRequests(c.HoldingRegisters, cHoldingRegisters, maxQuantityHoldingRegisters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
input, err := c.initRequests(c.InputRegisters, cInputRegisters, maxQuantityInputRegisters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[byte]requestSet{
|
||||||
|
c.SlaveID: {
|
||||||
|
coil: coil,
|
||||||
|
discrete: discrete,
|
||||||
|
holding: holding,
|
||||||
|
input: input,
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) Check() error {
|
||||||
|
if err := c.validateFieldDefinitions(c.DiscreteInputs, cDiscreteInputs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.validateFieldDefinitions(c.Coils, cCoils); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.validateFieldDefinitions(c.HoldingRegisters, cHoldingRegisters); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.validateFieldDefinitions(c.InputRegisters, cInputRegisters)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, registerType string, maxQuantity uint16) ([]request, error) {
|
||||||
|
fields, err := c.initFields(fieldDefs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return newRequestsFromFields(fields, c.SlaveID, registerType, maxQuantity), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) initFields(fieldDefs []fieldDefinition) ([]field, error) {
|
||||||
|
// Construct the fields from the field definitions
|
||||||
|
fields := make([]field, 0, len(fieldDefs))
|
||||||
|
for _, def := range fieldDefs {
|
||||||
|
f, err := c.newFieldFromDefinition(def)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("initializing field %q failed: %v", def.Name, err)
|
||||||
|
}
|
||||||
|
fields = append(fields, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition) (field, error) {
|
||||||
|
// Check if the addresses are consecutive
|
||||||
|
expected := def.Address[0]
|
||||||
|
for _, current := range def.Address[1:] {
|
||||||
|
expected++
|
||||||
|
if current != expected {
|
||||||
|
return field{}, fmt.Errorf("addresses of field %q are not consecutive", def.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize the field
|
||||||
|
f := field{
|
||||||
|
measurement: def.Measurement,
|
||||||
|
name: def.Name,
|
||||||
|
scale: def.Scale,
|
||||||
|
address: def.Address[0],
|
||||||
|
length: uint16(len(def.Address)),
|
||||||
|
}
|
||||||
|
if def.DataType != "" {
|
||||||
|
inType, err := c.normalizeInputDatatype(def.DataType, len(def.Address))
|
||||||
|
if err != nil {
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
outType, err := c.normalizeOutputDatatype(def.DataType)
|
||||||
|
if err != nil {
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
byteOrder, err := c.normalizeByteOrder(def.ByteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.converter, err = determineConverter(inType, byteOrder, outType, def.Scale)
|
||||||
|
if err != nil {
|
||||||
|
return f, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefinition, registerType string) error {
|
||||||
|
nameEncountered := map[string]bool{}
|
||||||
|
for _, item := range fieldDefs {
|
||||||
|
//check empty name
|
||||||
|
if item.Name == "" {
|
||||||
|
return fmt.Errorf("empty name in '%s'", registerType)
|
||||||
|
}
|
||||||
|
|
||||||
|
//search name duplicate
|
||||||
|
canonicalName := item.Measurement + "." + item.Name
|
||||||
|
if nameEncountered[canonicalName] {
|
||||||
|
return fmt.Errorf("name '%s' is duplicated in measurement '%s' '%s' - '%s'", item.Name, item.Measurement, registerType, item.Name)
|
||||||
|
}
|
||||||
|
nameEncountered[canonicalName] = true
|
||||||
|
|
||||||
|
if registerType == cInputRegisters || registerType == cHoldingRegisters {
|
||||||
|
// search byte order
|
||||||
|
switch item.ByteOrder {
|
||||||
|
case "AB", "BA", "ABCD", "CDAB", "BADC", "DCBA", "ABCDEFGH", "HGFEDCBA", "BADCFEHG", "GHEFCDAB":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid byte order '%s' in '%s' - '%s'", item.ByteOrder, registerType, item.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// search data type
|
||||||
|
switch item.DataType {
|
||||||
|
case "UINT16", "INT16", "UINT32", "INT32", "UINT64", "INT64", "FLOAT32-IEEE", "FLOAT64-IEEE", "FLOAT32", "FIXED", "UFIXED":
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid data type '%s' in '%s' - '%s'", item.DataType, registerType, item.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check scale
|
||||||
|
if item.Scale == 0.0 {
|
||||||
|
return fmt.Errorf("invalid scale '%f' in '%s' - '%s'", item.Scale, registerType, item.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check address
|
||||||
|
if len(item.Address) != 1 && len(item.Address) != 2 && len(item.Address) != 4 {
|
||||||
|
return fmt.Errorf("invalid address '%v' length '%v' in '%s' - '%s'", item.Address, len(item.Address), registerType, item.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if registerType == cInputRegisters || registerType == cHoldingRegisters {
|
||||||
|
if 2*len(item.Address) != len(item.ByteOrder) {
|
||||||
|
return fmt.Errorf("invalid byte order '%s' and address '%v' in '%s' - '%s'", item.ByteOrder, item.Address, registerType, item.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// search duplicated
|
||||||
|
if len(item.Address) > len(removeDuplicates(item.Address)) {
|
||||||
|
return fmt.Errorf("duplicate address '%v' in '%s' - '%s'", item.Address, registerType, item.Name)
|
||||||
|
}
|
||||||
|
} else if len(item.Address) != 1 {
|
||||||
|
return fmt.Errorf("invalid address'%v' length'%v' in '%s' - '%s'", item.Address, len(item.Address), registerType, item.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) normalizeInputDatatype(dataType string, words int) (string, error) {
|
||||||
|
// Handle our special types
|
||||||
|
switch dataType {
|
||||||
|
case "FIXED":
|
||||||
|
switch words {
|
||||||
|
case 1:
|
||||||
|
return "INT16", nil
|
||||||
|
case 2:
|
||||||
|
return "INT32", nil
|
||||||
|
case 4:
|
||||||
|
return "INT64", nil
|
||||||
|
default:
|
||||||
|
return "unknown", fmt.Errorf("invalid length %d for type %q", words, dataType)
|
||||||
|
}
|
||||||
|
case "FLOAT32", "UFIXED":
|
||||||
|
switch words {
|
||||||
|
case 1:
|
||||||
|
return "UINT16", nil
|
||||||
|
case 2:
|
||||||
|
return "UINT32", nil
|
||||||
|
case 4:
|
||||||
|
return "UINT64", nil
|
||||||
|
default:
|
||||||
|
return "unknown", fmt.Errorf("invalid length %d for type %q", words, dataType)
|
||||||
|
}
|
||||||
|
case "FLOAT32-IEEE":
|
||||||
|
return "FLOAT32", nil
|
||||||
|
case "FLOAT64-IEEE":
|
||||||
|
return "FLOAT64", nil
|
||||||
|
}
|
||||||
|
return normalizeInputDatatype(dataType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) normalizeOutputDatatype(dataType string) (string, error) {
|
||||||
|
// Handle our special types
|
||||||
|
switch dataType {
|
||||||
|
case "FIXED", "FLOAT32", "UFIXED":
|
||||||
|
return "FLOAT64", nil
|
||||||
|
}
|
||||||
|
return normalizeOutputDatatype("native")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConfigurationOriginal) normalizeByteOrder(byteOrder string) (string, error) {
|
||||||
|
// Handle our special types
|
||||||
|
switch byteOrder {
|
||||||
|
case "AB", "ABCDEFGH":
|
||||||
|
return "ABCD", nil
|
||||||
|
case "BADCFEHG":
|
||||||
|
return "BADC", nil
|
||||||
|
case "GHEFCDAB":
|
||||||
|
return "CDAB", nil
|
||||||
|
case "BA", "HGFEDCBA":
|
||||||
|
return "DCBA", nil
|
||||||
|
}
|
||||||
|
return normalizeByteOrder(byteOrder)
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
package modbus
|
package modbus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sort"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
mb "github.com/goburrow/modbus"
|
mb "github.com/grid-x/modbus"
|
||||||
|
|
||||||
"github.com/influxdata/telegraf"
|
"github.com/influxdata/telegraf"
|
||||||
"github.com/influxdata/telegraf/config"
|
"github.com/influxdata/telegraf/config"
|
||||||
"github.com/influxdata/telegraf/metric"
|
"github.com/influxdata/telegraf/metric"
|
||||||
|
|
@ -25,42 +24,37 @@ type Modbus struct {
|
||||||
DataBits int `toml:"data_bits"`
|
DataBits int `toml:"data_bits"`
|
||||||
Parity string `toml:"parity"`
|
Parity string `toml:"parity"`
|
||||||
StopBits int `toml:"stop_bits"`
|
StopBits int `toml:"stop_bits"`
|
||||||
SlaveID int `toml:"slave_id"`
|
|
||||||
Timeout config.Duration `toml:"timeout"`
|
Timeout config.Duration `toml:"timeout"`
|
||||||
Retries int `toml:"busy_retries"`
|
Retries int `toml:"busy_retries"`
|
||||||
RetriesWaitTime config.Duration `toml:"busy_retries_wait"`
|
RetriesWaitTime config.Duration `toml:"busy_retries_wait"`
|
||||||
DiscreteInputs []fieldContainer `toml:"discrete_inputs"`
|
|
||||||
Coils []fieldContainer `toml:"coils"`
|
|
||||||
HoldingRegisters []fieldContainer `toml:"holding_registers"`
|
|
||||||
InputRegisters []fieldContainer `toml:"input_registers"`
|
|
||||||
Log telegraf.Logger `toml:"-"`
|
Log telegraf.Logger `toml:"-"`
|
||||||
registers []register
|
// Register configuration
|
||||||
isConnected bool
|
ConfigurationOriginal
|
||||||
tcpHandler *mb.TCPClientHandler
|
// Connection handling
|
||||||
rtuHandler *mb.RTUClientHandler
|
|
||||||
asciiHandler *mb.ASCIIClientHandler
|
|
||||||
client mb.Client
|
client mb.Client
|
||||||
|
handler mb.ClientHandler
|
||||||
|
isConnected bool
|
||||||
|
// Request handling
|
||||||
|
requests map[byte]requestSet
|
||||||
}
|
}
|
||||||
|
|
||||||
type register struct {
|
type fieldConverterFunc func(bytes []byte) interface{}
|
||||||
Type string
|
|
||||||
RegistersRange []registerRange
|
type requestSet struct {
|
||||||
Fields []fieldContainer
|
coil []request
|
||||||
|
discrete []request
|
||||||
|
holding []request
|
||||||
|
input []request
|
||||||
}
|
}
|
||||||
|
|
||||||
type fieldContainer struct {
|
type field struct {
|
||||||
Measurement string `toml:"measurement"`
|
measurement string
|
||||||
Name string `toml:"name"`
|
name string
|
||||||
ByteOrder string `toml:"byte_order"`
|
scale float64
|
||||||
DataType string `toml:"data_type"`
|
|
||||||
Scale float64 `toml:"scale"`
|
|
||||||
Address []uint16 `toml:"address"`
|
|
||||||
value interface{}
|
|
||||||
}
|
|
||||||
|
|
||||||
type registerRange struct {
|
|
||||||
address uint16
|
address uint16
|
||||||
length uint16
|
length uint16
|
||||||
|
converter fieldConverterFunc
|
||||||
|
value interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -173,500 +167,29 @@ func (m *Modbus) Init() error {
|
||||||
return fmt.Errorf("retries cannot be negative")
|
return fmt.Errorf("retries cannot be negative")
|
||||||
}
|
}
|
||||||
|
|
||||||
err := m.InitRegister(m.DiscreteInputs, cDiscreteInputs)
|
// Check and process the configuration
|
||||||
if err != nil {
|
if err := m.ConfigurationOriginal.Check(); err != nil {
|
||||||
return err
|
return fmt.Errorf("original configuraton invalid: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = m.InitRegister(m.Coils, cCoils)
|
r, err := m.ConfigurationOriginal.Process()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return fmt.Errorf("cannot process original configuraton: %v", err)
|
||||||
}
|
}
|
||||||
|
m.requests = r
|
||||||
|
|
||||||
err = m.InitRegister(m.HoldingRegisters, cHoldingRegisters)
|
// Setup client
|
||||||
if err != nil {
|
if err := m.initClient(); err != nil {
|
||||||
return err
|
return fmt.Errorf("initializing client failed: %v", err)
|
||||||
}
|
|
||||||
|
|
||||||
err = m.InitRegister(m.InputRegisters, cInputRegisters)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Modbus) InitRegister(fields []fieldContainer, name string) error {
|
|
||||||
if len(fields) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := validateFieldContainers(fields, name)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs := []uint16{}
|
|
||||||
for _, field := range fields {
|
|
||||||
addrs = append(addrs, field.Address...)
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs = removeDuplicates(addrs)
|
|
||||||
sort.Slice(addrs, func(i, j int) bool { return addrs[i] < addrs[j] })
|
|
||||||
|
|
||||||
ii := 0
|
|
||||||
maxQuantity := 1
|
|
||||||
var registersRange []registerRange
|
|
||||||
if name == cDiscreteInputs || name == cCoils {
|
|
||||||
maxQuantity = 2000
|
|
||||||
} else if name == cInputRegisters || name == cHoldingRegisters {
|
|
||||||
maxQuantity = 125
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get range of consecutive integers
|
|
||||||
// [1, 2, 3, 5, 6, 10, 11, 12, 14]
|
|
||||||
// (1, 3) , (5, 2) , (10, 3), (14 , 1)
|
|
||||||
for range addrs {
|
|
||||||
if ii >= len(addrs) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
quantity := 1
|
|
||||||
start := addrs[ii]
|
|
||||||
end := start
|
|
||||||
|
|
||||||
for ii < len(addrs)-1 && addrs[ii+1]-addrs[ii] == 1 && quantity < maxQuantity {
|
|
||||||
end = addrs[ii+1]
|
|
||||||
ii++
|
|
||||||
quantity++
|
|
||||||
}
|
|
||||||
ii++
|
|
||||||
|
|
||||||
registersRange = append(registersRange, registerRange{start, end - start + 1})
|
|
||||||
}
|
|
||||||
|
|
||||||
m.registers = append(m.registers, register{name, registersRange, fields})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connect to a MODBUS Slave device via Modbus/[TCP|RTU|ASCII]
|
|
||||||
func connect(m *Modbus) error {
|
|
||||||
u, err := url.Parse(m.Controller)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch u.Scheme {
|
|
||||||
case "tcp":
|
|
||||||
var host, port string
|
|
||||||
host, port, err = net.SplitHostPort(u.Host)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.tcpHandler = mb.NewTCPClientHandler(host + ":" + port)
|
|
||||||
m.tcpHandler.Timeout = time.Duration(m.Timeout)
|
|
||||||
m.tcpHandler.SlaveId = byte(m.SlaveID)
|
|
||||||
m.client = mb.NewClient(m.tcpHandler)
|
|
||||||
err := m.tcpHandler.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.isConnected = true
|
|
||||||
return nil
|
|
||||||
case "file":
|
|
||||||
if m.TransmissionMode == "RTU" {
|
|
||||||
m.rtuHandler = mb.NewRTUClientHandler(u.Path)
|
|
||||||
m.rtuHandler.Timeout = time.Duration(m.Timeout)
|
|
||||||
m.rtuHandler.SlaveId = byte(m.SlaveID)
|
|
||||||
m.rtuHandler.BaudRate = m.BaudRate
|
|
||||||
m.rtuHandler.DataBits = m.DataBits
|
|
||||||
m.rtuHandler.Parity = m.Parity
|
|
||||||
m.rtuHandler.StopBits = m.StopBits
|
|
||||||
m.client = mb.NewClient(m.rtuHandler)
|
|
||||||
err := m.rtuHandler.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.isConnected = true
|
|
||||||
return nil
|
|
||||||
} else if m.TransmissionMode == "ASCII" {
|
|
||||||
m.asciiHandler = mb.NewASCIIClientHandler(u.Path)
|
|
||||||
m.asciiHandler.Timeout = time.Duration(m.Timeout)
|
|
||||||
m.asciiHandler.SlaveId = byte(m.SlaveID)
|
|
||||||
m.asciiHandler.BaudRate = m.BaudRate
|
|
||||||
m.asciiHandler.DataBits = m.DataBits
|
|
||||||
m.asciiHandler.Parity = m.Parity
|
|
||||||
m.asciiHandler.StopBits = m.StopBits
|
|
||||||
m.client = mb.NewClient(m.asciiHandler)
|
|
||||||
err := m.asciiHandler.Connect()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m.isConnected = true
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("invalid protocol '%s' - '%s' ", u.Scheme, m.TransmissionMode)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid controller")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func disconnect(m *Modbus) error {
|
|
||||||
u, err := url.Parse(m.Controller)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch u.Scheme {
|
|
||||||
case "tcp":
|
|
||||||
m.tcpHandler.Close()
|
|
||||||
return nil
|
|
||||||
case "file":
|
|
||||||
if m.TransmissionMode == "RTU" {
|
|
||||||
m.rtuHandler.Close()
|
|
||||||
return nil
|
|
||||||
} else if m.TransmissionMode == "ASCII" {
|
|
||||||
m.asciiHandler.Close()
|
|
||||||
return nil
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("invalid protocol '%s' - '%s' ", u.Scheme, m.TransmissionMode)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid controller")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFieldContainers(t []fieldContainer, n string) error {
|
|
||||||
nameEncountered := map[string]bool{}
|
|
||||||
for _, item := range t {
|
|
||||||
//check empty name
|
|
||||||
if item.Name == "" {
|
|
||||||
return fmt.Errorf("empty name in '%s'", n)
|
|
||||||
}
|
|
||||||
|
|
||||||
//search name duplicate
|
|
||||||
canonicalName := item.Measurement + "." + item.Name
|
|
||||||
if nameEncountered[canonicalName] {
|
|
||||||
return fmt.Errorf("name '%s' is duplicated in measurement '%s' '%s' - '%s'", item.Name, item.Measurement, n, item.Name)
|
|
||||||
}
|
|
||||||
nameEncountered[canonicalName] = true
|
|
||||||
|
|
||||||
if n == cInputRegisters || n == cHoldingRegisters {
|
|
||||||
// search byte order
|
|
||||||
switch item.ByteOrder {
|
|
||||||
case "AB", "BA", "ABCD", "CDAB", "BADC", "DCBA", "ABCDEFGH", "HGFEDCBA", "BADCFEHG", "GHEFCDAB":
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid byte order '%s' in '%s' - '%s'", item.ByteOrder, n, item.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// search data type
|
|
||||||
switch item.DataType {
|
|
||||||
case "UINT16", "INT16", "UINT32", "INT32", "UINT64", "INT64", "FLOAT32-IEEE", "FLOAT64-IEEE", "FLOAT32", "FIXED", "UFIXED":
|
|
||||||
break
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid data type '%s' in '%s' - '%s'", item.DataType, n, item.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// check scale
|
|
||||||
if item.Scale == 0.0 {
|
|
||||||
return fmt.Errorf("invalid scale '%f' in '%s' - '%s'", item.Scale, n, item.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check address
|
|
||||||
if len(item.Address) != 1 && len(item.Address) != 2 && len(item.Address) != 4 {
|
|
||||||
return fmt.Errorf("invalid address '%v' length '%v' in '%s' - '%s'", item.Address, len(item.Address), n, item.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if n == cInputRegisters || n == cHoldingRegisters {
|
|
||||||
if 2*len(item.Address) != len(item.ByteOrder) {
|
|
||||||
return fmt.Errorf("invalid byte order '%s' and address '%v' in '%s' - '%s'", item.ByteOrder, item.Address, n, item.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// search duplicated
|
|
||||||
if len(item.Address) > len(removeDuplicates(item.Address)) {
|
|
||||||
return fmt.Errorf("duplicate address '%v' in '%s' - '%s'", item.Address, n, item.Name)
|
|
||||||
}
|
|
||||||
} else if len(item.Address) != 1 {
|
|
||||||
return fmt.Errorf("invalid address'%v' length'%v' in '%s' - '%s'", item.Address, len(item.Address), n, item.Name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeDuplicates(elements []uint16) []uint16 {
|
|
||||||
encountered := map[uint16]bool{}
|
|
||||||
result := []uint16{}
|
|
||||||
|
|
||||||
for v := range elements {
|
|
||||||
if encountered[elements[v]] {
|
|
||||||
} else {
|
|
||||||
encountered[elements[v]] = true
|
|
||||||
result = append(result, elements[v])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func readRegisterValues(m *Modbus, rt string, rr registerRange) ([]byte, error) {
|
|
||||||
if rt == cDiscreteInputs {
|
|
||||||
return m.client.ReadDiscreteInputs(rr.address, rr.length)
|
|
||||||
} else if rt == cCoils {
|
|
||||||
return m.client.ReadCoils(rr.address, rr.length)
|
|
||||||
} else if rt == cInputRegisters {
|
|
||||||
return m.client.ReadInputRegisters(rr.address, rr.length)
|
|
||||||
} else if rt == cHoldingRegisters {
|
|
||||||
return m.client.ReadHoldingRegisters(rr.address, rr.length)
|
|
||||||
} else {
|
|
||||||
return []byte{}, fmt.Errorf("not Valid function")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Modbus) getFields() error {
|
|
||||||
for _, register := range m.registers {
|
|
||||||
rawValues := make(map[uint16][]byte)
|
|
||||||
bitRawValues := make(map[uint16]uint16)
|
|
||||||
for _, rr := range register.RegistersRange {
|
|
||||||
address := rr.address
|
|
||||||
readValues, err := readRegisterValues(m, register.Type, rr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Raw Values
|
|
||||||
if register.Type == cDiscreteInputs || register.Type == cCoils {
|
|
||||||
for _, readValue := range readValues {
|
|
||||||
for bitPosition := 0; bitPosition < 8; bitPosition++ {
|
|
||||||
bitRawValues[address] = getBitValue(readValue, bitPosition)
|
|
||||||
address = address + 1
|
|
||||||
if address > rr.address+rr.length {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Raw Values
|
|
||||||
if register.Type == cInputRegisters || register.Type == cHoldingRegisters {
|
|
||||||
batchSize := 2
|
|
||||||
for batchSize < len(readValues) {
|
|
||||||
rawValues[address] = readValues[0:batchSize:batchSize]
|
|
||||||
address = address + 1
|
|
||||||
readValues = readValues[batchSize:]
|
|
||||||
}
|
|
||||||
|
|
||||||
rawValues[address] = readValues[0:batchSize:batchSize]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if register.Type == cDiscreteInputs || register.Type == cCoils {
|
|
||||||
for i := 0; i < len(register.Fields); i++ {
|
|
||||||
register.Fields[i].value = bitRawValues[register.Fields[i].Address[0]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if register.Type == cInputRegisters || register.Type == cHoldingRegisters {
|
|
||||||
for i := 0; i < len(register.Fields); i++ {
|
|
||||||
var valuesT []byte
|
|
||||||
|
|
||||||
for j := 0; j < len(register.Fields[i].Address); j++ {
|
|
||||||
tempArray := rawValues[register.Fields[i].Address[j]]
|
|
||||||
for x := 0; x < len(tempArray); x++ {
|
|
||||||
valuesT = append(valuesT, tempArray[x])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
register.Fields[i].value = convertDataType(register.Fields[i], valuesT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getBitValue(n byte, pos int) uint16 {
|
|
||||||
return uint16(n >> uint(pos) & 0x01)
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertDataType(t fieldContainer, bytes []byte) interface{} {
|
|
||||||
switch t.DataType {
|
|
||||||
case "UINT16":
|
|
||||||
e16 := convertEndianness16(t.ByteOrder, bytes)
|
|
||||||
return scaleUint16(t.Scale, e16)
|
|
||||||
case "INT16":
|
|
||||||
e16 := convertEndianness16(t.ByteOrder, bytes)
|
|
||||||
f16 := int16(e16)
|
|
||||||
return scaleInt16(t.Scale, f16)
|
|
||||||
case "UINT32":
|
|
||||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
|
||||||
return scaleUint32(t.Scale, e32)
|
|
||||||
case "INT32":
|
|
||||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
|
||||||
f32 := int32(e32)
|
|
||||||
return scaleInt32(t.Scale, f32)
|
|
||||||
case "UINT64":
|
|
||||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
|
||||||
f64 := format64(t.DataType, e64).(uint64)
|
|
||||||
return scaleUint64(t.Scale, f64)
|
|
||||||
case "INT64":
|
|
||||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
|
||||||
f64 := format64(t.DataType, e64).(int64)
|
|
||||||
return scaleInt64(t.Scale, f64)
|
|
||||||
case "FLOAT32-IEEE":
|
|
||||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
|
||||||
f32 := math.Float32frombits(e32)
|
|
||||||
return scaleFloat32(t.Scale, f32)
|
|
||||||
case "FLOAT64-IEEE":
|
|
||||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
|
||||||
f64 := math.Float64frombits(e64)
|
|
||||||
return scaleFloat64(t.Scale, f64)
|
|
||||||
case "FIXED":
|
|
||||||
if len(bytes) == 2 {
|
|
||||||
e16 := convertEndianness16(t.ByteOrder, bytes)
|
|
||||||
f16 := int16(e16)
|
|
||||||
return scale16toFloat(t.Scale, f16)
|
|
||||||
} else if len(bytes) == 4 {
|
|
||||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
|
||||||
f32 := int32(e32)
|
|
||||||
return scale32toFloat(t.Scale, f32)
|
|
||||||
} else {
|
|
||||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
|
||||||
f64 := int64(e64)
|
|
||||||
return scale64toFloat(t.Scale, f64)
|
|
||||||
}
|
|
||||||
case "FLOAT32", "UFIXED":
|
|
||||||
if len(bytes) == 2 {
|
|
||||||
e16 := convertEndianness16(t.ByteOrder, bytes)
|
|
||||||
return scale16UtoFloat(t.Scale, e16)
|
|
||||||
} else if len(bytes) == 4 {
|
|
||||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
|
||||||
return scale32UtoFloat(t.Scale, e32)
|
|
||||||
} else {
|
|
||||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
|
||||||
return scale64UtoFloat(t.Scale, e64)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertEndianness16(o string, b []byte) uint16 {
|
|
||||||
switch o {
|
|
||||||
case "AB":
|
|
||||||
return binary.BigEndian.Uint16(b)
|
|
||||||
case "BA":
|
|
||||||
return binary.LittleEndian.Uint16(b)
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertEndianness32(o string, b []byte) uint32 {
|
|
||||||
switch o {
|
|
||||||
case "ABCD":
|
|
||||||
return binary.BigEndian.Uint32(b)
|
|
||||||
case "DCBA":
|
|
||||||
return binary.LittleEndian.Uint32(b)
|
|
||||||
case "BADC":
|
|
||||||
return uint32(binary.LittleEndian.Uint16(b[0:]))<<16 | uint32(binary.LittleEndian.Uint16(b[2:]))
|
|
||||||
case "CDAB":
|
|
||||||
return uint32(binary.BigEndian.Uint16(b[2:]))<<16 | uint32(binary.BigEndian.Uint16(b[0:]))
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func convertEndianness64(o string, b []byte) uint64 {
|
|
||||||
switch o {
|
|
||||||
case "ABCDEFGH":
|
|
||||||
return binary.BigEndian.Uint64(b)
|
|
||||||
case "HGFEDCBA":
|
|
||||||
return binary.LittleEndian.Uint64(b)
|
|
||||||
case "BADCFEHG":
|
|
||||||
return uint64(binary.LittleEndian.Uint16(b[0:]))<<48 | uint64(binary.LittleEndian.Uint16(b[2:]))<<32 | uint64(binary.LittleEndian.Uint16(b[4:]))<<16 | uint64(binary.LittleEndian.Uint16(b[6:]))
|
|
||||||
case "GHEFCDAB":
|
|
||||||
return uint64(binary.BigEndian.Uint16(b[6:]))<<48 | uint64(binary.BigEndian.Uint16(b[4:]))<<32 | uint64(binary.BigEndian.Uint16(b[2:]))<<16 | uint64(binary.BigEndian.Uint16(b[0:]))
|
|
||||||
default:
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func format64(f string, r uint64) interface{} {
|
|
||||||
switch f {
|
|
||||||
case "UINT64":
|
|
||||||
return r
|
|
||||||
case "INT64":
|
|
||||||
return int64(r)
|
|
||||||
default:
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func scale16toFloat(s float64, v int16) float64 {
|
|
||||||
return float64(v) * s
|
|
||||||
}
|
|
||||||
|
|
||||||
func scale32toFloat(s float64, v int32) float64 {
|
|
||||||
return float64(float64(v) * float64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func scale64toFloat(s float64, v int64) float64 {
|
|
||||||
return float64(float64(v) * float64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func scale16UtoFloat(s float64, v uint16) float64 {
|
|
||||||
return float64(v) * s
|
|
||||||
}
|
|
||||||
|
|
||||||
func scale32UtoFloat(s float64, v uint32) float64 {
|
|
||||||
return float64(float64(v) * float64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func scale64UtoFloat(s float64, v uint64) float64 {
|
|
||||||
return float64(float64(v) * float64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleInt16(s float64, v int16) int16 {
|
|
||||||
return int16(float64(v) * s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleUint16(s float64, v uint16) uint16 {
|
|
||||||
return uint16(float64(v) * s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleUint32(s float64, v uint32) uint32 {
|
|
||||||
return uint32(float64(v) * float64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleInt32(s float64, v int32) int32 {
|
|
||||||
return int32(float64(v) * float64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleFloat32(s float64, v float32) float32 {
|
|
||||||
return float32(float64(v) * s)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleFloat64(s float64, v float64) float64 {
|
|
||||||
return v * s
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleUint64(s float64, v uint64) uint64 {
|
|
||||||
return uint64(float64(v) * float64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleInt64(s float64, v int64) int64 {
|
|
||||||
return int64(float64(v) * float64(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gather implements the telegraf plugin interface method for data accumulation
|
// Gather implements the telegraf plugin interface method for data accumulation
|
||||||
func (m *Modbus) Gather(acc telegraf.Accumulator) error {
|
func (m *Modbus) Gather(acc telegraf.Accumulator) error {
|
||||||
if !m.isConnected {
|
if !m.isConnected {
|
||||||
err := connect(m)
|
if err := m.connect(); err != nil {
|
||||||
if err != nil {
|
|
||||||
m.isConnected = false
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -674,53 +197,236 @@ func (m *Modbus) Gather(acc telegraf.Accumulator) error {
|
||||||
timestamp := time.Now()
|
timestamp := time.Now()
|
||||||
for retry := 0; retry <= m.Retries; retry++ {
|
for retry := 0; retry <= m.Retries; retry++ {
|
||||||
timestamp = time.Now()
|
timestamp = time.Now()
|
||||||
err := m.getFields()
|
if err := m.gatherFields(); err != nil {
|
||||||
if err != nil {
|
if mberr, ok := err.(*mb.Error); ok && mberr.ExceptionCode == mb.ExceptionCodeServerDeviceBusy && retry < m.Retries {
|
||||||
mberr, ok := err.(*mb.ModbusError)
|
|
||||||
if ok && mberr.ExceptionCode == mb.ExceptionCodeServerDeviceBusy && retry < m.Retries {
|
|
||||||
m.Log.Infof("Device busy! Retrying %d more time(s)...", m.Retries-retry)
|
m.Log.Infof("Device busy! Retrying %d more time(s)...", m.Retries-retry)
|
||||||
time.Sleep(time.Duration(m.RetriesWaitTime))
|
time.Sleep(time.Duration(m.RetriesWaitTime))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Ignore return error to not shadow the initial error
|
// Show the disconnect error this way to not shadow the initial error
|
||||||
//nolint:errcheck,revive
|
if discerr := m.disconnect(); discerr != nil {
|
||||||
disconnect(m)
|
m.Log.Errorf("Disconnecting failed: %v", discerr)
|
||||||
m.isConnected = false
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Reading was successful, leave the retry loop
|
// Reading was successful, leave the retry loop
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
grouper := metric.NewSeriesGrouper()
|
for slaveID, requests := range m.requests {
|
||||||
for _, reg := range m.registers {
|
|
||||||
tags := map[string]string{
|
tags := map[string]string{
|
||||||
"name": m.Name,
|
"name": m.Name,
|
||||||
"type": reg.Type,
|
"type": cCoils,
|
||||||
|
"slave_id": strconv.Itoa(int(slaveID)),
|
||||||
|
}
|
||||||
|
m.collectFields(acc, timestamp, tags, requests.coil)
|
||||||
|
|
||||||
|
tags["type"] = cDiscreteInputs
|
||||||
|
m.collectFields(acc, timestamp, tags, requests.discrete)
|
||||||
|
|
||||||
|
tags["type"] = cHoldingRegisters
|
||||||
|
m.collectFields(acc, timestamp, tags, requests.holding)
|
||||||
|
|
||||||
|
tags["type"] = cInputRegisters
|
||||||
|
m.collectFields(acc, timestamp, tags, requests.input)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, field := range reg.Fields {
|
return nil
|
||||||
// In case no measurement was specified we use "modbus" as default
|
}
|
||||||
measurement := "modbus"
|
|
||||||
if field.Measurement != "" {
|
|
||||||
measurement = field.Measurement
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group the data by series
|
func (m *Modbus) initClient() error {
|
||||||
if err := grouper.Add(measurement, tags, timestamp, field.Name, field.value); err != nil {
|
u, err := url.Parse(m.Controller)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "tcp":
|
||||||
|
host, port, err := net.SplitHostPort(u.Host)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
handler := mb.NewTCPClientHandler(host + ":" + port)
|
||||||
|
handler.Timeout = time.Duration(m.Timeout)
|
||||||
|
m.handler = handler
|
||||||
|
case "file":
|
||||||
|
switch m.TransmissionMode {
|
||||||
|
case "RTU":
|
||||||
|
handler := mb.NewRTUClientHandler(u.Path)
|
||||||
|
handler.Timeout = time.Duration(m.Timeout)
|
||||||
|
handler.BaudRate = m.BaudRate
|
||||||
|
handler.DataBits = m.DataBits
|
||||||
|
handler.Parity = m.Parity
|
||||||
|
handler.StopBits = m.StopBits
|
||||||
|
m.handler = handler
|
||||||
|
case "ASCII":
|
||||||
|
handler := mb.NewASCIIClientHandler(u.Path)
|
||||||
|
handler.Timeout = time.Duration(m.Timeout)
|
||||||
|
handler.BaudRate = m.BaudRate
|
||||||
|
handler.DataBits = m.DataBits
|
||||||
|
handler.Parity = m.Parity
|
||||||
|
handler.StopBits = m.StopBits
|
||||||
|
m.handler = handler
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid protocol '%s' - '%s' ", u.Scheme, m.TransmissionMode)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid controller %q", m.Controller)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the metrics grouped by series to the accumulator
|
m.handler.SetSlave(m.SlaveID)
|
||||||
for _, metric := range grouper.Metrics() {
|
m.client = mb.NewClient(m.handler)
|
||||||
acc.AddMetric(metric)
|
m.isConnected = false
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to a MODBUS Slave device via Modbus/[TCP|RTU|ASCII]
|
||||||
|
func (m *Modbus) connect() error {
|
||||||
|
err := m.handler.Connect()
|
||||||
|
m.isConnected = err == nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Modbus) disconnect() error {
|
||||||
|
err := m.handler.Close()
|
||||||
|
m.isConnected = false
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Modbus) gatherFields() error {
|
||||||
|
for _, requests := range m.requests {
|
||||||
|
if err := m.gatherRequestsCoil(requests.coil); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.gatherRequestsDiscrete(requests.discrete); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.gatherRequestsHolding(requests.holding); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := m.gatherRequestsInput(requests.input); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *Modbus) gatherRequestsCoil(requests []request) error {
|
||||||
|
for _, request := range requests {
|
||||||
|
m.Log.Debugf("trying to read coil@%v[%v]...", request.address, request.length)
|
||||||
|
bytes, err := m.client.ReadCoils(request.address, request.length)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Log.Debugf("got coil@%v[%v]: %v", request.address, request.length, bytes)
|
||||||
|
|
||||||
|
// Bit value handling
|
||||||
|
for i, field := range request.fields {
|
||||||
|
offset := field.address - request.address
|
||||||
|
idx := offset / 8
|
||||||
|
bit := offset % 8
|
||||||
|
|
||||||
|
request.fields[i].value = uint16((bytes[idx] >> bit) & 0x01)
|
||||||
|
m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, (bytes[idx]>>bit)&0x01, request.fields[i].value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Modbus) gatherRequestsDiscrete(requests []request) error {
|
||||||
|
for _, request := range requests {
|
||||||
|
m.Log.Debugf("trying to read discrete@%v[%v]...", request.address, request.length)
|
||||||
|
bytes, err := m.client.ReadDiscreteInputs(request.address, request.length)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Log.Debugf("got discrete@%v[%v]: %v", request.address, request.length, bytes)
|
||||||
|
|
||||||
|
// Bit value handling
|
||||||
|
for i, field := range request.fields {
|
||||||
|
offset := field.address - request.address
|
||||||
|
idx := offset / 8
|
||||||
|
bit := offset % 8
|
||||||
|
|
||||||
|
request.fields[i].value = uint16((bytes[idx] >> bit) & 0x01)
|
||||||
|
m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, (bytes[idx]>>bit)&0x01, request.fields[i].value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Modbus) gatherRequestsHolding(requests []request) error {
|
||||||
|
for _, request := range requests {
|
||||||
|
m.Log.Debugf("trying to read holding@%v[%v]...", request.address, request.length)
|
||||||
|
bytes, err := m.client.ReadHoldingRegisters(request.address, request.length)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Log.Debugf("got holding@%v[%v]: %v", request.address, request.length, bytes)
|
||||||
|
|
||||||
|
// Non-bit value handling
|
||||||
|
for i, field := range request.fields {
|
||||||
|
// Determine the offset of the field values in the read array
|
||||||
|
offset := 2 * (field.address - request.address) // registers are 16bit = 2 byte
|
||||||
|
length := 2 * field.length // field length is in registers a 16bit
|
||||||
|
|
||||||
|
// Convert the actual value
|
||||||
|
request.fields[i].value = field.converter(bytes[offset : offset+length])
|
||||||
|
m.Log.Debugf(" field %s with offset %d with len %d: %v --> %v", field.name, offset, length, bytes[offset:offset+length], request.fields[i].value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Modbus) gatherRequestsInput(requests []request) error {
|
||||||
|
for _, request := range requests {
|
||||||
|
m.Log.Debugf("trying to read input@%v[%v]...", request.address, request.length)
|
||||||
|
bytes, err := m.client.ReadInputRegisters(request.address, request.length)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.Log.Debugf("got input@%v[%v]: %v", request.address, request.length, bytes)
|
||||||
|
|
||||||
|
// Non-bit value handling
|
||||||
|
for i, field := range request.fields {
|
||||||
|
// Determine the offset of the field values in the read array
|
||||||
|
offset := 2 * (field.address - request.address) // registers are 16bit = 2 byte
|
||||||
|
length := 2 * field.length // field length is in registers a 16bit
|
||||||
|
|
||||||
|
// Convert the actual value
|
||||||
|
request.fields[i].value = field.converter(bytes[offset : offset+length])
|
||||||
|
m.Log.Debugf(" field %s with offset %d with len %d: %v --> %v", field.name, offset, length, bytes[offset:offset+length], request.fields[i].value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Modbus) collectFields(acc telegraf.Accumulator, timestamp time.Time, tags map[string]string, requests []request) {
|
||||||
|
grouper := metric.NewSeriesGrouper()
|
||||||
|
for _, request := range requests {
|
||||||
|
for _, field := range request.fields {
|
||||||
|
// In case no measurement was specified we use "modbus" as default
|
||||||
|
measurement := "modbus"
|
||||||
|
if field.measurement != "" {
|
||||||
|
measurement = field.measurement
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group the data by series
|
||||||
|
if err := grouper.Add(measurement, tags, timestamp, field.name, field.value); err != nil {
|
||||||
|
acc.AddError(fmt.Errorf("cannot add field %q for measurement %q: %v", field.name, measurement, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the metrics grouped by series to the accumulator
|
||||||
|
for _, x := range grouper.Metrics() {
|
||||||
|
acc.AddMetric(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Add this plugin to telegraf
|
// Add this plugin to telegraf
|
||||||
func init() {
|
func init() {
|
||||||
inputs.Add("modbus", func() telegraf.Input { return &Modbus{} })
|
inputs.Add("modbus", func() telegraf.Input { return &Modbus{} })
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@ package modbus
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
m "github.com/goburrow/modbus"
|
mb "github.com/grid-x/modbus"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/require"
|
||||||
"github.com/tbrandon/mbserver"
|
"github.com/tbrandon/mbserver"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
"github.com/influxdata/telegraf/testutil"
|
"github.com/influxdata/telegraf/testutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -78,44 +81,52 @@ func TestCoils(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
serv := mbserver.NewServer()
|
serv := mbserver.NewServer()
|
||||||
err := serv.ListenTCP("localhost:1502")
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
defer serv.Close()
|
defer serv.Close()
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
handler := m.NewTCPClientHandler("localhost:1502")
|
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||||
err = handler.Connect()
|
require.NoError(t, handler.Connect())
|
||||||
assert.NoError(t, err)
|
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
client := m.NewClient(handler)
|
client := mb.NewClient(handler)
|
||||||
|
|
||||||
for _, ct := range coilTests {
|
for _, ct := range coilTests {
|
||||||
t.Run(ct.name, func(t *testing.T) {
|
t.Run(ct.name, func(t *testing.T) {
|
||||||
_, err = client.WriteMultipleCoils(ct.address, ct.quantity, ct.write)
|
_, err := client.WriteMultipleCoils(ct.address, ct.quantity, ct.write)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
modbus := Modbus{
|
modbus := Modbus{
|
||||||
Name: "TestCoils",
|
Name: "TestCoils",
|
||||||
Controller: "tcp://localhost:1502",
|
Controller: "tcp://localhost:1502",
|
||||||
SlaveID: 1,
|
Log: testutil.Logger{},
|
||||||
Coils: []fieldContainer{
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.Coils = []fieldDefinition{
|
||||||
{
|
{
|
||||||
Name: ct.name,
|
Name: ct.name,
|
||||||
Address: []uint16{ct.address},
|
Address: []uint16{ct.address},
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"modbus",
|
||||||
|
map[string]string{
|
||||||
|
"type": cCoils,
|
||||||
|
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||||
|
"name": modbus.Name,
|
||||||
},
|
},
|
||||||
Log: testutil.Logger{},
|
map[string]interface{}{ct.name: ct.read},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = modbus.Init()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
err = modbus.Gather(&acc)
|
require.NoError(t, modbus.Init())
|
||||||
assert.NoError(t, err)
|
require.NotEmpty(t, modbus.requests)
|
||||||
assert.NotEmpty(t, modbus.registers)
|
require.NoError(t, modbus.Gather(&acc))
|
||||||
|
acc.Wait(len(expected))
|
||||||
|
|
||||||
for _, coil := range modbus.registers {
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
assert.Equal(t, ct.read, coil.Fields[0].value)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -614,26 +625,26 @@ func TestHoldingRegisters(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
serv := mbserver.NewServer()
|
serv := mbserver.NewServer()
|
||||||
err := serv.ListenTCP("localhost:1502")
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
defer serv.Close()
|
defer serv.Close()
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
handler := m.NewTCPClientHandler("localhost:1502")
|
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||||
err = handler.Connect()
|
require.NoError(t, handler.Connect())
|
||||||
assert.NoError(t, err)
|
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
client := m.NewClient(handler)
|
client := mb.NewClient(handler)
|
||||||
|
|
||||||
for _, hrt := range holdingRegisterTests {
|
for _, hrt := range holdingRegisterTests {
|
||||||
t.Run(hrt.name, func(t *testing.T) {
|
t.Run(hrt.name, func(t *testing.T) {
|
||||||
_, err = client.WriteMultipleRegisters(hrt.address[0], hrt.quantity, hrt.write)
|
_, err := client.WriteMultipleRegisters(hrt.address[0], hrt.quantity, hrt.write)
|
||||||
assert.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
modbus := Modbus{
|
modbus := Modbus{
|
||||||
Name: "TestHoldingRegisters",
|
Name: "TestHoldingRegisters",
|
||||||
Controller: "tcp://localhost:1502",
|
Controller: "tcp://localhost:1502",
|
||||||
SlaveID: 1,
|
Log: testutil.Logger{},
|
||||||
HoldingRegisters: []fieldContainer{
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.HoldingRegisters = []fieldDefinition{
|
||||||
{
|
{
|
||||||
Name: hrt.name,
|
Name: hrt.name,
|
||||||
ByteOrder: hrt.byteOrder,
|
ByteOrder: hrt.byteOrder,
|
||||||
|
|
@ -641,88 +652,264 @@ func TestHoldingRegisters(t *testing.T) {
|
||||||
Scale: hrt.scale,
|
Scale: hrt.scale,
|
||||||
Address: hrt.address,
|
Address: hrt.address,
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"modbus",
|
||||||
|
map[string]string{
|
||||||
|
"type": cHoldingRegisters,
|
||||||
|
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||||
|
"name": modbus.Name,
|
||||||
},
|
},
|
||||||
Log: testutil.Logger{},
|
map[string]interface{}{hrt.name: hrt.read},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = modbus.Init()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
assert.NoError(t, modbus.Gather(&acc))
|
require.NoError(t, modbus.Init())
|
||||||
assert.NotEmpty(t, modbus.registers)
|
require.NotEmpty(t, modbus.requests)
|
||||||
|
require.NoError(t, modbus.Gather(&acc))
|
||||||
|
acc.Wait(len(expected))
|
||||||
|
|
||||||
for _, coil := range modbus.registers {
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
assert.Equal(t, hrt.read, coil.Fields[0].value)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadMultipleCoilLimit(t *testing.T) {
|
func TestReadMultipleCoilWithHole(t *testing.T) {
|
||||||
serv := mbserver.NewServer()
|
serv := mbserver.NewServer()
|
||||||
err := serv.ListenTCP("localhost:1502")
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
assert.NoError(t, err)
|
|
||||||
defer serv.Close()
|
defer serv.Close()
|
||||||
|
|
||||||
handler := m.NewTCPClientHandler("localhost:1502")
|
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||||
err = handler.Connect()
|
require.NoError(t, handler.Connect())
|
||||||
assert.NoError(t, err)
|
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
client := m.NewClient(handler)
|
client := mb.NewClient(handler)
|
||||||
|
|
||||||
fcs := []fieldContainer{}
|
fcs := []fieldDefinition{}
|
||||||
|
expectedFields := make(map[string]interface{})
|
||||||
writeValue := uint16(0)
|
writeValue := uint16(0)
|
||||||
for i := 0; i <= 4000; i++ {
|
readValue := uint16(0)
|
||||||
fc := fieldContainer{}
|
for i := 0; i < 14; i++ {
|
||||||
|
fc := fieldDefinition{}
|
||||||
fc.Name = fmt.Sprintf("coil-%v", i)
|
fc.Name = fmt.Sprintf("coil-%v", i)
|
||||||
fc.Address = []uint16{uint16(i)}
|
fc.Address = []uint16{uint16(i)}
|
||||||
fcs = append(fcs, fc)
|
fcs = append(fcs, fc)
|
||||||
|
|
||||||
t.Run(fc.Name, func(t *testing.T) {
|
_, err := client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||||
_, err = client.WriteSingleCoil(fc.Address[0], writeValue)
|
require.NoError(t, err)
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
expectedFields[fc.Name] = readValue
|
||||||
writeValue = 65280 - writeValue
|
writeValue = 65280 - writeValue
|
||||||
|
readValue = 1 - readValue
|
||||||
}
|
}
|
||||||
|
for i := 15; i < 18; i++ {
|
||||||
|
fc := fieldDefinition{}
|
||||||
|
fc.Name = fmt.Sprintf("coil-%v", i)
|
||||||
|
fc.Address = []uint16{uint16(i)}
|
||||||
|
fcs = append(fcs, fc)
|
||||||
|
|
||||||
|
_, err := client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedFields[fc.Name] = readValue
|
||||||
|
writeValue = 65280 - writeValue
|
||||||
|
readValue = 1 - readValue
|
||||||
|
}
|
||||||
|
for i := 24; i < 33; i++ {
|
||||||
|
fc := fieldDefinition{}
|
||||||
|
fc.Name = fmt.Sprintf("coil-%v", i)
|
||||||
|
fc.Address = []uint16{uint16(i)}
|
||||||
|
fcs = append(fcs, fc)
|
||||||
|
|
||||||
|
_, err := client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedFields[fc.Name] = readValue
|
||||||
|
writeValue = 65280 - writeValue
|
||||||
|
readValue = 1 - readValue
|
||||||
|
}
|
||||||
|
require.Len(t, expectedFields, len(fcs))
|
||||||
|
|
||||||
|
modbus := Modbus{
|
||||||
|
Name: "TestReadMultipleCoilWithHole",
|
||||||
|
Controller: "tcp://localhost:1502",
|
||||||
|
Log: testutil.Logger{Name: "modbus:MultipleCoilWithHole"},
|
||||||
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.Coils = fcs
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"modbus",
|
||||||
|
map[string]string{
|
||||||
|
"type": cCoils,
|
||||||
|
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||||
|
"name": modbus.Name,
|
||||||
|
},
|
||||||
|
expectedFields,
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
require.NoError(t, modbus.Init())
|
||||||
|
require.NotEmpty(t, modbus.requests)
|
||||||
|
require.NoError(t, modbus.Gather(&acc))
|
||||||
|
acc.Wait(len(expected))
|
||||||
|
|
||||||
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadMultipleCoilLimit(t *testing.T) {
|
||||||
|
serv := mbserver.NewServer()
|
||||||
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
|
defer serv.Close()
|
||||||
|
|
||||||
|
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||||
|
require.NoError(t, handler.Connect())
|
||||||
|
defer handler.Close()
|
||||||
|
client := mb.NewClient(handler)
|
||||||
|
|
||||||
|
fcs := []fieldDefinition{}
|
||||||
|
expectedFields := make(map[string]interface{})
|
||||||
|
writeValue := uint16(0)
|
||||||
|
readValue := uint16(0)
|
||||||
|
for i := 0; i < 4000; i++ {
|
||||||
|
fc := fieldDefinition{}
|
||||||
|
fc.Name = fmt.Sprintf("coil-%v", i)
|
||||||
|
fc.Address = []uint16{uint16(i)}
|
||||||
|
fcs = append(fcs, fc)
|
||||||
|
|
||||||
|
_, err := client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedFields[fc.Name] = readValue
|
||||||
|
writeValue = 65280 - writeValue
|
||||||
|
readValue = 1 - readValue
|
||||||
|
}
|
||||||
|
require.Len(t, expectedFields, len(fcs))
|
||||||
|
|
||||||
modbus := Modbus{
|
modbus := Modbus{
|
||||||
Name: "TestReadCoils",
|
Name: "TestReadCoils",
|
||||||
Controller: "tcp://localhost:1502",
|
Controller: "tcp://localhost:1502",
|
||||||
SlaveID: 1,
|
Log: testutil.Logger{},
|
||||||
Coils: fcs,
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.Coils = fcs
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"modbus",
|
||||||
|
map[string]string{
|
||||||
|
"type": cCoils,
|
||||||
|
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||||
|
"name": modbus.Name,
|
||||||
|
},
|
||||||
|
expectedFields,
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = modbus.Init()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
err = modbus.Gather(&acc)
|
require.NoError(t, modbus.Init())
|
||||||
assert.NoError(t, err)
|
require.NotEmpty(t, modbus.requests)
|
||||||
|
require.NoError(t, modbus.Gather(&acc))
|
||||||
|
acc.Wait(len(expected))
|
||||||
|
|
||||||
writeValue = 0
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
for i := 0; i <= 4000; i++ {
|
}
|
||||||
t.Run(modbus.registers[0].Fields[i].Name, func(t *testing.T) {
|
|
||||||
assert.Equal(t, writeValue, modbus.registers[0].Fields[i].value)
|
func TestReadMultipleHoldingRegisterWithHole(t *testing.T) {
|
||||||
writeValue = 1 - writeValue
|
serv := mbserver.NewServer()
|
||||||
})
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
|
defer serv.Close()
|
||||||
|
|
||||||
|
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||||
|
require.NoError(t, handler.Connect())
|
||||||
|
defer handler.Close()
|
||||||
|
client := mb.NewClient(handler)
|
||||||
|
|
||||||
|
fcs := []fieldDefinition{}
|
||||||
|
expectedFields := make(map[string]interface{})
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
fc := fieldDefinition{
|
||||||
|
Name: fmt.Sprintf("HoldingRegister-%v", i),
|
||||||
|
ByteOrder: "AB",
|
||||||
|
DataType: "INT16",
|
||||||
|
Scale: 1.0,
|
||||||
|
Address: []uint16{uint16(i)},
|
||||||
}
|
}
|
||||||
|
fcs = append(fcs, fc)
|
||||||
|
|
||||||
|
_, err := client.WriteSingleRegister(fc.Address[0], uint16(i))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedFields[fc.Name] = int64(i)
|
||||||
|
}
|
||||||
|
for i := 20; i < 30; i++ {
|
||||||
|
fc := fieldDefinition{
|
||||||
|
Name: fmt.Sprintf("HoldingRegister-%v", i),
|
||||||
|
ByteOrder: "AB",
|
||||||
|
DataType: "INT16",
|
||||||
|
Scale: 1.0,
|
||||||
|
Address: []uint16{uint16(i)},
|
||||||
|
}
|
||||||
|
fcs = append(fcs, fc)
|
||||||
|
|
||||||
|
_, err := client.WriteSingleRegister(fc.Address[0], uint16(i))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedFields[fc.Name] = int64(i)
|
||||||
|
}
|
||||||
|
require.Len(t, expectedFields, len(fcs))
|
||||||
|
|
||||||
|
modbus := Modbus{
|
||||||
|
Name: "TestHoldingRegister",
|
||||||
|
Controller: "tcp://localhost:1502",
|
||||||
|
Log: testutil.Logger{},
|
||||||
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.HoldingRegisters = fcs
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"modbus",
|
||||||
|
map[string]string{
|
||||||
|
"type": cHoldingRegisters,
|
||||||
|
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||||
|
"name": modbus.Name,
|
||||||
|
},
|
||||||
|
expectedFields,
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
require.NoError(t, modbus.Init())
|
||||||
|
require.NotEmpty(t, modbus.requests)
|
||||||
|
require.NoError(t, modbus.Gather(&acc))
|
||||||
|
acc.Wait(len(expected))
|
||||||
|
|
||||||
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadMultipleHoldingRegisterLimit(t *testing.T) {
|
func TestReadMultipleHoldingRegisterLimit(t *testing.T) {
|
||||||
serv := mbserver.NewServer()
|
serv := mbserver.NewServer()
|
||||||
err := serv.ListenTCP("localhost:1502")
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
assert.NoError(t, err)
|
|
||||||
defer serv.Close()
|
defer serv.Close()
|
||||||
|
|
||||||
handler := m.NewTCPClientHandler("localhost:1502")
|
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||||
err = handler.Connect()
|
require.NoError(t, handler.Connect())
|
||||||
assert.NoError(t, err)
|
|
||||||
defer handler.Close()
|
defer handler.Close()
|
||||||
client := m.NewClient(handler)
|
client := mb.NewClient(handler)
|
||||||
|
|
||||||
fcs := []fieldContainer{}
|
fcs := []fieldDefinition{}
|
||||||
|
expectedFields := make(map[string]interface{})
|
||||||
for i := 0; i <= 400; i++ {
|
for i := 0; i <= 400; i++ {
|
||||||
fc := fieldContainer{}
|
fc := fieldDefinition{}
|
||||||
fc.Name = fmt.Sprintf("HoldingRegister-%v", i)
|
fc.Name = fmt.Sprintf("HoldingRegister-%v", i)
|
||||||
fc.ByteOrder = "AB"
|
fc.ByteOrder = "AB"
|
||||||
fc.DataType = "INT16"
|
fc.DataType = "INT16"
|
||||||
|
|
@ -730,28 +917,40 @@ func TestReadMultipleHoldingRegisterLimit(t *testing.T) {
|
||||||
fc.Address = []uint16{uint16(i)}
|
fc.Address = []uint16{uint16(i)}
|
||||||
fcs = append(fcs, fc)
|
fcs = append(fcs, fc)
|
||||||
|
|
||||||
t.Run(fc.Name, func(t *testing.T) {
|
_, err := client.WriteSingleRegister(fc.Address[0], uint16(i))
|
||||||
_, err = client.WriteSingleRegister(fc.Address[0], uint16(i))
|
require.NoError(t, err)
|
||||||
assert.NoError(t, err)
|
|
||||||
})
|
expectedFields[fc.Name] = int64(i)
|
||||||
}
|
}
|
||||||
|
|
||||||
modbus := Modbus{
|
modbus := Modbus{
|
||||||
Name: "TestHoldingRegister",
|
Name: "TestHoldingRegister",
|
||||||
Controller: "tcp://localhost:1502",
|
Controller: "tcp://localhost:1502",
|
||||||
SlaveID: 1,
|
Log: testutil.Logger{},
|
||||||
HoldingRegisters: fcs,
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.HoldingRegisters = fcs
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"modbus",
|
||||||
|
map[string]string{
|
||||||
|
"type": cHoldingRegisters,
|
||||||
|
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||||
|
"name": modbus.Name,
|
||||||
|
},
|
||||||
|
expectedFields,
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = modbus.Init()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
err = modbus.Gather(&acc)
|
require.NoError(t, modbus.Init())
|
||||||
assert.NoError(t, err)
|
require.NotEmpty(t, modbus.requests)
|
||||||
|
require.NoError(t, modbus.Gather(&acc))
|
||||||
|
acc.Wait(len(expected))
|
||||||
|
|
||||||
for i := 0; i <= 400; i++ {
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
assert.Equal(t, int16(i), modbus.registers[0].Fields[i].value)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRetrySuccessful(t *testing.T) {
|
func TestRetrySuccessful(t *testing.T) {
|
||||||
|
|
@ -760,8 +959,7 @@ func TestRetrySuccessful(t *testing.T) {
|
||||||
value := 1
|
value := 1
|
||||||
|
|
||||||
serv := mbserver.NewServer()
|
serv := mbserver.NewServer()
|
||||||
err := serv.ListenTCP("localhost:1502")
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
assert.NoError(t, err)
|
|
||||||
defer serv.Close()
|
defer serv.Close()
|
||||||
|
|
||||||
// Make read on coil-registers fail for some trials by making the device
|
// Make read on coil-registers fail for some trials by making the device
|
||||||
|
|
@ -781,40 +979,47 @@ func TestRetrySuccessful(t *testing.T) {
|
||||||
return data, except
|
return data, except
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("retry_success", func(t *testing.T) {
|
|
||||||
modbus := Modbus{
|
modbus := Modbus{
|
||||||
Name: "TestRetry",
|
Name: "TestRetry",
|
||||||
Controller: "tcp://localhost:1502",
|
Controller: "tcp://localhost:1502",
|
||||||
SlaveID: 1,
|
|
||||||
Retries: maxretries,
|
Retries: maxretries,
|
||||||
Coils: []fieldContainer{
|
Log: testutil.Logger{},
|
||||||
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.Coils = []fieldDefinition{
|
||||||
{
|
{
|
||||||
Name: "retry_success",
|
Name: "retry_success",
|
||||||
Address: []uint16{0},
|
Address: []uint16{0},
|
||||||
},
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := []telegraf.Metric{
|
||||||
|
testutil.MustMetric(
|
||||||
|
"modbus",
|
||||||
|
map[string]string{
|
||||||
|
"type": cCoils,
|
||||||
|
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||||
|
"name": modbus.Name,
|
||||||
},
|
},
|
||||||
Log: testutil.Logger{},
|
map[string]interface{}{"retry_success": uint16(value)},
|
||||||
|
time.Unix(0, 0),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
err = modbus.Init()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
err = modbus.Gather(&acc)
|
require.NoError(t, modbus.Init())
|
||||||
assert.NoError(t, err)
|
require.NotEmpty(t, modbus.requests)
|
||||||
assert.NotEmpty(t, modbus.registers)
|
require.NoError(t, modbus.Gather(&acc))
|
||||||
|
acc.Wait(len(expected))
|
||||||
|
|
||||||
for _, coil := range modbus.registers {
|
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||||
assert.Equal(t, uint16(value), coil.Fields[0].value)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRetryFail(t *testing.T) {
|
func TestRetryFailExhausted(t *testing.T) {
|
||||||
maxretries := 2
|
maxretries := 2
|
||||||
|
|
||||||
serv := mbserver.NewServer()
|
serv := mbserver.NewServer()
|
||||||
err := serv.ListenTCP("localhost:1502")
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
assert.NoError(t, err)
|
|
||||||
defer serv.Close()
|
defer serv.Close()
|
||||||
|
|
||||||
// Make the read on coils fail with busy
|
// Make the read on coils fail with busy
|
||||||
|
|
@ -827,27 +1032,35 @@ func TestRetryFail(t *testing.T) {
|
||||||
return data, &mbserver.SlaveDeviceBusy
|
return data, &mbserver.SlaveDeviceBusy
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("retry_fail", func(t *testing.T) {
|
|
||||||
modbus := Modbus{
|
modbus := Modbus{
|
||||||
Name: "TestRetryFail",
|
Name: "TestRetryFailExhausted",
|
||||||
Controller: "tcp://localhost:1502",
|
Controller: "tcp://localhost:1502",
|
||||||
SlaveID: 1,
|
|
||||||
Retries: maxretries,
|
Retries: maxretries,
|
||||||
Coils: []fieldContainer{
|
Log: testutil.Logger{},
|
||||||
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.Coils = []fieldDefinition{
|
||||||
{
|
{
|
||||||
Name: "retry_fail",
|
Name: "retry_fail",
|
||||||
Address: []uint16{0},
|
Address: []uint16{0},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
Log: testutil.Logger{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = modbus.Init()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
err = modbus.Gather(&acc)
|
require.NoError(t, modbus.Init())
|
||||||
assert.Error(t, err)
|
require.NotEmpty(t, modbus.requests)
|
||||||
})
|
|
||||||
|
err := modbus.Gather(&acc)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, "modbus: exception '6' (server device busy), function '129'", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryFailIllegal(t *testing.T) {
|
||||||
|
maxretries := 2
|
||||||
|
|
||||||
|
serv := mbserver.NewServer()
|
||||||
|
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||||
|
defer serv.Close()
|
||||||
|
|
||||||
// Make the read on coils fail with illegal function preventing retry
|
// Make the read on coils fail with illegal function preventing retry
|
||||||
counter := 0
|
counter := 0
|
||||||
|
|
@ -861,26 +1074,26 @@ func TestRetryFail(t *testing.T) {
|
||||||
return data, &mbserver.IllegalFunction
|
return data, &mbserver.IllegalFunction
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("retry_fail", func(t *testing.T) {
|
|
||||||
modbus := Modbus{
|
modbus := Modbus{
|
||||||
Name: "TestRetryFail",
|
Name: "TestRetryFailExhausted",
|
||||||
Controller: "tcp://localhost:1502",
|
Controller: "tcp://localhost:1502",
|
||||||
SlaveID: 1,
|
|
||||||
Retries: maxretries,
|
Retries: maxretries,
|
||||||
Coils: []fieldContainer{
|
Log: testutil.Logger{},
|
||||||
|
}
|
||||||
|
modbus.SlaveID = 1
|
||||||
|
modbus.Coils = []fieldDefinition{
|
||||||
{
|
{
|
||||||
Name: "retry_fail",
|
Name: "retry_fail",
|
||||||
Address: []uint16{0},
|
Address: []uint16{0},
|
||||||
},
|
},
|
||||||
},
|
|
||||||
Log: testutil.Logger{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = modbus.Init()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
var acc testutil.Accumulator
|
var acc testutil.Accumulator
|
||||||
err = modbus.Gather(&acc)
|
require.NoError(t, modbus.Init())
|
||||||
assert.Error(t, err)
|
require.NotEmpty(t, modbus.requests)
|
||||||
assert.Equal(t, counter, 1)
|
|
||||||
})
|
err := modbus.Gather(&acc)
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, "modbus: exception '1' (illegal function), function '129'", err.Error())
|
||||||
|
require.Equal(t, counter, 1)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package modbus
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
type request struct {
|
||||||
|
address uint16
|
||||||
|
length uint16
|
||||||
|
fields []field
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequestsFromFields(fields []field, slaveID byte, registerType string, maxBatchSize uint16) []request {
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort the fields by address (ascending) and length
|
||||||
|
sort.Slice(fields, func(i, j int) bool {
|
||||||
|
addrI := fields[i].address
|
||||||
|
addrJ := fields[j].address
|
||||||
|
return addrI < addrJ || (addrI == addrJ && fields[i].length > fields[j].length)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Construct the consecutive register chunks for the addresses and construct Modbus requests.
|
||||||
|
// For field addresses like [1, 2, 3, 5, 6, 10, 11, 12, 14] we should construct the following
|
||||||
|
// requests (1, 3) , (5, 2) , (10, 3), (14 , 1). Furthermore, we should respect field boundaries
|
||||||
|
// and the given maximum chunk sizes.
|
||||||
|
var requests []request
|
||||||
|
|
||||||
|
current := request{
|
||||||
|
address: fields[0].address,
|
||||||
|
length: fields[0].length,
|
||||||
|
fields: []field{fields[0]},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, f := range fields[1:] {
|
||||||
|
// Check if we need to interrupt the current chunk and require a new one
|
||||||
|
needInterrupt := f.address != current.address+current.length // not consecutive
|
||||||
|
needInterrupt = needInterrupt || f.length+current.length > maxBatchSize // too large
|
||||||
|
|
||||||
|
if !needInterrupt {
|
||||||
|
// Still save to add the field to the current request
|
||||||
|
current.length += f.length
|
||||||
|
current.fields = append(current.fields, f) // TODO: omit the field with a future flag
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish the current request, add it to the list and construct a new one
|
||||||
|
requests = append(requests, current)
|
||||||
|
current = request{
|
||||||
|
address: f.address,
|
||||||
|
length: f.length,
|
||||||
|
fields: []field{f},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requests = append(requests, current)
|
||||||
|
|
||||||
|
return requests
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
package modbus
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func determineConverter(inType, byteOrder, outType string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
if scale != 0.0 {
|
||||||
|
return determineConverterScale(inType, byteOrder, outType, scale)
|
||||||
|
}
|
||||||
|
return determineConverterNoScale(inType, byteOrder, outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func determineConverterScale(inType, byteOrder, outType string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
switch inType {
|
||||||
|
case "INT16":
|
||||||
|
return determineConverterI16Scale(outType, byteOrder, scale)
|
||||||
|
case "UINT16":
|
||||||
|
return determineConverterU16Scale(outType, byteOrder, scale)
|
||||||
|
case "INT32":
|
||||||
|
return determineConverterI32Scale(outType, byteOrder, scale)
|
||||||
|
case "UINT32":
|
||||||
|
return determineConverterU32Scale(outType, byteOrder, scale)
|
||||||
|
case "INT64":
|
||||||
|
return determineConverterI64Scale(outType, byteOrder, scale)
|
||||||
|
case "UINT64":
|
||||||
|
return determineConverterU64Scale(outType, byteOrder, scale)
|
||||||
|
case "FLOAT32":
|
||||||
|
return determineConverterF32Scale(outType, byteOrder, scale)
|
||||||
|
case "FLOAT64":
|
||||||
|
return determineConverterF64Scale(outType, byteOrder, scale)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid input data-type: %s", inType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func determineConverterNoScale(inType, byteOrder, outType string) (fieldConverterFunc, error) {
|
||||||
|
switch inType {
|
||||||
|
case "INT16":
|
||||||
|
return determineConverterI16(outType, byteOrder)
|
||||||
|
case "UINT16":
|
||||||
|
return determineConverterU16(outType, byteOrder)
|
||||||
|
case "INT32":
|
||||||
|
return determineConverterI32(outType, byteOrder)
|
||||||
|
case "UINT32":
|
||||||
|
return determineConverterU32(outType, byteOrder)
|
||||||
|
case "INT64":
|
||||||
|
return determineConverterI64(outType, byteOrder)
|
||||||
|
case "UINT64":
|
||||||
|
return determineConverterU64(outType, byteOrder)
|
||||||
|
case "FLOAT32":
|
||||||
|
return determineConverterF32(outType, byteOrder)
|
||||||
|
case "FLOAT64":
|
||||||
|
return determineConverterF64(outType, byteOrder)
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid input data-type: %s", inType)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
package modbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type convert16 func([]byte) uint16
|
||||||
|
|
||||||
|
func endianessConverter16(byteOrder string) (convert16, error) {
|
||||||
|
switch byteOrder {
|
||||||
|
case "ABCD": // Big endian (Motorola)
|
||||||
|
return binary.BigEndian.Uint16, nil
|
||||||
|
case "DCBA": // Little endian (Intel)
|
||||||
|
return binary.LittleEndian.Uint16, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid byte-order: %s", byteOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I16 - no scale
|
||||||
|
func determineConverterI16(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter16(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return int16(tohost(b))
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return int64(int16(tohost(b)))
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return uint64(int16(tohost(b)))
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return float64(int16(tohost(b)))
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// U16 - no scale
|
||||||
|
func determineConverterU16(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter16(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return tohost(b)
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return int64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return uint64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return float64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I16 - scale
|
||||||
|
func determineConverterI16Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter16(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int16(tohost(b))
|
||||||
|
return int16(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int16(tohost(b))
|
||||||
|
return int64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int16(tohost(b))
|
||||||
|
return uint64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int16(tohost(b))
|
||||||
|
return float64(in) * scale
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// U16 - scale
|
||||||
|
func determineConverterU16Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter16(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return uint16(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return int64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return uint64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return float64(in) * scale
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
package modbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type convert32 func([]byte) uint32
|
||||||
|
|
||||||
|
func binaryMSWLEU32(b []byte) uint32 {
|
||||||
|
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
|
||||||
|
return uint32(binary.LittleEndian.Uint16(b[0:]))<<16 | uint32(binary.LittleEndian.Uint16(b[2:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func binaryLSWBEU32(b []byte) uint32 {
|
||||||
|
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
|
||||||
|
return uint32(binary.BigEndian.Uint16(b[2:]))<<16 | uint32(binary.BigEndian.Uint16(b[0:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func endianessConverter32(byteOrder string) (convert32, error) {
|
||||||
|
switch byteOrder {
|
||||||
|
case "ABCD": // Big endian (Motorola)
|
||||||
|
return binary.BigEndian.Uint32, nil
|
||||||
|
case "BADC": // Big endian with bytes swapped
|
||||||
|
return binaryMSWLEU32, nil
|
||||||
|
case "CDAB": // Little endian with bytes swapped
|
||||||
|
return binaryLSWBEU32, nil
|
||||||
|
case "DCBA": // Little endian (Intel)
|
||||||
|
return binary.LittleEndian.Uint32, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid byte-order: %s", byteOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I32 - no scale
|
||||||
|
func determineConverterI32(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter32(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return int32(tohost(b))
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return int64(int32(tohost(b)))
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return uint64(int32(tohost(b)))
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return float64(int32(tohost(b)))
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// U32 - no scale
|
||||||
|
func determineConverterU32(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter32(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return tohost(b)
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return int64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return uint64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return float64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F32 - no scale
|
||||||
|
func determineConverterF32(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter32(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
raw := tohost(b)
|
||||||
|
return math.Float32frombits(raw)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
raw := tohost(b)
|
||||||
|
in := math.Float32frombits(raw)
|
||||||
|
return float64(in)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I32 - scale
|
||||||
|
func determineConverterI32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter32(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int32(tohost(b))
|
||||||
|
return int32(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int32(tohost(b))
|
||||||
|
return int64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int32(tohost(b))
|
||||||
|
return uint64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int32(tohost(b))
|
||||||
|
return float64(in) * scale
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// U32 - scale
|
||||||
|
func determineConverterU32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter32(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return uint32(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return int64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return uint64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return float64(in) * scale
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F32 - scale
|
||||||
|
func determineConverterF32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter32(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
raw := tohost(b)
|
||||||
|
in := math.Float32frombits(raw)
|
||||||
|
return float32(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
raw := tohost(b)
|
||||||
|
in := math.Float32frombits(raw)
|
||||||
|
return float64(in) * scale
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,182 @@
|
||||||
|
package modbus
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
)
|
||||||
|
|
||||||
|
type convert64 func([]byte) uint64
|
||||||
|
|
||||||
|
func binaryMSWLEU64(b []byte) uint64 {
|
||||||
|
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
|
||||||
|
return uint64(binary.LittleEndian.Uint16(b[0:]))<<48 | uint64(binary.LittleEndian.Uint16(b[2:]))<<32 | uint64(binary.LittleEndian.Uint16(b[4:]))<<16 | uint64(binary.LittleEndian.Uint16(b[6:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func binaryLSWBEU64(b []byte) uint64 {
|
||||||
|
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
|
||||||
|
return uint64(binary.BigEndian.Uint16(b[6:]))<<48 | uint64(binary.BigEndian.Uint16(b[4:]))<<32 | uint64(binary.BigEndian.Uint16(b[2:]))<<16 | uint64(binary.BigEndian.Uint16(b[0:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func endianessConverter64(byteOrder string) (convert64, error) {
|
||||||
|
switch byteOrder {
|
||||||
|
case "ABCD": // Big endian (Motorola)
|
||||||
|
return binary.BigEndian.Uint64, nil
|
||||||
|
case "BADC": // Big endian with bytes swapped
|
||||||
|
return binaryMSWLEU64, nil
|
||||||
|
case "CDAB": // Little endian with bytes swapped
|
||||||
|
return binaryLSWBEU64, nil
|
||||||
|
case "DCBA": // Little endian (Intel)
|
||||||
|
return binary.LittleEndian.Uint64, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid byte-order: %s", byteOrder)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I64 - no scale
|
||||||
|
func determineConverterI64(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter64(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native", "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return int64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int64(tohost(b))
|
||||||
|
return uint64(in)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int64(tohost(b))
|
||||||
|
return float64(in)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// U64 - no scale
|
||||||
|
func determineConverterU64(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter64(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return int64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
case "native", "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return tohost(b)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
return float64(tohost(b))
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F64 - no scale
|
||||||
|
func determineConverterF64(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter64(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native", "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
raw := tohost(b)
|
||||||
|
return math.Float64frombits(raw)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// I64 - scale
|
||||||
|
func determineConverterI64Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter64(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int64(tohost(b))
|
||||||
|
return int64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int64(tohost(b))
|
||||||
|
return int64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int64(tohost(b))
|
||||||
|
return uint64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := int64(tohost(b))
|
||||||
|
return float64(in) * scale
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// U64 - scale
|
||||||
|
func determineConverterU64Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter64(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return uint64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "INT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return int64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "UINT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return uint64(float64(in) * scale)
|
||||||
|
}, nil
|
||||||
|
case "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
in := tohost(b)
|
||||||
|
return float64(in) * scale
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
|
|
||||||
|
// F64 - scale
|
||||||
|
func determineConverterF64Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endianessConverter64(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch outType {
|
||||||
|
case "native", "FLOAT64":
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
raw := tohost(b)
|
||||||
|
in := math.Float64frombits(raw)
|
||||||
|
return in * scale
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue