Skip to content

Commit 2e7b232

Browse files
authored
Modbus refactor (influxdata#9141)
1 parent 58479fd commit 2e7b232

13 files changed

+1555
-678
lines changed

docs/LICENSE_OF_DEPENDENCIES.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,6 @@ following works:
7171
- github.com/go-redis/redis [BSD 2-Clause "Simplified" License](https://github.com/go-redis/redis/blob/master/LICENSE)
7272
- github.com/go-sql-driver/mysql [Mozilla Public License 2.0](https://github.com/go-sql-driver/mysql/blob/master/LICENSE)
7373
- github.com/go-stack/stack [MIT License](https://github.com/go-stack/stack/blob/master/LICENSE.md)
74-
- github.com/goburrow/modbus [BSD 3-Clause "New" or "Revised" License](https://github.com/goburrow/modbus/blob/master/LICENSE)
75-
- github.com/goburrow/serial [MIT License](https://github.com/goburrow/serial/LICENSE)
7674
- github.com/gobwas/glob [MIT License](https://github.com/gobwas/glob/blob/master/LICENSE)
7775
- github.com/gofrs/uuid [MIT License](https://github.com/gofrs/uuid/blob/master/LICENSE)
7876
- github.com/gogo/googleapis [Apache License 2.0](https://github.com/gogo/googleapis/blob/master/LICENSE)
@@ -92,6 +90,8 @@ following works:
9290
- github.com/gorilla/mux [BSD 3-Clause "New" or "Revised" License](https://github.com/gorilla/mux/blob/master/LICENSE)
9391
- github.com/gorilla/websocket [BSD 2-Clause "Simplified" License](https://github.com/gorilla/websocket/blob/master/LICENSE)
9492
- github.com/gosnmp/gosnmp [BSD 2-Clause "Simplified" License](https://github.com/gosnmp/gosnmp/blob/master/LICENSE)
93+
- github.com/grid-x/modbus [BSD 3-Clause "New" or "Revised" License](https://github.com/grid-x/modbus/blob/master/LICENSE)
94+
- github.com/grid-x/serial [MIT License](https://github.com/grid-x/serial/blob/master/LICENSE)
9595
- github.com/grpc-ecosystem/grpc-gateway [BSD 3-Clause "New" or "Revised" License](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt)
9696
- github.com/hailocab/go-hostpool [MIT License](https://github.com/hailocab/go-hostpool/blob/master/LICENSE)
9797
- github.com/harlow/kinesis-consumer [MIT License](https://github.com/harlow/kinesis-consumer/blob/master/MIT-LICENSE)

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ require (
5353
github.com/go-ping/ping v0.0.0-20210201095549-52eed920f98c
5454
github.com/go-redis/redis v6.15.9+incompatible
5555
github.com/go-sql-driver/mysql v1.5.0
56-
github.com/goburrow/modbus v0.1.0
56+
github.com/goburrow/modbus v0.1.0 // indirect
5757
github.com/goburrow/serial v0.1.0 // indirect
5858
github.com/gobwas/glob v0.2.3
5959
github.com/gofrs/uuid v3.3.0+incompatible
@@ -66,6 +66,7 @@ require (
6666
github.com/gopcua/opcua v0.1.13
6767
github.com/gorilla/mux v1.7.3
6868
github.com/gosnmp/gosnmp v1.32.0
69+
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b
6970
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
7071
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
7172
github.com/harlow/kinesis-consumer v0.3.1-0.20181230152818-2f58b136fee0

go.sum

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,10 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
561561
github.com/gosnmp/gosnmp v1.32.0 h1:gctewmZx5qFI0oHMzRnjETqIZ093d9NgZy9TQr3V0iA=
562562
github.com/gosnmp/gosnmp v1.32.0/go.mod h1:EIp+qkEpXoVsyZxXKy0AmXQx0mCHMMcIhXXvNDMpgF0=
563563
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
564+
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b h1:Y4xqzO0CDNoehCr3ncgie3IgFTO9AzV8PMMEWESFM5c=
565+
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b/go.mod h1:YaK0rKJenZ74vZFcSSLlAQqtG74PMI68eDjpDCDDmTw=
566+
github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08 h1:syBxnRYnSPUDdkdo5U4sy2roxBPQDjNiw4od7xlsABQ=
567+
github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk=
564568
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
565569
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
566570
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=

plugins/inputs/modbus/README.md

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ Metric are custom and configured using the `discrete_inputs`, `coils`,
9696

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

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

119119
Select the type `UFIXED` when the input type is declared to hold unsigned integer values, which cannot
120120
be negative. The documentation of your modbus device should indicate this by a term like
@@ -127,6 +127,20 @@ with N decimal places'.
127127
(FLOAT32 is deprecated and should not be used any more. UFIXED provides the same conversion
128128
from unsigned values).
129129

130+
### Trouble shooting
131+
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
132+
swap the bytes within the register word (16-bit).
133+
134+
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).
135+
136+
In case are using a serial device and get an `permission denied` error, please check the permissions of your serial device and change accordingly.
137+
138+
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.
139+
140+
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).
141+
142+
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).
143+
130144
### Example Output
131145

132146
```sh
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package modbus
2+
3+
import "fmt"
4+
5+
const (
6+
maxQuantityDiscreteInput = uint16(2000)
7+
maxQuantityCoils = uint16(2000)
8+
maxQuantityInputRegisters = uint16(125)
9+
maxQuantityHoldingRegisters = uint16(125)
10+
)
11+
12+
type Configuration interface {
13+
Check() error
14+
Process() (map[byte]requestSet, error)
15+
}
16+
17+
func removeDuplicates(elements []uint16) []uint16 {
18+
encountered := map[uint16]bool{}
19+
result := []uint16{}
20+
21+
for _, addr := range elements {
22+
if !encountered[addr] {
23+
encountered[addr] = true
24+
result = append(result, addr)
25+
}
26+
}
27+
28+
return result
29+
}
30+
31+
func normalizeInputDatatype(dataType string) (string, error) {
32+
switch dataType {
33+
case "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64", "FLOAT32", "FLOAT64":
34+
return dataType, nil
35+
}
36+
return "unknown", fmt.Errorf("unknown type %q", dataType)
37+
}
38+
39+
func normalizeOutputDatatype(dataType string) (string, error) {
40+
switch dataType {
41+
case "", "native":
42+
return "native", nil
43+
case "INT64", "UINT64", "FLOAT64":
44+
return dataType, nil
45+
}
46+
return "unknown", fmt.Errorf("unknown type %q", dataType)
47+
}
48+
49+
func normalizeByteOrder(byteOrder string) (string, error) {
50+
switch byteOrder {
51+
case "ABCD", "MSW-BE", "MSW": // Big endian (Motorola)
52+
return "ABCD", nil
53+
case "BADC", "MSW-LE": // Big endian with bytes swapped
54+
return "BADC", nil
55+
case "CDAB", "LSW-BE": // Little endian with bytes swapped
56+
return "CDAB", nil
57+
case "DCBA", "LSW-LE", "LSW": // Little endian (Intel)
58+
return "DCBA", nil
59+
}
60+
return "unknown", fmt.Errorf("unknown byte-order %q", byteOrder)
61+
}
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
package modbus
2+
3+
import (
4+
"fmt"
5+
)
6+
7+
type fieldDefinition struct {
8+
Measurement string `toml:"measurement"`
9+
Name string `toml:"name"`
10+
ByteOrder string `toml:"byte_order"`
11+
DataType string `toml:"data_type"`
12+
Scale float64 `toml:"scale"`
13+
Address []uint16 `toml:"address"`
14+
}
15+
16+
type ConfigurationOriginal struct {
17+
SlaveID byte `toml:"slave_id"`
18+
DiscreteInputs []fieldDefinition `toml:"discrete_inputs"`
19+
Coils []fieldDefinition `toml:"coils"`
20+
HoldingRegisters []fieldDefinition `toml:"holding_registers"`
21+
InputRegisters []fieldDefinition `toml:"input_registers"`
22+
}
23+
24+
func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) {
25+
coil, err := c.initRequests(c.Coils, cCoils, maxQuantityCoils)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
discrete, err := c.initRequests(c.DiscreteInputs, cDiscreteInputs, maxQuantityDiscreteInput)
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
holding, err := c.initRequests(c.HoldingRegisters, cHoldingRegisters, maxQuantityHoldingRegisters)
36+
if err != nil {
37+
return nil, err
38+
}
39+
40+
input, err := c.initRequests(c.InputRegisters, cInputRegisters, maxQuantityInputRegisters)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
return map[byte]requestSet{
46+
c.SlaveID: {
47+
coil: coil,
48+
discrete: discrete,
49+
holding: holding,
50+
input: input,
51+
},
52+
}, nil
53+
}
54+
55+
func (c *ConfigurationOriginal) Check() error {
56+
if err := c.validateFieldDefinitions(c.DiscreteInputs, cDiscreteInputs); err != nil {
57+
return err
58+
}
59+
60+
if err := c.validateFieldDefinitions(c.Coils, cCoils); err != nil {
61+
return err
62+
}
63+
64+
if err := c.validateFieldDefinitions(c.HoldingRegisters, cHoldingRegisters); err != nil {
65+
return err
66+
}
67+
68+
return c.validateFieldDefinitions(c.InputRegisters, cInputRegisters)
69+
}
70+
71+
func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, registerType string, maxQuantity uint16) ([]request, error) {
72+
fields, err := c.initFields(fieldDefs)
73+
if err != nil {
74+
return nil, err
75+
}
76+
return newRequestsFromFields(fields, c.SlaveID, registerType, maxQuantity), nil
77+
}
78+
79+
func (c *ConfigurationOriginal) initFields(fieldDefs []fieldDefinition) ([]field, error) {
80+
// Construct the fields from the field definitions
81+
fields := make([]field, 0, len(fieldDefs))
82+
for _, def := range fieldDefs {
83+
f, err := c.newFieldFromDefinition(def)
84+
if err != nil {
85+
return nil, fmt.Errorf("initializing field %q failed: %v", def.Name, err)
86+
}
87+
fields = append(fields, f)
88+
}
89+
90+
return fields, nil
91+
}
92+
93+
func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition) (field, error) {
94+
// Check if the addresses are consecutive
95+
expected := def.Address[0]
96+
for _, current := range def.Address[1:] {
97+
expected++
98+
if current != expected {
99+
return field{}, fmt.Errorf("addresses of field %q are not consecutive", def.Name)
100+
}
101+
}
102+
103+
// Initialize the field
104+
f := field{
105+
measurement: def.Measurement,
106+
name: def.Name,
107+
scale: def.Scale,
108+
address: def.Address[0],
109+
length: uint16(len(def.Address)),
110+
}
111+
if def.DataType != "" {
112+
inType, err := c.normalizeInputDatatype(def.DataType, len(def.Address))
113+
if err != nil {
114+
return f, err
115+
}
116+
outType, err := c.normalizeOutputDatatype(def.DataType)
117+
if err != nil {
118+
return f, err
119+
}
120+
byteOrder, err := c.normalizeByteOrder(def.ByteOrder)
121+
if err != nil {
122+
return f, err
123+
}
124+
125+
f.converter, err = determineConverter(inType, byteOrder, outType, def.Scale)
126+
if err != nil {
127+
return f, err
128+
}
129+
}
130+
131+
return f, nil
132+
}
133+
134+
func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefinition, registerType string) error {
135+
nameEncountered := map[string]bool{}
136+
for _, item := range fieldDefs {
137+
//check empty name
138+
if item.Name == "" {
139+
return fmt.Errorf("empty name in '%s'", registerType)
140+
}
141+
142+
//search name duplicate
143+
canonicalName := item.Measurement + "." + item.Name
144+
if nameEncountered[canonicalName] {
145+
return fmt.Errorf("name '%s' is duplicated in measurement '%s' '%s' - '%s'", item.Name, item.Measurement, registerType, item.Name)
146+
}
147+
nameEncountered[canonicalName] = true
148+
149+
if registerType == cInputRegisters || registerType == cHoldingRegisters {
150+
// search byte order
151+
switch item.ByteOrder {
152+
case "AB", "BA", "ABCD", "CDAB", "BADC", "DCBA", "ABCDEFGH", "HGFEDCBA", "BADCFEHG", "GHEFCDAB":
153+
default:
154+
return fmt.Errorf("invalid byte order '%s' in '%s' - '%s'", item.ByteOrder, registerType, item.Name)
155+
}
156+
157+
// search data type
158+
switch item.DataType {
159+
case "UINT16", "INT16", "UINT32", "INT32", "UINT64", "INT64", "FLOAT32-IEEE", "FLOAT64-IEEE", "FLOAT32", "FIXED", "UFIXED":
160+
default:
161+
return fmt.Errorf("invalid data type '%s' in '%s' - '%s'", item.DataType, registerType, item.Name)
162+
}
163+
164+
// check scale
165+
if item.Scale == 0.0 {
166+
return fmt.Errorf("invalid scale '%f' in '%s' - '%s'", item.Scale, registerType, item.Name)
167+
}
168+
}
169+
170+
// check address
171+
if len(item.Address) != 1 && len(item.Address) != 2 && len(item.Address) != 4 {
172+
return fmt.Errorf("invalid address '%v' length '%v' in '%s' - '%s'", item.Address, len(item.Address), registerType, item.Name)
173+
}
174+
175+
if registerType == cInputRegisters || registerType == cHoldingRegisters {
176+
if 2*len(item.Address) != len(item.ByteOrder) {
177+
return fmt.Errorf("invalid byte order '%s' and address '%v' in '%s' - '%s'", item.ByteOrder, item.Address, registerType, item.Name)
178+
}
179+
180+
// search duplicated
181+
if len(item.Address) > len(removeDuplicates(item.Address)) {
182+
return fmt.Errorf("duplicate address '%v' in '%s' - '%s'", item.Address, registerType, item.Name)
183+
}
184+
} else if len(item.Address) != 1 {
185+
return fmt.Errorf("invalid address'%v' length'%v' in '%s' - '%s'", item.Address, len(item.Address), registerType, item.Name)
186+
}
187+
}
188+
return nil
189+
}
190+
191+
func (c *ConfigurationOriginal) normalizeInputDatatype(dataType string, words int) (string, error) {
192+
// Handle our special types
193+
switch dataType {
194+
case "FIXED":
195+
switch words {
196+
case 1:
197+
return "INT16", nil
198+
case 2:
199+
return "INT32", nil
200+
case 4:
201+
return "INT64", nil
202+
default:
203+
return "unknown", fmt.Errorf("invalid length %d for type %q", words, dataType)
204+
}
205+
case "FLOAT32", "UFIXED":
206+
switch words {
207+
case 1:
208+
return "UINT16", nil
209+
case 2:
210+
return "UINT32", nil
211+
case 4:
212+
return "UINT64", nil
213+
default:
214+
return "unknown", fmt.Errorf("invalid length %d for type %q", words, dataType)
215+
}
216+
case "FLOAT32-IEEE":
217+
return "FLOAT32", nil
218+
case "FLOAT64-IEEE":
219+
return "FLOAT64", nil
220+
}
221+
return normalizeInputDatatype(dataType)
222+
}
223+
224+
func (c *ConfigurationOriginal) normalizeOutputDatatype(dataType string) (string, error) {
225+
// Handle our special types
226+
switch dataType {
227+
case "FIXED", "FLOAT32", "UFIXED":
228+
return "FLOAT64", nil
229+
}
230+
return normalizeOutputDatatype("native")
231+
}
232+
233+
func (c *ConfigurationOriginal) normalizeByteOrder(byteOrder string) (string, error) {
234+
// Handle our special types
235+
switch byteOrder {
236+
case "AB", "ABCDEFGH":
237+
return "ABCD", nil
238+
case "BADCFEHG":
239+
return "BADC", nil
240+
case "GHEFCDAB":
241+
return "CDAB", nil
242+
case "BA", "HGFEDCBA":
243+
return "DCBA", nil
244+
}
245+
return normalizeByteOrder(byteOrder)
246+
}

0 commit comments

Comments
 (0)