diff --git a/plugins/inputs/modbus/README.md b/plugins/inputs/modbus/README.md index 29721487ffec3..1d0d253a7ab65 100644 --- a/plugins/inputs/modbus/README.md +++ b/plugins/inputs/modbus/README.md @@ -3,7 +3,7 @@ The Modbus plugin collects Discrete Inputs, Coils, Input Registers and Holding Registers via Modbus TCP or Modbus RTU/ASCII. -## Configuration +## Example configuration ```toml [[inputs.modbus]] @@ -46,6 +46,13 @@ Registers via Modbus TCP or Modbus RTU/ASCII. ## Note: You have to enable telegraf's debug mode to see those messages! # debug_connection = false + ## Define the configuration schema + ## |---register -- define fields per register type in the original style (only supports one slave ID) + ## |---request -- define fields on a requests base + configuration_type = "register" + + ## --- "register" configuration style --- + ## Measurements ## @@ -74,10 +81,11 @@ Registers via Modbus TCP or Modbus RTU/ASCII. ## |---BA, DCBA - Little Endian ## |---BADC - Mid-Big Endian ## |---CDAB - Mid-Little Endian - ## data_type - INT16, UINT16, INT32, UINT32, INT64, UINT64, FLOAT32-IEEE, FLOAT64-IEEE (the IEEE 754 binary representation) - ## FLOAT32 (deprecated), FIXED, UFIXED (fixed-point representation on input) - ## scale - the final numeric variable representation - ## address - variable address + ## data_type - INT16, UINT16, INT32, UINT32, INT64, UINT64, + ## FLOAT32-IEEE, FLOAT64-IEEE (the IEEE 754 binary representation) + ## FLOAT32, FIXED, UFIXED (fixed-point representation on input) + ## scale - the final numeric variable representation + ## address - variable address holding_registers = [ { name = "power_factor", byte_order = "AB", data_type = "FIXED", scale=0.01, address = [8]}, @@ -93,6 +101,86 @@ Registers via Modbus TCP or Modbus RTU/ASCII. { name = "pump1_speed", byte_order = "ABCD", data_type = "INT32", scale=1.0, address = [3,4]}, ] + ## --- "request" configuration style --- + + ## Per request definition + ## + + ## Define a request sent to the device + ## Multiple of those requests can be defined. Data will be collated into metrics at the end of data collection. + [[inputs.modbus.request]] + ## ID of the modbus slave device to query. + ## If you need to query multiple slave-devices, create several "request" definitions. + slave_id = 1 + + ## Byte order of the data. + ## |---ABCD -- Big Endian (Motorola) + ## |---DCBA -- Little Endian (Intel) + ## |---BADC -- Big Endian with byte swap + ## |---CDAB -- Little Endian with byte swap + byte_order = "ABCD" + + ## Type of the register for the request + ## Can be "coil", "discrete", "holding" or "input" + register = "coil" + + ## Name of the measurement. + ## Can be overriden by the individual field definitions. Defaults to "modbus" + # measurement = "modbus" + + ## Field definitions + ## Analog Variables, Input Registers and Holding Registers + ## address - address of the register to query. For coil and discrete inputs this is the bit address. + ## name *1 - field name + ## type *1,2 - type of the modbus field, can be INT16, UINT16, INT32, UINT32, INT64, UINT64 and + ## FLOAT32, FLOAT64 (IEEE 754 binary representation) + ## scale *1,2 - (optional) factor to scale the variable with + ## output *1,2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if + ## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc). + ## measurement *1 - (optional) measurement name, defaults to the setting of the request + ## omit - (optional) omit this field. Useful to leave out single values when querying many registers + ## with a single request. Defaults to "false". + ## + ## *1: Those fields are ignored if field is omitted ("omit"=true) + ## + ## *2: Thise fields are ignored for both "coil" and "discrete"-input type of registers. For those register types + ## the fields are output as zero or one in UINT64 format by default. + + ## Coil / discrete input example + fields = [ + { address=0, name="motor1_run"}, + { address=1, name="jog", measurement="motor"}, + { address=2, name="motor1_stop", omit=true}, + { address=3, name="motor1_overheating"}, + ] + + [[inputs.modbus.request]] + ## Holding example + ## All of those examples will result in FLOAT64 field outputs + slave_id = 1 + byte_order = "DCBA" + register = "holding" + fields = [ + { address=0, name="voltage", type="INT16", scale=0.1 }, + { address=1, name="current", type="INT32", scale=0.001 }, + { address=3, name="power", type="UINT32", omit=true }, + { address=5, name="energy", type="FLOAT32", scale=0.001, measurement="W" }, + { address=7, name="frequency", type="UINT32", scale=0.1 }, + { address=8, name="power_factor", type="INT64", scale=0.01 }, + ] + + [[inputs.modbus.request]] + ## Input example with type conversions + slave_id = 1 + byte_order = "ABCD" + register = "input" + fields = [ + { address=0, name="rpm", type="INT16" }, # will result in INT64 field + { address=1, name="temperature", type="INT16", scale=0.1 }, # will result in FLOAT64 field + { address=2, name="force", type="INT32", output="FLOAT64" }, # will result in FLOAT64 field + { address=4, name="hours", type="UINT32" }, # will result in UIN64 field + ] + ## Enable workarounds required by some devices to work correctly # [inputs.modbus.workarounds] ## Pause between read requests sent to the device. This might be necessary for (slow) serial devices. @@ -109,12 +197,27 @@ You can debug Modbus connection issues by enabling `debug_connection`. To see th Please use `pause_between_requests` with care. Especially make sure that the total gather time, including the pause(s), does not exceed the configured collection interval. Note, that pauses add up if multiple requests are sent! -## Metrics +## Configuration styles + +The modbus plugin supports multiple configuration styles that can be set using the `configuration_type` setting. The different styles are described below. Please note that styles cannot be mixed, i.e. only the settings belonging to the configured `configuration_type` are used for constructing _modbus_ requests and creation of metrics. + +Directly jump to the styles: + +- [original / register plugin style](#register-configuration-style) +- [per-request style](#request-configuration-style) + +--- + +### `register` configuration style + +This is the original style used by this plugin. It allows a per-register configuration for a single slave-device. + +#### Metrics Metric are custom and configured using the `discrete_inputs`, `coils`, `holding_register` and `input_registers` options. -## Usage of `data_type` +#### Usage of `data_type` 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 @@ -123,16 +226,16 @@ integer or floating-point-number. The size of the output type is assumed to be l for all supported input types. The mapping from the input type to the output type is fixed and cannot be configured. -### Integers: `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64` +##### Integers: `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64` These types are used for integer input values. Select the one that matches your modbus data source. -### Floating Point: `FLOAT32-IEEE`, `FLOAT64-IEEE` +##### Floating Point: `FLOAT32-IEEE`, `FLOAT64-IEEE` Use these types if your modbus registers contain a value that is encoded in this format. These types always include the sign and therefore there exists no variant. -### Fixed Point: `FIXED`, `UFIXED` (`FLOAT32`) +##### Fixed Point: `FIXED`, `UFIXED` (`FLOAT32`) 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 @@ -149,6 +252,76 @@ with N decimal places'. (FLOAT32 is deprecated and should not be used any more. UFIXED provides the same conversion from unsigned values). +--- + +### `request` configuration style + +This sytle can be used to specify the modbus requests directly. It allows to specify multiple `[[inputs.modbus.request]]` sections including multiple slave-devices. This way, _modbus_ gateway devices can be queried. Please not that _requests_ might be split for non-consecutive addresses. If you want to avoid this behavior please add _fields_ with the `omit` flag set filling the gaps between addresses. + +#### Slave device + +You can use the `slave_id` setting to specify the ID of the slave device to query. It should be specified for each request and defaults to zero otherwise. Please note, only one `slave_id` can be specified for a request. + +#### Byte order of the register + +The `byte_order` setting specifies the byte- and word-order of the registers. It can be set to `ABCD` for _big endian (Motorola)_ or `DCBA` for _little endian (Intel)_ format as well as `BADC` and `CDAB` for _big endian_ or _little endian_ with _byte swap_. + +#### Register type + +The `register` setting specifies the modbus register-set to query and can be set to `coil`, `discrete`, `holding` or `input`. + +#### Per-request measurement setting + +You can specify the name of the measurement for the following field definitions using the `measurement` setting. If the setting is omitted `modbus` is used. Furthermore, the measurement value can be overridden by each field individually. + +#### Field definitions + +Each `request` can contain a list of fields to collect from the modbus device. + +##### address + +A field is identified by an `address` that reflects the modbus register address. You can usually find the address values for the different datapoints in the datasheet of your modbus device. This is a mandatory setting. + +For _coil_ and _discrete input_ registers this setting specifies the __bit__ containing the value of the field. + +##### name + +Using the `name` setting you can specify the field-name in the metric as output by the plugin. This setting is ignored if the field's `omit` is set to `true` and can be omitted in this case. + +__Please note:__ There cannot be multiple fields with the same `name` in one metric identified by `measurement`, `slave_id` and `register`. + +##### register datatype + +The `register` setting specifies the datatype of the modbus register and can be set to `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64` or `UINT64` for integer types or `FLOAT32` and `FLOAT64` for IEEE 754 binary representations of floating point values. Usually the datatype of the register is listed in the datasheet of your modbus device in relation to the `address` described above. + + This setting is ignored if the field's `omit` is set to `true` or if the `register` type is a bit-type (`coil` or `discrete`) and can be omitted in these cases. + +##### scaling + +You can use the `scale` setting to scale the register values, e.g. if the register contains a fix-point values in `UINT32` format with two decimal places for example. To convert the read register value to the actual value you can set the `scale=0.01`. The scale is used as a factor as `field_value * scale`. + +This setting is ignored if the field's `omit` is set to `true` or if the `register` type is a bit-type (`coil` or `discrete`) and can be omitted in these cases. + +__Please note:__ The resulting field-type will be set to `FLOAT64` if no output format is specified. + +##### output datatype + +Using the `output` setting you might explicitly specify the output field-datatype. The `output` type can be `INT64`, `UINT64` or `FLOAT64`. If not set explicitly, the output type is guessed as follows: If `scale` is set to a non-zero value, the output type is `FLOAT64`. Otherwise, the output type corresponds to the register datatype _class_, i.e. `INT*` will result in `INT64`, `UINT*` in `UINT64` and `FLOAT*` in `FLOAT64`. + +This setting is ignored if the field's `omit` is set to `true` or if the `register` type is a bit-type (`coil` or `discrete`) and can be omitted in these cases. For `coil` and `discrete` registers the field-value is output as zero or one in `UINT16` format. + +#### per-field measurement setting + +The `measurement` setting can be used to override the measurement name on a per-field basis. This might be useful if you can to split the fields in one request to multiple measurements. If not specified, the value specified in the [`request` section](#per-request-measurement-setting) or, if also omitted, `modbus` is used. + +This setting is ignored if the field's `omit` is set to `true` and can be omitted in this case. + +#### omitting a field + +When specifying `omit=true`, the corresponding field will be ignored when collecting the metric but is taken into account when constructing the modbus requests. This way, you can fill "holes" in the addresses to construct consecutive address ranges resulting in a single request. Using a single modbus request can be beneficial as the values are all collected at the same point in time. + +--- + ## Trouble shooting ### Strange data @@ -165,6 +338,7 @@ In case you get an `exception '2' (illegal data address)` error you might try to 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 your data still looks corrupted, 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). +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). ### Workarounds diff --git a/plugins/inputs/modbus/configuration.go b/plugins/inputs/modbus/configuration.go index 143f12867dea6..8f85cf14a972e 100644 --- a/plugins/inputs/modbus/configuration.go +++ b/plugins/inputs/modbus/configuration.go @@ -14,6 +14,7 @@ const ( type Configuration interface { Check() error Process() (map[byte]requestSet, error) + SampleConfigPart() string } func removeDuplicates(elements []uint16) []uint16 { @@ -35,7 +36,7 @@ func normalizeInputDatatype(dataType string) (string, error) { case "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64", "FLOAT32", "FLOAT64": return dataType, nil } - return "unknown", fmt.Errorf("unknown type %q", dataType) + return "unknown", fmt.Errorf("unknown input type %q", dataType) } func normalizeOutputDatatype(dataType string) (string, error) { @@ -45,7 +46,7 @@ func normalizeOutputDatatype(dataType string) (string, error) { case "INT64", "UINT64", "FLOAT64": return dataType, nil } - return "unknown", fmt.Errorf("unknown type %q", dataType) + return "unknown", fmt.Errorf("unknown output type %q", dataType) } func normalizeByteOrder(byteOrder string) (string, error) { diff --git a/plugins/inputs/modbus/configuration_original.go b/plugins/inputs/modbus/configuration_register.go similarity index 70% rename from plugins/inputs/modbus/configuration_original.go rename to plugins/inputs/modbus/configuration_register.go index 78861df74e0f7..b73f3c7ce0203 100644 --- a/plugins/inputs/modbus/configuration_original.go +++ b/plugins/inputs/modbus/configuration_register.go @@ -6,6 +6,56 @@ import ( "fmt" ) +const sampleConfigPartPerRegister = ` + ## Per register definition + ## + + ## Digital Variables, Discrete Inputs and Coils + ## measurement - the (optional) measurement name, defaults to "modbus" + ## name - the variable name + ## address - variable address + + discrete_inputs = [ + { name = "start", address = [0]}, + { name = "stop", address = [1]}, + { name = "reset", address = [2]}, + { name = "emergency_stop", address = [3]}, + ] + coils = [ + { name = "motor1_run", address = [0]}, + { name = "motor1_jog", address = [1]}, + { name = "motor1_stop", address = [2]}, + ] + + ## Analog Variables, Input Registers and Holding Registers + ## measurement - the (optional) measurement name, defaults to "modbus" + ## name - the variable name + ## byte_order - the ordering of bytes + ## |---AB, ABCD - Big Endian + ## |---BA, DCBA - Little Endian + ## |---BADC - Mid-Big Endian + ## |---CDAB - Mid-Little Endian + ## data_type - INT16, UINT16, INT32, UINT32, INT64, UINT64, + ## FLOAT32-IEEE, FLOAT64-IEEE (the IEEE 754 binary representation) + ## FLOAT32, FIXED, UFIXED (fixed-point representation on input) + ## scale - the final numeric variable representation + ## address - variable address + + holding_registers = [ + { name = "power_factor", byte_order = "AB", data_type = "FIXED", scale=0.01, address = [8]}, + { name = "voltage", byte_order = "AB", data_type = "FIXED", scale=0.1, address = [0]}, + { name = "energy", byte_order = "ABCD", data_type = "FIXED", scale=0.001, address = [5,6]}, + { name = "current", byte_order = "ABCD", data_type = "FIXED", scale=0.001, address = [1,2]}, + { name = "frequency", byte_order = "AB", data_type = "UFIXED", scale=0.1, address = [7]}, + { name = "power", byte_order = "ABCD", data_type = "UFIXED", scale=0.1, address = [3,4]}, + ] + input_registers = [ + { name = "tank_level", byte_order = "AB", data_type = "INT16", scale=1.0, address = [0]}, + { name = "tank_ph", byte_order = "AB", data_type = "INT16", scale=1.0, address = [1]}, + { name = "pump1_speed", byte_order = "ABCD", data_type = "INT32", scale=1.0, address = [3,4]}, + ] +` + type fieldDefinition struct { Measurement string `toml:"measurement"` Name string `toml:"name"` @@ -23,23 +73,43 @@ type ConfigurationOriginal struct { InputRegisters []fieldDefinition `toml:"input_registers"` } +func (c *ConfigurationOriginal) SampleConfigPart() string { + return sampleConfigPartPerRegister +} + +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) Process() (map[byte]requestSet, error) { - coil, err := c.initRequests(c.Coils, cCoils, maxQuantityCoils) + coil, err := c.initRequests(c.Coils, maxQuantityCoils) if err != nil { return nil, err } - discrete, err := c.initRequests(c.DiscreteInputs, cDiscreteInputs, maxQuantityDiscreteInput) + discrete, err := c.initRequests(c.DiscreteInputs, maxQuantityDiscreteInput) if err != nil { return nil, err } - holding, err := c.initRequests(c.HoldingRegisters, cHoldingRegisters, maxQuantityHoldingRegisters) + holding, err := c.initRequests(c.HoldingRegisters, maxQuantityHoldingRegisters) if err != nil { return nil, err } - input, err := c.initRequests(c.InputRegisters, cInputRegisters, maxQuantityInputRegisters) + input, err := c.initRequests(c.InputRegisters, maxQuantityInputRegisters) if err != nil { return nil, err } @@ -54,28 +124,12 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) { }, 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) { +func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, maxQuantity uint16) ([]request, error) { fields, err := c.initFields(fieldDefs) if err != nil { return nil, err } - return newRequestsFromFields(fields, c.SlaveID, registerType, maxQuantity), nil + return newRequestsFromFields(fields, maxQuantity), nil } func (c *ConfigurationOriginal) initFields(fieldDefs []fieldDefinition) ([]field, error) { @@ -106,7 +160,6 @@ func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition) (fie f := field{ measurement: def.Measurement, name: def.Name, - scale: def.Scale, address: def.Address[0], length: uint16(len(def.Address)), } diff --git a/plugins/inputs/modbus/configuration_request.go b/plugins/inputs/modbus/configuration_request.go new file mode 100644 index 0000000000000..2231616142e67 --- /dev/null +++ b/plugins/inputs/modbus/configuration_request.go @@ -0,0 +1,372 @@ +package modbus + +import ( + "fmt" + "hash/maphash" +) + +const sampleConfigPartPerRequest = ` + ## Per request definition + ## + + ## Define a request sent to the device + ## Multiple of those requests can be defined. Data will be collated into metrics at the end of data collection. + [[inputs.modbus.request]] + ## ID of the modbus slave device to query. + ## If you need to query multiple slave-devices, create several "request" definitions. + # slave_id = 0 + + ## Byte order of the data. + ## |---ABCD or MSW-BE -- Big Endian (Motorola) + ## |---DCBA or LSW-LE -- Little Endian (Intel) + ## |---BADC or MSW-LE -- Big Endian with byte swap + ## |---CDAB or LSW-BE -- Little Endian with byte swap + # byte_order = "ABCD" + + ## Type of the register for the request + ## Can be "coil", "discrete", "holding" or "input" + # register = "holding" + + ## Name of the measurement. + ## Can be overriden by the individual field definitions. Defaults to "modbus" + # measurement = "modbus" + + ## Field definitions + ## Analog Variables, Input Registers and Holding Registers + ## address - address of the register to query. For coil and discrete inputs this is the bit address. + ## name *1 - field name + ## type *1,2 - type of the modbus field, can be INT16, UINT16, INT32, UINT32, INT64, UINT64 and + ## FLOAT32, FLOAT64 (IEEE 754 binary representation) + ## scale *1,2 - (optional) factor to scale the variable with + ## output *1,2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if + ## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc). + ## measurement *1 - (optional) measurement name, defaults to the setting of the request + ## omit - (optional) omit this field. Useful to leave out single values when querying many registers + ## with a single request. Defaults to "false". + ## + ## *1: Those fields are ignored if field is omitted ("omit"=true) + ## + ## *2: Thise fields are ignored for both "coil" and "discrete"-input type of registers. For those register types + ## the fields are output as zero or one in UINT64 format by default. + + ## Coil / discrete input example + # fields = [ + # { address=0, name="motor1_run"}, + # { address=1, name="jog", measurement="motor"}, + # { address=2, name="motor1_stop", omit=true}, + # { address=3, name="motor1_overheating"}, + # ] + + ## Holding / input example + ## All of those examples will result in FLOAT64 field outputs + # fields = [ + # { address=0, name="voltage", type="INT16", scale=0.1 }, + # { address=1, name="current", type="INT32", scale=0.001 }, + # { address=3, name="power", type="UINT32", omit=true }, + # { address=5, name="energy", type="FLOAT32", scale=0.001, measurement="W" }, + # { address=7, name="frequency", type="UINT32", scale=0.1 }, + # { address=8, name="power_factor", type="INT64", scale=0.01 }, + # ] + + ## Holding / input example with type conversions + # fields = [ + # { address=0, name="rpm", type="INT16" }, # will result in INT64 field + # { address=1, name="temperature", type="INT16", scale=0.1 }, # will result in FLOAT64 field + # { address=2, name="force", type="INT32", output="FLOAT64" }, # will result in FLOAT64 field + # { address=4, name="hours", type="UINT32" }, # will result in UIN64 field + # ] +` + +type requestFieldDefinition struct { + Address uint16 `toml:"address"` + Name string `toml:"name"` + InputType string `toml:"type"` + Scale float64 `toml:"scale"` + OutputType string `toml:"output"` + Measurement string `toml:"measurement"` + Omit bool `toml:"omit"` +} + +type requestDefinition struct { + SlaveID byte `toml:"slave_id"` + ByteOrder string `toml:"byte_order"` + RegisterType string `toml:"register"` + Measurement string `toml:"measurement"` + Fields []requestFieldDefinition `toml:"fields"` +} + +type ConfigurationPerRequest struct { + Requests []requestDefinition `toml:"request"` +} + +func (c *ConfigurationPerRequest) SampleConfigPart() string { + return sampleConfigPartPerRequest +} + +func (c *ConfigurationPerRequest) Check() error { + seed := maphash.MakeSeed() + seenFields := make(map[uint64]bool) + + for _, def := range c.Requests { + // Check byte order of the data + switch def.ByteOrder { + case "": + def.ByteOrder = "ABCD" + case "ABCD", "DCBA", "BADC", "CDAB", "MSW-BE", "MSW-LE", "LSW-LE", "LSW-BE": + default: + return fmt.Errorf("unknown byte-order %q", def.ByteOrder) + } + + // Check register type + switch def.RegisterType { + case "": + def.RegisterType = "holding" + case "coil", "discrete", "holding", "input": + default: + return fmt.Errorf("unknown register-type %q", def.RegisterType) + } + + // Set the default for measurement if required + if def.Measurement == "" { + def.Measurement = "modbus" + } + + // Check the fields + for fidx, f := range def.Fields { + // Check the input type for all fields except the bit-field ones. + // We later need the type (even for omitted fields) to determine the length. + if def.RegisterType == cHoldingRegisters || def.RegisterType == cInputRegisters { + switch f.InputType { + case "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64", "FLOAT32", "FLOAT64": + default: + return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name) + } + } + + // Other properties don't need to be checked for omitted fields + if f.Omit { + continue + } + + // Name is mandatory + if f.Name == "" { + return fmt.Errorf("empty field name in request for slave %d", def.SlaveID) + } + + // Check fields only relevant for non-bit register types + if def.RegisterType == cHoldingRegisters || def.RegisterType == cInputRegisters { + // Check output type + switch f.OutputType { + case "", "INT64", "UINT64", "FLOAT64": + default: + return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name) + } + } + + // Handle the default for measurement + if f.Measurement == "" { + f.Measurement = def.Measurement + } + def.Fields[fidx] = f + + // Check for duplicate field definitions + id, err := c.fieldID(seed, def.SlaveID, def.RegisterType, def.Measurement, f.Name) + if err != nil { + return fmt.Errorf("cannot determine field id for %q: %v", f.Name, err) + } + if seenFields[id] { + return fmt.Errorf("field %q duplicated in measurement %q (slave %d/%q)", f.Name, f.Measurement, def.SlaveID, def.RegisterType) + } + seenFields[id] = true + } + } + + return nil +} + +func (c *ConfigurationPerRequest) Process() (map[byte]requestSet, error) { + result := map[byte]requestSet{} + + for _, def := range c.Requests { + // Set default + if def.RegisterType == "" { + def.RegisterType = "holding" + } + + // Construct the fields + isTyped := def.RegisterType == "holding" || def.RegisterType == "input" + fields, err := c.initFields(def.Fields, isTyped, def.ByteOrder) + if err != nil { + return nil, err + } + + // Make sure we have a set to work with + set, found := result[def.SlaveID] + if !found { + set = requestSet{ + coil: []request{}, + discrete: []request{}, + holding: []request{}, + input: []request{}, + } + } + + switch def.RegisterType { + case "coil": + requests := newRequestsFromFields(fields, maxQuantityCoils) + set.coil = append(set.coil, requests...) + case "discrete": + requests := newRequestsFromFields(fields, maxQuantityDiscreteInput) + set.discrete = append(set.discrete, requests...) + case "holding": + requests := newRequestsFromFields(fields, maxQuantityHoldingRegisters) + set.holding = append(set.holding, requests...) + case "input": + requests := newRequestsFromFields(fields, maxQuantityInputRegisters) + set.input = append(set.input, requests...) + default: + return nil, fmt.Errorf("unknown register type %q", def.RegisterType) + } + result[def.SlaveID] = set + } + + return result, nil +} + +func (c *ConfigurationPerRequest) initFields(fieldDefs []requestFieldDefinition, typed bool, byteOrder string) ([]field, error) { + // Construct the fields from the field definitions + fields := make([]field, 0, len(fieldDefs)) + for _, def := range fieldDefs { + f, err := c.newFieldFromDefinition(def, typed, byteOrder) + if err != nil { + return nil, fmt.Errorf("initializing field %q failed: %v", def.Name, err) + } + fields = append(fields, f) + } + + return fields, nil +} + +func (c *ConfigurationPerRequest) newFieldFromDefinition(def requestFieldDefinition, typed bool, byteOrder string) (field, error) { + var err error + + fieldLength := uint16(1) + if typed { + if fieldLength, err = c.determineFieldLength(def.InputType); err != nil { + return field{}, err + } + } + + // Initialize the field + f := field{ + measurement: def.Measurement, + name: def.Name, + address: def.Address, + length: fieldLength, + omit: def.Omit, + } + + // No more processing for un-typed (coil and discrete registers) or omitted fields + if !typed || def.Omit { + return f, nil + } + + // Automagically determine the output type... + if def.OutputType == "" { + if def.Scale == 0.0 { + // For non-scaling cases we should choose the output corresponding to the input class + // i.e. INT64 for INT*, UINT64 for UINT* etc. + var err error + if def.OutputType, err = c.determineOutputDatatype(def.InputType); err != nil { + return field{}, err + } + } else { + // For scaling cases we always want FLOAT64 by default + def.OutputType = "FLOAT64" + } + } + + // Setting default byte-order + if byteOrder == "" { + byteOrder = "ABCD" + } + + // Normalize the data relevant for determining the converter + inType, err := normalizeInputDatatype(def.InputType) + if err != nil { + return field{}, err + } + outType, err := normalizeOutputDatatype(def.OutputType) + if err != nil { + return field{}, err + } + order, err := normalizeByteOrder(byteOrder) + if err != nil { + return field{}, err + } + + f.converter, err = determineConverter(inType, order, outType, def.Scale) + if err != nil { + return field{}, err + } + + return f, nil +} + +func (c *ConfigurationPerRequest) fieldID(seed maphash.Seed, slave byte, register, measurement, name string) (uint64, error) { + var mh maphash.Hash + mh.SetSeed(seed) + + if err := mh.WriteByte(slave); err != nil { + return 0, err + } + if err := mh.WriteByte(0); err != nil { + return 0, err + } + if _, err := mh.WriteString(register); err != nil { + return 0, err + } + if err := mh.WriteByte(0); err != nil { + return 0, err + } + if _, err := mh.WriteString(measurement); err != nil { + return 0, err + } + if err := mh.WriteByte(0); err != nil { + return 0, err + } + if _, err := mh.WriteString(name); err != nil { + return 0, err + } + if err := mh.WriteByte(0); err != nil { + return 0, err + } + + return mh.Sum64(), nil +} + +func (c *ConfigurationPerRequest) determineOutputDatatype(input string) (string, error) { + // Handle our special types + switch input { + case "INT16", "INT32", "INT64": + return "INT64", nil + case "UINT16", "UINT32", "UINT64": + return "UINT64", nil + case "FLOAT32", "FLOAT64": + return "FLOAT64", nil + } + return "unknown", fmt.Errorf("invalid input datatype %q for determining output", input) +} + +func (c *ConfigurationPerRequest) determineFieldLength(input string) (uint16, error) { + // Handle our special types + switch input { + case "INT16", "UINT16": + return 1, nil + case "INT32", "UINT32", "FLOAT32": + return 2, nil + case "INT64", "UINT64", "FLOAT64": + return 4, nil + } + return 0, fmt.Errorf("invalid input datatype %q for determining field length", input) +} diff --git a/plugins/inputs/modbus/modbus.go b/plugins/inputs/modbus/modbus.go index 02add97429a85..865a430b74929 100644 --- a/plugins/inputs/modbus/modbus.go +++ b/plugins/inputs/modbus/modbus.go @@ -38,7 +38,10 @@ type Modbus struct { Workarounds ModbusWorkarounds `toml:"workarounds"` Log telegraf.Logger `toml:"-"` // Register configuration + ConfigurationType string `toml:"configuration_type"` ConfigurationOriginal + ConfigurationPerRequest + // Connection handling client mb.Client handler mb.ClientHandler @@ -59,9 +62,9 @@ type requestSet struct { type field struct { measurement string name string - scale float64 address uint16 length uint16 + omit bool converter fieldConverterFunc value interface{} } @@ -74,7 +77,7 @@ const ( ) const description = `Retrieve data from MODBUS slave devices` -const sampleConfig = ` +const sampleConfigStart = ` ## Connection Configuration ## ## The plugin supports connections to PLCs via MODBUS/TCP, RTU over TCP, ASCII over TCP or @@ -104,7 +107,6 @@ const sampleConfig = ` # data_bits = 8 # parity = "N" # stop_bits = 1 - # transmission_mode = "RTU" ## Trace the connection to the modbus device as debug messages ## Note: You have to enable telegraf's debug mode to see those messages! @@ -115,54 +117,12 @@ const sampleConfig = ` ## For Serial you can choose between "RTU" and "ASCII" # transmission_mode = "RTU" - ## Measurements - ## - - ## Digital Variables, Discrete Inputs and Coils - ## measurement - the (optional) measurement name, defaults to "modbus" - ## name - the variable name - ## address - variable address - - discrete_inputs = [ - { name = "start", address = [0]}, - { name = "stop", address = [1]}, - { name = "reset", address = [2]}, - { name = "emergency_stop", address = [3]}, - ] - coils = [ - { name = "motor1_run", address = [0]}, - { name = "motor1_jog", address = [1]}, - { name = "motor1_stop", address = [2]}, - ] - - ## Analog Variables, Input Registers and Holding Registers - ## measurement - the (optional) measurement name, defaults to "modbus" - ## name - the variable name - ## byte_order - the ordering of bytes - ## |---AB, ABCD - Big Endian - ## |---BA, DCBA - Little Endian - ## |---BADC - Mid-Big Endian - ## |---CDAB - Mid-Little Endian - ## data_type - INT16, UINT16, INT32, UINT32, INT64, UINT64, - ## FLOAT32-IEEE, FLOAT64-IEEE (the IEEE 754 binary representation) - ## FLOAT32, FIXED, UFIXED (fixed-point representation on input) - ## scale - the final numeric variable representation - ## address - variable address - - holding_registers = [ - { name = "power_factor", byte_order = "AB", data_type = "FIXED", scale=0.01, address = [8]}, - { name = "voltage", byte_order = "AB", data_type = "FIXED", scale=0.1, address = [0]}, - { name = "energy", byte_order = "ABCD", data_type = "FIXED", scale=0.001, address = [5,6]}, - { name = "current", byte_order = "ABCD", data_type = "FIXED", scale=0.001, address = [1,2]}, - { name = "frequency", byte_order = "AB", data_type = "UFIXED", scale=0.1, address = [7]}, - { name = "power", byte_order = "ABCD", data_type = "UFIXED", scale=0.1, address = [3,4]}, - ] - input_registers = [ - { name = "tank_level", byte_order = "AB", data_type = "INT16", scale=1.0, address = [0]}, - { name = "tank_ph", byte_order = "AB", data_type = "INT16", scale=1.0, address = [1]}, - { name = "pump1_speed", byte_order = "ABCD", data_type = "INT32", scale=1.0, address = [3,4]}, - ] - + ## Define the configuration schema + ## |---register -- define fields per register type in the original style (only supports one slave ID) + ## |---request -- define fields on a requests base + configuration_type = "register" +` +const sampleConfigEnd = ` ## Enable workarounds required by some devices to work correctly # [inputs.modbus.workarounds] ## Pause between read requests sent to the device. This might be necessary for (slow) serial devices. @@ -175,7 +135,18 @@ const sampleConfig = ` // SampleConfig returns a basic configuration for the plugin func (m *Modbus) SampleConfig() string { - return sampleConfig + configs := []Configuration{} + cfgOriginal := m.ConfigurationOriginal + cfgPerRequest := m.ConfigurationPerRequest + configs = append(configs, &cfgOriginal, &cfgPerRequest) + + totalConfig := sampleConfigStart + for _, c := range configs { + totalConfig += c.SampleConfigPart() + "\n" + } + totalConfig += "\n" + totalConfig += sampleConfigEnd + return totalConfig } // Description returns a short description of what the plugin does @@ -193,14 +164,25 @@ func (m *Modbus) Init() error { return fmt.Errorf("retries cannot be negative") } + // Determine the configuration style + var cfg Configuration + switch m.ConfigurationType { + case "", "register": + cfg = &m.ConfigurationOriginal + case "request": + cfg = &m.ConfigurationPerRequest + default: + return fmt.Errorf("unknown configuration type %q", m.ConfigurationType) + } + // Check and process the configuration - if err := m.ConfigurationOriginal.Check(); err != nil { - return fmt.Errorf("original configuraton invalid: %v", err) + if err := cfg.Check(); err != nil { + return fmt.Errorf("configuraton invalid: %v", err) } - r, err := m.ConfigurationOriginal.Process() + r, err := cfg.Process() if err != nil { - return fmt.Errorf("cannot process original configuraton: %v", err) + return fmt.Errorf("cannot process configuraton: %v", err) } m.requests = r @@ -331,7 +313,6 @@ func (m *Modbus) initClient() error { return fmt.Errorf("invalid controller %q", m.Controller) } - m.handler.SetSlave(m.SlaveID) m.client = mb.NewClient(m.handler) m.isConnected = false @@ -352,7 +333,8 @@ func (m *Modbus) disconnect() error { } func (m *Modbus) gatherFields() error { - for _, requests := range m.requests { + for slaveID, requests := range m.requests { + m.handler.SetSlave(slaveID) if err := m.gatherRequestsCoil(requests.coil); err != nil { return err } diff --git a/plugins/inputs/modbus/modbus_test.go b/plugins/inputs/modbus/modbus_test.go index 4f9f4eca39434..ec1e73cd2dca3 100644 --- a/plugins/inputs/modbus/modbus_test.go +++ b/plugins/inputs/modbus/modbus_test.go @@ -1099,3 +1099,585 @@ func TestRetryFailIllegal(t *testing.T) { require.Equal(t, "modbus: exception '1' (illegal function), function '129'", err.Error()) require.Equal(t, counter, 1) } + +func TestConfigurationRegister(t *testing.T) { + modbus := Modbus{ + Name: "TestRetryFailExhausted", + Controller: "tcp://localhost:1502", + ConfigurationType: "register", + Log: testutil.Logger{}, + } + modbus.SlaveID = 1 + modbus.Coils = []fieldDefinition{ + { + Name: "coil", + Address: []uint16{0}, + }, + } + modbus.DiscreteInputs = []fieldDefinition{ + { + Name: "discrete", + Address: []uint16{0}, + }, + } + modbus.HoldingRegisters = []fieldDefinition{ + { + Name: "holding", + Address: []uint16{0}, + DataType: "INT16", + ByteOrder: "AB", + Scale: 1.0, + }, + } + modbus.InputRegisters = []fieldDefinition{ + { + Name: "input", + Address: []uint16{0}, + DataType: "INT16", + ByteOrder: "AB", + Scale: 1.0, + }, + } + + require.NoError(t, modbus.Init()) + require.NotEmpty(t, modbus.requests) + require.NotNil(t, modbus.requests[1]) + require.Len(t, modbus.requests[1].coil, len(modbus.Coils)) + require.Len(t, modbus.requests[1].discrete, len(modbus.DiscreteInputs)) + require.Len(t, modbus.requests[1].holding, len(modbus.HoldingRegisters)) + require.Len(t, modbus.requests[1].input, len(modbus.InputRegisters)) +} + +func TestConfigurationPerRequest(t *testing.T) { + modbus := Modbus{ + Name: "Test", + Controller: "tcp://localhost:1502", + ConfigurationType: "request", + Log: testutil.Logger{}, + } + modbus.Requests = []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "coil", + Fields: []requestFieldDefinition{ + { + Name: "coil-0", + Address: uint16(0), + }, + { + Name: "coil-1", + Address: uint16(1), + Omit: true, + }, + { + Name: "coil-2", + Address: uint16(2), + InputType: "INT64", + Scale: 1.2, + OutputType: "FLOAT64", + Measurement: "modbus", + }, + }, + }, + { + SlaveID: 1, + RegisterType: "coil", + Fields: []requestFieldDefinition{ + { + Name: "coil-3", + Address: uint16(6), + }, + { + Name: "coil-4", + Address: uint16(7), + Omit: true, + }, + { + Name: "coil-5", + Address: uint16(8), + InputType: "INT64", + Scale: 1.2, + OutputType: "FLOAT64", + Measurement: "modbus", + }, + }, + }, + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "discrete", + Fields: []requestFieldDefinition{ + { + Name: "discrete-0", + Address: uint16(0), + }, + { + Name: "discrete-1", + Address: uint16(1), + Omit: true, + }, + { + Name: "discrete-2", + Address: uint16(2), + InputType: "INT64", + Scale: 1.2, + OutputType: "FLOAT64", + Measurement: "modbus", + }, + }, + }, + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "holding", + Fields: []requestFieldDefinition{ + { + Name: "holding-0", + Address: uint16(0), + InputType: "INT16", + }, + { + Name: "holding-1", + Address: uint16(1), + InputType: "UINT16", + Omit: true, + }, + { + Name: "holding-2", + Address: uint16(2), + InputType: "INT64", + Scale: 1.2, + OutputType: "FLOAT64", + Measurement: "modbus", + }, + }, + }, + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "input", + Fields: []requestFieldDefinition{ + { + Name: "input-0", + Address: uint16(0), + InputType: "INT16", + }, + { + Name: "input-1", + Address: uint16(1), + InputType: "UINT16", + Omit: true, + }, + { + Name: "input-2", + Address: uint16(2), + InputType: "INT64", + Scale: 1.2, + OutputType: "FLOAT64", + Measurement: "modbus", + }, + }, + }, + } + + require.NoError(t, modbus.Init()) + require.NotEmpty(t, modbus.requests) + require.NotNil(t, modbus.requests[1]) + require.Len(t, modbus.requests[1].coil, 2) + require.Len(t, modbus.requests[1].discrete, 1) + require.Len(t, modbus.requests[1].holding, 1) + require.Len(t, modbus.requests[1].input, 1) +} + +func TestConfigurationPerRequestFail(t *testing.T) { + tests := []struct { + name string + requests []requestDefinition + errormsg string + }{ + { + name: "empty field name (coil)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "coil", + Fields: []requestFieldDefinition{ + { + Address: uint16(15), + }, + }, + }, + }, + errormsg: "configuraton invalid: empty field name in request for slave 1", + }, + { + name: "invalid byte-order (coil)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "AB", + RegisterType: "coil", + Fields: []requestFieldDefinition{}, + }, + }, + errormsg: "configuraton invalid: unknown byte-order \"AB\"", + }, + { + name: "duplicate fields (coil)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "coil", + Fields: []requestFieldDefinition{ + { + Name: "coil-0", + Address: uint16(0), + }, + { + Name: "coil-0", + Address: uint16(1), + }, + }, + }, + }, + errormsg: "configuraton invalid: field \"coil-0\" duplicated in measurement \"modbus\" (slave 1/\"coil\")", + }, + { + name: "duplicate fields multiple requests (coil)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "coil", + Fields: []requestFieldDefinition{ + { + Name: "coil-0", + Address: uint16(0), + Measurement: "foo", + }, + }, + }, + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "coil", + Fields: []requestFieldDefinition{ + { + Name: "coil-0", + Address: uint16(0), + Measurement: "foo", + }, + }, + }, + }, + errormsg: "configuraton invalid: field \"coil-0\" duplicated in measurement \"foo\" (slave 1/\"coil\")", + }, + { + name: "invalid byte-order (discrete)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "AB", + RegisterType: "discrete", + Fields: []requestFieldDefinition{}, + }, + }, + errormsg: "configuraton invalid: unknown byte-order \"AB\"", + }, + { + name: "duplicate fields (discrete)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "discrete", + Fields: []requestFieldDefinition{ + { + Name: "discrete-0", + Address: uint16(0), + }, + { + Name: "discrete-0", + Address: uint16(1), + }, + }, + }, + }, + errormsg: "configuraton invalid: field \"discrete-0\" duplicated in measurement \"modbus\" (slave 1/\"discrete\")", + }, + { + name: "duplicate fields multiple requests (discrete)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "discrete", + Fields: []requestFieldDefinition{ + { + Name: "discrete-0", + Address: uint16(0), + Measurement: "foo", + }, + }, + }, + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "discrete", + Fields: []requestFieldDefinition{ + { + Name: "discrete-0", + Address: uint16(0), + Measurement: "foo", + }, + }, + }, + }, + errormsg: "configuraton invalid: field \"discrete-0\" duplicated in measurement \"foo\" (slave 1/\"discrete\")", + }, + { + name: "invalid byte-order (holding)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "AB", + RegisterType: "holding", + Fields: []requestFieldDefinition{}, + }, + }, + errormsg: "configuraton invalid: unknown byte-order \"AB\"", + }, + { + name: "invalid field name (holding)", + requests: []requestDefinition{ + { + SlaveID: 1, + RegisterType: "holding", + Fields: []requestFieldDefinition{ + { + Address: uint16(0), + }, + }, + }, + }, + errormsg: "configuraton invalid: empty field name in request for slave 1", + }, + { + name: "invalid field input type (holding)", + requests: []requestDefinition{ + { + SlaveID: 1, + RegisterType: "holding", + Fields: []requestFieldDefinition{ + { + Name: "holding-0", + Address: uint16(0), + }, + }, + }, + }, + errormsg: "cannot process configuraton: initializing field \"holding-0\" failed: invalid input datatype \"\" for determining field length", + }, + { + name: "invalid field output type (holding)", + requests: []requestDefinition{ + { + SlaveID: 1, + RegisterType: "holding", + Fields: []requestFieldDefinition{ + { + Name: "holding-0", + Address: uint16(0), + InputType: "UINT16", + OutputType: "UINT8", + }, + }, + }, + }, + errormsg: "cannot process configuraton: initializing field \"holding-0\" failed: unknown output type \"UINT8\"", + }, + { + name: "duplicate fields (holding)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "holding", + Fields: []requestFieldDefinition{ + { + Name: "holding-0", + Address: uint16(0), + }, + { + Name: "holding-0", + Address: uint16(1), + }, + }, + }, + }, + errormsg: "configuraton invalid: field \"holding-0\" duplicated in measurement \"modbus\" (slave 1/\"holding\")", + }, + { + name: "duplicate fields multiple requests (holding)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "holding", + Fields: []requestFieldDefinition{ + { + Name: "holding-0", + Address: uint16(0), + Measurement: "foo", + }, + }, + }, + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "holding", + Fields: []requestFieldDefinition{ + { + Name: "holding-0", + Address: uint16(0), + Measurement: "foo", + }, + }, + }, + }, + errormsg: "configuraton invalid: field \"holding-0\" duplicated in measurement \"foo\" (slave 1/\"holding\")", + }, + { + name: "invalid byte-order (input)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "AB", + RegisterType: "input", + Fields: []requestFieldDefinition{}, + }, + }, + errormsg: "configuraton invalid: unknown byte-order \"AB\"", + }, + { + name: "invalid field name (input)", + requests: []requestDefinition{ + { + SlaveID: 1, + RegisterType: "input", + Fields: []requestFieldDefinition{ + { + Address: uint16(0), + }, + }, + }, + }, + errormsg: "configuraton invalid: empty field name in request for slave 1", + }, + { + name: "invalid field input type (input)", + requests: []requestDefinition{ + { + SlaveID: 1, + RegisterType: "input", + Fields: []requestFieldDefinition{ + { + Name: "input-0", + Address: uint16(0), + }, + }, + }, + }, + errormsg: "cannot process configuraton: initializing field \"input-0\" failed: invalid input datatype \"\" for determining field length", + }, + { + name: "invalid field output type (input)", + requests: []requestDefinition{ + { + SlaveID: 1, + RegisterType: "input", + Fields: []requestFieldDefinition{ + { + Name: "input-0", + Address: uint16(0), + InputType: "UINT16", + OutputType: "UINT8", + }, + }, + }, + }, + errormsg: "cannot process configuraton: initializing field \"input-0\" failed: unknown output type \"UINT8\"", + }, + { + name: "duplicate fields (input)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "input", + Fields: []requestFieldDefinition{ + { + Name: "input-0", + Address: uint16(0), + }, + { + Name: "input-0", + Address: uint16(1), + }, + }, + }, + }, + errormsg: "configuraton invalid: field \"input-0\" duplicated in measurement \"modbus\" (slave 1/\"input\")", + }, + { + name: "duplicate fields multiple requests (input)", + requests: []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "input", + Fields: []requestFieldDefinition{ + { + Name: "input-0", + Address: uint16(0), + Measurement: "foo", + }, + }, + }, + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "input", + Fields: []requestFieldDefinition{ + { + Name: "input-0", + Address: uint16(0), + Measurement: "foo", + }, + }, + }, + }, + errormsg: "configuraton invalid: field \"input-0\" duplicated in measurement \"foo\" (slave 1/\"input\")", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + plugin := Modbus{ + Name: "Test", + Controller: "tcp://localhost:1502", + ConfigurationType: "request", + Log: testutil.Logger{}, + } + plugin.Requests = tt.requests + + err := plugin.Init() + require.Error(t, err) + require.Equal(t, tt.errormsg, err.Error()) + require.Empty(t, plugin.requests) + }) + } +} diff --git a/plugins/inputs/modbus/request.go b/plugins/inputs/modbus/request.go index b2a31d9dcf4d3..890f730db6759 100644 --- a/plugins/inputs/modbus/request.go +++ b/plugins/inputs/modbus/request.go @@ -10,7 +10,7 @@ type request struct { fields []field } -func newRequestsFromFields(fields []field, slaveID byte, registerType string, maxBatchSize uint16) []request { +func newRequestsFromFields(fields []field, maxBatchSize uint16) []request { if len(fields) == 0 { return nil } @@ -40,9 +40,12 @@ func newRequestsFromFields(fields []field, slaveID byte, registerType string, ma needInterrupt = needInterrupt || f.length+current.length > maxBatchSize // too large if !needInterrupt { - // Still save to add the field to the current request + // Still safe 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 + if !f.omit { + // Omit adding the field but use it for constructing the request. + current.fields = append(current.fields, f) + } continue } diff --git a/plugins/inputs/modbus/type_conversions16.go b/plugins/inputs/modbus/type_conversions16.go index 088a5d10c445a..8fc920d217dbb 100644 --- a/plugins/inputs/modbus/type_conversions16.go +++ b/plugins/inputs/modbus/type_conversions16.go @@ -11,9 +11,9 @@ type convert16 func([]byte) uint16 func endianessConverter16(byteOrder string) (convert16, error) { switch byteOrder { - case "ABCD": // Big endian (Motorola) + case "ABCD", "CDAB": // Big endian (Motorola) return binary.BigEndian.Uint16, nil - case "DCBA": // Little endian (Intel) + case "DCBA", "BADC": // Little endian (Intel) return binary.LittleEndian.Uint16, nil } return nil, fmt.Errorf("invalid byte-order: %s", byteOrder)