Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modbus rework #9141

Merged
merged 35 commits into from
May 27, 2021
Merged
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b01e08d
Move to maintained 'github.com/grid-x/modbus' almost drop-in replacem…
Apr 14, 2021
da20f23
Reorganize modbus plugin code to factor out time-conversions and conf…
Apr 14, 2021
b01e144
Move type conversion handling to separate files for maintainability.
Apr 16, 2021
c31392c
Factor out configuration handling and type-conversion initialization.
Apr 16, 2021
6c92cf6
Rename 'register' type to 'request' as this better reflects what it is.
Apr 16, 2021
db010d9
Move from 'assert' to 'require' for testing.
Apr 16, 2021
5892fe3
Change construction of requests such that each request represents a r…
Apr 16, 2021
5410b2f
Add test for field-definition with holes in the address ranges.
Apr 16, 2021
1ddcd81
Simplify register init in the original configuration.
Apr 16, 2021
1b6dcbb
Simplify getFields() method.
Apr 16, 2021
12a77bd
Also make the non-exported structs' fields private.
Apr 16, 2021
1b01f39
Simplify connection handling and de-entangle configuration processing.
Apr 16, 2021
ea66c93
Rework request initialization and factor out requests.
Apr 16, 2021
3bce242
Cleanup testing and switch to metric-based testing.
Apr 16, 2021
dd21275
Prepare handling of multiple slave-devices.
Apr 16, 2021
c58291b
Formatting.
Apr 16, 2021
7bf3a4c
Shorten code a bit.
Apr 16, 2021
f5f8381
Move non-exported functions to the end of the file.
Apr 16, 2021
beeb493
Fix linter issues in configuration handling.
Apr 16, 2021
98ee3e4
Fix linter issues in requests.
Apr 16, 2021
91884cf
Fix linter issues in type-conversions.
Apr 16, 2021
3c6f3ca
Fix license documentation.
Apr 18, 2021
13c0e16
Restore the original type conversion logic.
Apr 21, 2021
b0e91c8
Fix linter issue in type-conversion.
Apr 22, 2021
33d4e84
Add trouble-shooting section to readme.
May 3, 2021
6049c08
Add debug output to see the raw-values read from the registers.
May 3, 2021
1e6fb83
Extend debug output to also see the resulting value.
May 3, 2021
4183264
Add logger during test and change Debug to Debugf.
May 3, 2021
39c227b
Allow tracing the connection to debug connectivity problems. This is …
May 10, 2021
c3ff22b
Add option to close the connection after each gather cycle and force …
May 10, 2021
253ddcc
Revert "Add option to close the connection after each gather cycle an…
May 11, 2021
efabb1a
Revert "Allow tracing the connection to debug connectivity problems. …
May 11, 2021
6658954
Add more debug messages to debug #8506.
May 18, 2021
4ce9a1e
Add test case for non-consecutive coils.
May 19, 2021
41c9793
Remove unused slaveID variable.
May 27, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/LICENSE_OF_DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-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/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/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)
Expand All @@ -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/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/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/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)
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ require (
github.com/go-ping/ping v0.0.0-20210201095549-52eed920f98c
github.com/go-redis/redis v6.15.9+incompatible
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/gobwas/glob v0.2.3
github.com/gofrs/uuid v3.3.0+incompatible
Expand All @@ -66,6 +66,7 @@ require (
github.com/gopcua/opcua v0.1.13
github.com/gorilla/mux v1.7.3
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/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/harlow/kinesis-consumer v0.3.1-0.20181230152818-2f58b136fee0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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/go.mod h1:EIp+qkEpXoVsyZxXKy0AmXQx0mCHMMcIhXXvNDMpgF0=
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-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
Expand Down
18 changes: 16 additions & 2 deletions plugins/inputs/modbus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Metric are custom and configured using the `discrete_inputs`, `coils`,

The field `data_type` defines the representation of the data value on input from the modbus registers.
The input values are then converted from the given `data_type` to a type that is apropriate when
sending the value to the output plugin. These output types are usually one of string,
sending the value to the output plugin. These output types are usually one of string,
integer or floating-point-number. The size of the output type is assumed to be large enough
for all supported input types. The mapping from the input type to the output type is fixed
and cannot be configured.
Expand All @@ -114,7 +114,7 @@ always include the sign and therefore there exists no variant.

These types are handled as an integer type on input, but are converted to floating point representation
for further processing (e.g. scaling). Use one of these types when the input value is a decimal fixed point
representation of a non-integer value.
representation of a non-integer value.

Select the type `UFIXED` when the input type is declared to hold unsigned integer values, which cannot
be negative. The documentation of your modbus device should indicate this by a term like
Expand All @@ -127,6 +127,20 @@ with N decimal places'.
(FLOAT32 is deprecated and should not be used any more. UFIXED provides the same conversion
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

```sh
Expand Down
61 changes: 61 additions & 0 deletions plugins/inputs/modbus/configuration.go
Original file line number Diff line number Diff line change
@@ -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)
}
246 changes: 246 additions & 0 deletions plugins/inputs/modbus/configuration_original.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading