1
0
Fork 0

Adding upstream version 1.34.4.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-24 07:26:29 +02:00
parent e393c3af3f
commit 4978089aab
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
4963 changed files with 677545 additions and 0 deletions

View file

@ -0,0 +1,885 @@
<!-- markdownlint-disable MD024 -->
# Modbus Input Plugin
This plugin collects data from [Modbus][modbus] registers using e.g. Modbus TCP
or serial interfaces with Modbus RTU or Modbus ASCII.
⭐ Telegraf v1.14.0
🏷️ iot
💻 all
[modbus]: https://www.modbus.org/
## Global configuration options <!-- @/docs/includes/plugin_config.md -->
In addition to the plugin-specific configuration settings, plugins support
additional global and plugin configuration settings. These settings are used to
modify metrics, tags, and field or create aliases and configure ordering, etc.
See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
[CONFIGURATION.md]: ../../../docs/CONFIGURATION.md#plugins
## Configuration
```toml @sample_general_begin.conf @sample_register.conf @sample_request.conf @sample_metric.conf @sample_general_end.conf
# Retrieve data from MODBUS slave devices
[[inputs.modbus]]
## Connection Configuration
##
## The plugin supports connections to PLCs via MODBUS/TCP, RTU over TCP, ASCII over TCP or
## via serial line communication in binary (RTU) or readable (ASCII) encoding
##
## Device name
name = "Device"
## Slave ID - addresses a MODBUS device on the bus
## Range: 0 - 255 [0 = broadcast; 248 - 255 = reserved]
slave_id = 1
## Timeout for each request
timeout = "1s"
## Maximum number of retries and the time to wait between retries
## when a slave-device is busy.
# busy_retries = 0
# busy_retries_wait = "100ms"
# TCP - connect via Modbus/TCP
controller = "tcp://localhost:502"
## Serial (RS485; RS232)
## For RS485 specific setting check the end of the configuration.
## For unix-like operating systems use:
# controller = "file:///dev/ttyUSB0"
## For Windows operating systems use:
# controller = "COM1"
# baud_rate = 9600
# data_bits = 8
# parity = "N"
# stop_bits = 1
## Transmission mode for Modbus packets depending on the controller type.
## For Modbus over TCP you can choose between "TCP" , "RTUoverTCP" and
## "ASCIIoverTCP".
## For Serial controllers you can choose between "RTU" and "ASCII".
## By default this is set to "auto" selecting "TCP" for ModbusTCP connections
## and "RTU" for serial connections.
# transmission_mode = "auto"
## Trace the connection to the modbus device
# log_level = "trace"
## 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
## |---metric -- define fields on a metric base
configuration_type = "register"
## Exclude the register type tag
## Please note, this will also influence the grouping of metrics as you won't
## see one metric per register type anymore!
# exclude_register_type_tag = false
## --- "register" configuration style ---
## Measurements
##
## Digital Variables, Discrete Inputs and Coils
## measurement - the (optional) measurement name, defaults to "modbus"
## name - the variable name
## data_type - the (optional) output type, can be BOOL or UINT16 (default)
## 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 - BIT (single bit of a register)
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64,
## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation)
## FIXED, UFIXED (fixed-point representation on input)
## STRING (byte-sequence converted to string)
## bit - (optional) bit of the register, ONLY valid for BIT type
## 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]},
{ name = "firmware", byte_order = "AB", data_type = "STRING", address = [5, 6, 7, 8, 9, 10, 11, 12]},
]
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]},
]
## --- "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 overridden by the individual field definitions. Defaults to "modbus"
# measurement = "modbus"
## Request optimization algorithm.
## |---none -- Do not perform any optimization and use the given layout(default)
## |---shrink -- Shrink requests to actually requested fields
## | by stripping leading and trailing omits
## |---rearrange -- Rearrange request boundaries within consecutive address ranges
## | to reduce the number of requested registers by keeping
## | the number of requests.
## |---max_insert -- Rearrange request keeping the number of extra fields below the value
## provided in "optimization_max_register_fill". It is not necessary to define 'omitted'
## fields as the optimisation will add such field only where needed.
# optimization = "none"
## Maximum number register the optimizer is allowed to insert between two fields to
## save requests.
## This option is only used for the 'max_insert' optimization strategy.
## NOTE: All omitted fields are ignored, so this option denotes the effective hole
## size to fill.
# optimization_max_register_fill = 50
## 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
## BIT (single bit of a register)
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
## STRING (byte-sequence converted to string)
## length *1,2 - (optional) number of registers, ONLY valid for STRING type
## bit *1,2 - (optional) bit of the register, ONLY valid for BIT type
## scale *1,2,4 - (optional) factor to scale the variable with
## output *1,3,4 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64.
## Defaults to FLOAT64 for numeric fields if "scale" is provided.
## Otherwise the input "type" class is used (e.g. INT* -> INT64).
## 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: These fields are ignored if field is omitted ("omit"=true)
## *2: These fields are ignored for both "coil" and "discrete"-input type of registers.
## *3: This field can only be "UINT16" or "BOOL" if specified for both "coil"
## and "discrete"-input type of registers. By default the fields are
## output as zero or one in UINT16 format unless "BOOL" is used.
## *4: These fields cannot be used with "STRING"-type fields.
## 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", output="BOOL" },
{ address=4, name="firmware", type="STRING", length=8 },
]
[inputs.modbus.request.tags]
machine = "impresser"
location = "main building"
[[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.tags]
machine = "impresser"
location = "main building"
[[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
]
[inputs.modbus.request.tags]
machine = "impresser"
location = "main building"
## --- "metric" configuration style ---
## Per metric definition
##
## Request optimization algorithm across metrics
## |---none -- Do not perform any optimization and just group requests
## | within metrics (default)
## |---max_insert -- Collate registers across all defined metrics and fill in
## holes to optimize the number of requests.
# optimization = "none"
## Maximum number of registers the optimizer is allowed to insert between
## non-consecutive registers to save requests.
## This option is only used for the 'max_insert' optimization strategy and
## effectively denotes the hole size between registers to fill.
# optimization_max_register_fill = 50
## Define a metric produced by the requests to the device
## Multiple of those metrics can be defined. The referenced registers will
## be collated into requests send to the device
[[inputs.modbus.metric]]
## ID of the modbus slave device to query
## If you need to query multiple slave-devices, create several "metric" 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"
## Name of the measurement
# measurement = "modbus"
## Field definitions
## register - type of the modbus register, can be "coil", "discrete",
## "holding" or "input". Defaults to "holding".
## address - address of the register to query. For coil and discrete inputs this is the bit address.
## name - field name
## type *1 - type of the modbus field, can be
## BIT (single bit of a register)
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
## STRING (byte-sequence converted to string)
## length *1 - (optional) number of registers, ONLY valid for STRING type
## bit *1,2 - (optional) bit of the register, ONLY valid for BIT type
## scale *1,3 - (optional) factor to scale the variable with
## output *2,3 - (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).
##
## *1: These fields are ignored for both "coil" and "discrete"-input type of registers.
## *2: This field can only be "UINT16" or "BOOL" if specified for both "coil"
## and "discrete"-input type of registers. By default the fields are
## output as zero or one in UINT16 format unless "BOOL" is used.
## *3: These fields cannot be used with "STRING"-type fields.
fields = [
{ register="coil", address=0, name="door_open"},
{ register="coil", address=1, name="status_ok"},
{ register="holding", address=0, name="voltage", type="INT16" },
{ address=1, name="current", type="INT32", scale=0.001 },
{ address=5, name="energy", type="FLOAT32", scale=0.001 },
{ address=7, name="frequency", type="UINT32", scale=0.1 },
{ address=8, name="power_factor", type="INT64", scale=0.01 },
{ address=9, name="firmware", type="STRING", length=8 },
]
## Tags assigned to the metric
# [inputs.modbus.metric.tags]
# machine = "impresser"
# location = "main building"
## RS485 specific settings. Only take effect for serial controllers.
## Note: This has to be at the end of the modbus configuration due to
## TOML constraints.
# [inputs.modbus.rs485]
## Delay RTS prior to sending
# delay_rts_before_send = "0ms"
## Delay RTS after to sending
# delay_rts_after_send = "0ms"
## Pull RTS line to high during sending
# rts_high_during_send = false
## Pull RTS line to high after sending
# rts_high_after_send = false
## Enabling receiving (Rx) during transmission (Tx)
# rx_during_tx = false
## Enable workarounds required by some devices to work correctly
# [inputs.modbus.workarounds]
## Pause after connect delays the first request by the specified time.
## This might be necessary for (slow) devices.
# pause_after_connect = "0ms"
## Pause between read requests sent to the device.
## This might be necessary for (slow) serial devices.
# pause_between_requests = "0ms"
## Close the connection after every gather cycle.
## Usually the plugin closes the connection after a certain idle-timeout,
## however, if you query a device with limited simultaneous connectivity
## (e.g. serial devices) from multiple instances you might want to only
## stay connected during gather and disconnect afterwards.
# close_connection_after_gather = false
## Force the plugin to read each field in a separate request.
## This might be necessary for devices not conforming to the spec,
## see https://github.com/influxdata/telegraf/issues/12071.
# one_request_per_field = false
## Enforce the starting address to be zero for the first request on
## coil registers. This is necessary for some devices see
## https://github.com/influxdata/telegraf/issues/8905
# read_coils_starting_at_zero = false
## String byte-location in registers AFTER byte-order conversion
## Some device (e.g. EM340) place the string byte in only the upper or
## lower byte location of a register see
## https://github.com/influxdata/telegraf/issues/14748
## Available settings:
## lower -- use only lower byte of the register i.e. 00XX 00XX 00XX 00XX
## upper -- use only upper byte of the register i.e. XX00 XX00 XX00 XX00
## By default both bytes of the register are used i.e. XXXX XXXX.
# string_register_location = ""
```
## Notes
You can debug Modbus connection issues by enabling `debug_connection`. To see
those debug messages, Telegraf has to be started with debugging enabled
(i.e. with the `--debug` option). Please be aware that connection tracing will
produce a lot of messages and should __NOT__ be used in production environments.
Please use `pause_after_connect` / `pause_between_requests` with care. Ensure
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!
## 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)
- [per-metrict style](#metric-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.
> [!NOTE]
> For legacy reasons this configuration style is not completely consistent with the other styles.
#### 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 appropriate when sending the value to the output
plugin. These output types are usually an 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.
##### Booleans: `BOOL`
This type is only valid for _coil_ and _discrete_ registers. The value will be
`true` if the register has a non-zero (ON) value and `false` otherwise.
##### Integers: `INT8L`, `INT8H`, `UINT8L`, `UINT8H`
These types are used for 8-bit integer values. Select the one that matches your
modbus data source. The `L` and `H` suffix denotes the low- and high byte of
the register respectively.
##### Integers: `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`
These types are used for integer input values. Select the one that matches your
modbus data source. For _coil_ and _discrete_ registers only `UINT16` is valid.
##### Floating Point: `FLOAT16-IEEE`, `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, therefore no variant exists.
##### Fixed Point: `FIXED`, `UFIXED`
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.
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 'uint16 containing fixed-point
representation with N decimal places'.
Select the type `FIXED` when the input type is declared to hold signed integer
values. Your documentation of the modbus device should indicate this with a term
like 'int32 containing fixed-point representation with N decimal places'.
##### String: `STRING`
This type is used to query the number of registers specified in the `address`
setting and convert the byte-sequence to a string. Please note, if the
byte-sequence contains a `null` byte, the string is truncated at this position.
You cannot use the `scale` setting for string fields.
##### Bit: `BIT`
This type is used to query a single bit of a register specified in the `address`
setting and convert the value to an unsigned integer. This type __requires__ the
`bit` setting to be specified.
---
### `request` configuration style
This style can be used to specify the modbus requests directly. It enables
specifying multiple `[[inputs.modbus.request]]` sections including multiple
slave-devices. This way, _modbus_ gateway devices can be queried. Please note
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, otherwise it defaults to
zero. Please note, only one `slave_id` can be specified per 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.
#### Optimization setting
__Please only use request optimization if you do understand the implications!__
The `optimization` setting can be used to optimize the actual requests sent to
the device. The following algorithms are available
##### `none` (_default_)
Do not perform any optimization. Please note that the requests are still obeying
the maximum request sizes. Furthermore, completely empty requests, i.e. all
fields specify `omit=true`, are removed. Otherwise, the requests are sent as
specified by the user including request of omitted fields. This setting should
be used if you want full control over the requests e.g. to accommodate for
device constraints.
##### `shrink`
This optimization allows to remove leading and trailing fields from requests if
those fields are omitted. This can shrink the request number and sizes in cases
where you specify large amounts of omitted fields, e.g. for documentation
purposes.
##### `rearrange`
Requests are processed similar to `shrink` but the request boundaries are
rearranged such that usually less registers are being read while keeping the
number of requests. This optimization algorithm only works on consecutive
address ranges and respects user-defined gaps in the field addresses.
__Please note:__ This optimization might take long in case of many
non-consecutive, non-omitted fields!
##### `aggressive`
Requests are processed similar to `rearrange` but user-defined gaps in the field
addresses are filled automatically. This usually reduces the number of requests,
but will increase the number of registers read due to larger requests.
This algorithm might be useful if you only want to specify the fields you are
interested in but want to minimize the number of requests sent to the device.
__Please note:__ This optimization might take long in case of many
non-consecutive, non-omitted fields!
##### `max_insert`
Fields are assigned to the same request as long as the hole between the fields
do not exceed the maximum fill size given in `optimization_max_register_fill`.
User-defined omitted fields are ignored and interpreted as holes, so the best
practice is to not manually insert omitted fields for this optimizer. This
allows to specify only actually used fields and let the optimizer figure out
the request organization which can dramatically improve query time. The
trade-off here is between the cost of reading additional registers trashed
later and the cost of many requests.
__Please note:__ The optimal value for `optimization_max_register_fill` depends
on the network and the queried device. It is hence recommended to test several
values and assess performance in order to find the best value. Use the
`--test --debug` flags to monitor how may requests are sent and the number of
touched registers.
#### 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 data-points
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 `type` setting specifies the datatype of the modbus register and can be
set to `INT8L`, `INT8H`, `UINT8L`, `UINT8H` where `L` is the lower byte of the
register and `H` is the higher byte.
Furthermore, the types `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64` or `UINT64`
for integer types or `FLOAT16`, `FLOAT32` and `FLOAT64` for IEEE 754 binary
representations of floating point values exist. `FLOAT16` denotes a
half-precision float with a 16-bit representation.
Usually the datatype of the register is listed in the datasheet of your modbus
device in relation to the `address` described above.
The `STRING` datatype is special in that it requires the `length` setting to
be specified containing the length (in terms of number of registers) containing
the string. The returned byte-sequence is interpreted as string and truncated
to the first `null` byte found if any. The `scale` and `output` setting cannot
be used for this `type`.
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 e.g. `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 can 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` and can be
omitted. In case the `register` type is a bit-type (`coil` or `discrete`) only
`UINT16` or `BOOL` are valid with the former being the default if omitted.
For `coil` and `discrete` registers the field-value is output as zero or one in
`UINT16` format or as `true` and `false` in `BOOL` 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 want 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.
#### Tags definitions
Each `request` can be accompanied by tags valid for this request.
__Please note:__ These tags take precedence over predefined tags such as `name`,
`type` or `slave_id`.
---
### `metric` configuration style
This style can be used to specify the desired metrics directly instead of
focusing on the modbus view. Multiple `[[inputs.modbus.metric]]` sections
including multiple slave-devices can be specified. This way, _modbus_ gateway
devices can be queried. The plugin automatically collects registers across
the specified metrics, groups them per slave and register-type and (optionally)
optimizes the resulting requests for non-consecutive 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 metric section, otherwise it defaults to
zero. Please note, only one `slave_id` can be specified per metric section.
#### Byte order of the registers
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_.
#### Measurement name
You can specify the name of the measurement for the fields defined in the
given section using the `measurement` setting. If the setting is omitted
`modbus` is used.
#### Optimization setting
__Please only use request optimization if you do understand the implications!__
The `optimization` setting can specified globally, i.e. __NOT__ per metric
section, and is used to optimize the actual requests sent to the device. Here,
the optimization is applied across _all metric sections_! The following
algorithms are available
##### `none` (_default_)
Do not perform any optimization. Please note that consecutive registers are
still grouped into one requests while obeying the maximum request sizes. This
setting should be used if you want to touch as less registers as possible at
the cost of more requests sent to the device.
##### `max_insert`
Fields are assigned to the same request as long as the hole between the touched
registers does not exceed the maximum fill size given via
`optimization_max_register_fill`. This optimization might lead to a drastically
reduced request number and thus an improved query time. The trade-off here is
between the cost of reading additional registers trashed later and the cost of
many requests.
__Please note:__ The optimal value for `optimization_max_register_fill` depends
on the network and the queried device. It is hence recommended to test several
values and assess performance in order to find the best value. Use the
`--test --debug` flags to monitor how may requests are sent and the number of
touched registers.
#### Field definitions
Each `metric` can contain a list of fields to collect from the modbus device.
The specified fields directly corresponds to the fields of the resulting metric.
##### register
The `register` setting specifies the modbus register-set to query and can be set
to `coil`, `discrete`, `holding` or `input`.
##### address
A field is identified by an `address` that reflects the modbus register
address. You can usually find the address values for the different data-points
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.
__Please note:__ There cannot be multiple fields with the same `name` in one
metric identified by `measurement`, `slave_id`, `register` and tag-set.
##### register datatype
The `type` setting specifies the datatype of the modbus register and can be
set to `INT8L`, `INT8H`, `UINT8L`, `UINT8H` where `L` is the lower byte of the
register and `H` is the higher byte.
Furthermore, the types `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64` or `UINT64`
for integer types or `FLOAT16`, `FLOAT32` and `FLOAT64` for IEEE 754 binary
representations of floating point values exist. `FLOAT16` denotes a
half-precision float with a 16-bit representation.
Usually the datatype of the register is listed in the datasheet of your modbus
device in relation to the `address` described above.
The `STRING` datatype is special in that it requires the `length` setting to
be specified containing the length (in terms of number of registers) containing
the string. The returned byte-sequence is interpreted as string and truncated
to the first `null` byte found if any. The `scale` and `output` setting cannot
be used for this `type`.
This setting is ignored if the `register` 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 e.g. `field_value * scale`.
This setting is ignored if the `register` 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 can 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`.
In case the `register` is a bit-type (`coil` or `discrete`) only `UINT16` or
`BOOL` are valid with the former being the default if omitted. For `coil` and
`discrete` registers the field-value is output as zero or one in `UINT16` format
or as `true` and `false` in `BOOL` format.
#### Tags definitions
Each `metric` can be accompanied by a set of tag. These tags directly correspond
to the tags of the resulting metric.
__Please note:__ These tags take precedence over predefined tags such as `name`,
`type` or `slave_id`.
---
## Troubleshooting
### Strange data
Modbus documentation is often a mess. People confuse memory-address (starts at
one) and register address (starts at zero) or are unsure about the word-order
used. 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).
If you are using a serial device and get a `permission denied` error, check the
permissions of your serial device and change them 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
confusion between memory and register addresses.
If you see strange values, the `byte_order` might be wrong. You can either probe
all combinations (`ABCD`, `CDBA`, `BADC` or `DCBA`) or set `byte_order="ABCD"
data_type="UINT32"` and use the resulting value(s) in an online converter like
[this][online-converter]. This especially makes 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 an 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 an issue).
[online-converter]: https://www.scadacore.com/tools/programming-calculators/online-hex-converter/
### Workarounds
Some Modbus devices need special read characteristics when reading data and will
fail otherwise. For example, some serial devices need a pause between register
read requests. Others might only support a limited number of simultaneously
connected devices, like serial devices or some ModbusTCP devices. In case you
need to access those devices in parallel you might want to disconnect
immediately after the plugin finishes reading.
To enable this plugin to also handle those "special" devices, there is the
`workarounds` configuration option. In case your documentation states certain
read requirements or you get read timeouts or other read errors, you might want
to try one or more workaround options. If you find that other/more workarounds
are required for your device, please let us know.
In case your device needs a workaround that is not yet implemented, please open
an issue or submit a pull-request.
## Metrics
The plugin reads the configured registers and constructs metrics based on the
specified configuration. There is no predefined metric format.
## Example Output
```text
modbus,name=device,slave_id=1,type=holding_register energy=3254.5,power=23.5,frequency=49,97 1701777274026591864
```

View file

@ -0,0 +1,64 @@
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)
sampleConfigPart() string
}
func removeDuplicates(elements []uint16) []uint16 {
encountered := make(map[uint16]bool, len(elements))
result := make([]uint16, 0, len(elements))
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 "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H",
"INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64",
"FLOAT16", "FLOAT32", "FLOAT64", "STRING":
return dataType, nil
}
return "unknown", fmt.Errorf("unknown input type %q", dataType)
}
func normalizeOutputDatatype(dataType string) (string, error) {
switch dataType {
case "", "native":
return "native", nil
case "INT64", "UINT64", "FLOAT64", "STRING":
return dataType, nil
}
return "unknown", fmt.Errorf("unknown output 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)
}

View file

@ -0,0 +1,399 @@
package modbus
import (
_ "embed"
"errors"
"fmt"
"hash/maphash"
"math"
"github.com/influxdata/telegraf"
)
//go:embed sample_metric.conf
var sampleConfigPartPerMetric string
type metricFieldDefinition struct {
RegisterType string `toml:"register"`
Address uint16 `toml:"address"`
Length uint16 `toml:"length"`
Name string `toml:"name"`
InputType string `toml:"type"`
Scale float64 `toml:"scale"`
OutputType string `toml:"output"`
Bit uint8 `toml:"bit"`
}
type metricDefinition struct {
SlaveID byte `toml:"slave_id"`
ByteOrder string `toml:"byte_order"`
Measurement string `toml:"measurement"`
Fields []metricFieldDefinition `toml:"fields"`
Tags map[string]string `toml:"tags"`
}
type configurationPerMetric struct {
Optimization string `toml:"optimization"`
MaxExtraRegisters uint16 `toml:"optimization_max_register_fill"`
Metrics []metricDefinition `toml:"metric"`
workarounds workarounds
excludeRegisterType bool
logger telegraf.Logger
}
func (*configurationPerMetric) sampleConfigPart() string {
return sampleConfigPartPerMetric
}
func (c *configurationPerMetric) check() error {
switch c.workarounds.StringRegisterLocation {
case "", "both", "lower", "upper":
// Do nothing as those are valid
default:
return fmt.Errorf("invalid 'string_register_location' %q", c.workarounds.StringRegisterLocation)
}
seed := maphash.MakeSeed()
seenFields := make(map[uint64]bool)
// Check optimization algorithm
switch c.Optimization {
case "", "none":
c.Optimization = "none"
case "max_insert":
if c.MaxExtraRegisters == 0 {
c.MaxExtraRegisters = 50
}
default:
return fmt.Errorf("unknown optimization %q", c.Optimization)
}
for defidx, def := range c.Metrics {
// 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)
}
// Set the default for measurement if required
if def.Measurement == "" {
def.Measurement = "modbus"
}
// Reject any configuration without fields as it
// makes no sense to not define anything but a request.
if len(def.Fields) == 0 {
return errors.New("found request section without fields")
}
// Check the fields
for fidx, f := range def.Fields {
// Name is mandatory
if f.Name == "" {
return fmt.Errorf("empty field name in request for slave %d", def.SlaveID)
}
// Check register type
switch f.RegisterType {
case "":
f.RegisterType = "holding"
case "coil", "discrete", "holding", "input":
default:
return fmt.Errorf("unknown register-type %q for field %q", f.RegisterType, f.Name)
}
// Check the input and output type for all fields as we later need
// it to determine the number of registers to query.
switch f.RegisterType {
case "holding", "input":
// Check the input type
switch f.InputType {
case "":
case "INT8L", "INT8H", "INT16", "INT32", "INT64",
"UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64",
"FLOAT16", "FLOAT32", "FLOAT64":
if f.Length != 0 {
return fmt.Errorf("length option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.Bit != 0 {
return fmt.Errorf("bit option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.OutputType == "STRING" {
return fmt.Errorf("cannot output field %q as string", f.Name)
}
case "STRING":
if f.Length < 1 {
return fmt.Errorf("missing length for string field %q", f.Name)
}
if f.Bit != 0 {
return fmt.Errorf("bit option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.Scale != 0.0 {
return fmt.Errorf("scale option cannot be used for string field %q", f.Name)
}
if f.OutputType != "" && f.OutputType != "STRING" {
return fmt.Errorf("invalid output type %q for string field %q", f.OutputType, f.Name)
}
case "BIT":
if f.Length != 0 {
return fmt.Errorf("length option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.OutputType == "STRING" {
return fmt.Errorf("cannot output field %q as string", f.Name)
}
default:
return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name)
}
// Check output type
switch f.OutputType {
case "", "INT64", "UINT64", "FLOAT64", "STRING":
default:
return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name)
}
case "coil", "discrete":
// Bit register types can only be UINT64 or BOOL
switch f.OutputType {
case "", "UINT16", "BOOL":
default:
return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name)
}
}
def.Fields[fidx] = f
// Check for duplicate field definitions
id := c.fieldID(seed, def, f)
if seenFields[id] {
return fmt.Errorf("field %q duplicated in measurement %q (slave %d)", f.Name, def.Measurement, def.SlaveID)
}
seenFields[id] = true
}
c.Metrics[defidx] = def
}
return nil
}
func (c *configurationPerMetric) process() (map[byte]requestSet, error) {
collection := make(map[byte]map[string][]field)
// Collect the requested registers across metrics and transform them into
// requests. This will produce one request per slave and register-type
for _, def := range c.Metrics {
// Make sure we have a set to work with
set, found := collection[def.SlaveID]
if !found {
set = make(map[string][]field)
}
for _, fdef := range def.Fields {
// Construct the field from the field definition
f, err := c.newField(fdef, def)
if err != nil {
return nil, fmt.Errorf("initializing field %q of measurement %q failed: %w", fdef.Name, def.Measurement, err)
}
// Attach the field to the correct set
set[fdef.RegisterType] = append(set[fdef.RegisterType], f)
}
collection[def.SlaveID] = set
}
result := make(map[byte]requestSet)
params := groupingParams{
optimization: c.Optimization,
maxExtraRegisters: c.MaxExtraRegisters,
log: c.logger,
}
for sid, scollection := range collection {
var set requestSet
for registerType, fields := range scollection {
switch registerType {
case "coil":
params.maxBatchSize = maxQuantityCoils
if c.workarounds.OnRequestPerField {
params.maxBatchSize = 1
}
params.enforceFromZero = c.workarounds.ReadCoilsStartingAtZero
requests := groupFieldsToRequests(fields, params)
set.coil = append(set.coil, requests...)
case "discrete":
params.maxBatchSize = maxQuantityDiscreteInput
if c.workarounds.OnRequestPerField {
params.maxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.discrete = append(set.discrete, requests...)
case "holding":
params.maxBatchSize = maxQuantityHoldingRegisters
if c.workarounds.OnRequestPerField {
params.maxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.holding = append(set.holding, requests...)
case "input":
params.maxBatchSize = maxQuantityInputRegisters
if c.workarounds.OnRequestPerField {
params.maxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.input = append(set.input, requests...)
default:
return nil, fmt.Errorf("unknown register type %q", registerType)
}
}
if !set.empty() {
result[sid] = set
}
}
return result, nil
}
func (c *configurationPerMetric) newField(def metricFieldDefinition, mdef metricDefinition) (field, error) {
typed := def.RegisterType == "holding" || def.RegisterType == "input"
fieldLength := uint16(1)
if typed {
var err error
if fieldLength, err = c.determineFieldLength(def.InputType, def.Length); err != nil {
return field{}, err
}
}
// Check for address overflow
if def.Address > math.MaxUint16-fieldLength {
return field{}, fmt.Errorf("%w for field %q", errAddressOverflow, def.Name)
}
// Initialize the field
f := field{
measurement: mdef.Measurement,
name: def.Name,
address: def.Address,
length: fieldLength,
tags: mdef.Tags,
}
// Handle type conversions for coil and discrete registers
if !typed {
var err error
f.converter, err = determineUntypedConverter(def.OutputType)
if err != nil {
return field{}, err
}
// No more processing for un-typed (coil and discrete registers) fields
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 except for
// string fields
if def.InputType != "STRING" {
def.OutputType = "FLOAT64"
} else {
def.OutputType = "STRING"
}
}
}
// Setting default byte-order
byteOrder := mdef.ByteOrder
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, def.Bit, c.workarounds.StringRegisterLocation)
if err != nil {
return field{}, err
}
return f, nil
}
func (c *configurationPerMetric) fieldID(seed maphash.Seed, def metricDefinition, field metricFieldDefinition) uint64 {
var mh maphash.Hash
mh.SetSeed(seed)
mh.WriteByte(def.SlaveID)
mh.WriteByte(0)
if !c.excludeRegisterType {
mh.WriteString(field.RegisterType)
mh.WriteByte(0)
}
mh.WriteString(def.Measurement)
mh.WriteByte(0)
mh.WriteString(field.Name)
mh.WriteByte(0)
// tags
for k, v := range def.Tags {
mh.WriteString(k)
mh.WriteByte('=')
mh.WriteString(v)
mh.WriteByte(':')
}
mh.WriteByte(0)
return mh.Sum64()
}
func (*configurationPerMetric) determineOutputDatatype(input string) (string, error) {
// Handle our special types
switch input {
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
return "INT64", nil
case "BIT", "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
return "UINT64", nil
case "FLOAT16", "FLOAT32", "FLOAT64":
return "FLOAT64", nil
case "STRING":
return "STRING", nil
}
return "unknown", fmt.Errorf("invalid input datatype %q for determining output", input)
}
func (*configurationPerMetric) determineFieldLength(input string, length uint16) (uint16, error) {
// Handle our special types
switch input {
case "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H":
return 1, nil
case "INT16", "UINT16", "FLOAT16":
return 1, nil
case "INT32", "UINT32", "FLOAT32":
return 2, nil
case "INT64", "UINT64", "FLOAT64":
return 4, nil
case "STRING":
return length, nil
}
return 0, fmt.Errorf("invalid input datatype %q for determining field length", input)
}

View file

@ -0,0 +1,393 @@
package modbus
import (
"testing"
"time"
mb "github.com/grid-x/modbus"
"github.com/stretchr/testify/require"
"github.com/tbrandon/mbserver"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/testutil"
)
func TestMetric(t *testing.T) {
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "metric",
Log: testutil.Logger{},
}
plugin.Metrics = []metricDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
Measurement: "test",
Fields: []metricFieldDefinition{
{
Name: "coil-0",
Address: uint16(0),
RegisterType: "coil",
},
{
Name: "coil-1",
Address: uint16(1),
RegisterType: "coil",
},
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
},
{
Name: "holding-1",
Address: uint16(1),
InputType: "UINT16",
RegisterType: "holding",
},
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
Fields: []metricFieldDefinition{
{
Name: "coil-0",
Address: uint16(2),
RegisterType: "coil",
},
{
Name: "coil-1",
Address: uint16(3),
RegisterType: "coil",
},
{
Name: "holding-0",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "FLOAT64",
},
},
Tags: map[string]string{
"location": "main building",
"device": "mydevice",
},
},
{
SlaveID: 2,
Fields: []metricFieldDefinition{
{
Name: "coil-6",
Address: uint16(6),
RegisterType: "coil",
},
{
Name: "coil-7",
Address: uint16(7),
RegisterType: "coil",
},
{
Name: "discrete-0",
Address: uint16(0),
RegisterType: "discrete",
},
{
Name: "holding-99",
Address: uint16(99),
InputType: "INT16",
},
},
},
{
SlaveID: 2,
Fields: []metricFieldDefinition{
{
Name: "coil-4",
Address: uint16(4),
RegisterType: "coil",
},
{
Name: "coil-5",
Address: uint16(5),
RegisterType: "coil",
},
{
Name: "input-0",
Address: uint16(0),
RegisterType: "input",
InputType: "UINT16",
},
{
Name: "input-1",
Address: uint16(2),
RegisterType: "input",
InputType: "UINT16",
},
{
Name: "holding-9",
Address: uint16(9),
InputType: "INT16",
},
},
Tags: map[string]string{
"location": "main building",
"device": "mydevice",
},
},
}
require.NoError(t, plugin.Init())
require.NotEmpty(t, plugin.requests)
require.NotNil(t, plugin.requests[1])
require.Len(t, plugin.requests[1].coil, 1, "coil 1")
require.Len(t, plugin.requests[1].holding, 1, "holding 1")
require.Empty(t, plugin.requests[1].discrete)
require.Empty(t, plugin.requests[1].input)
require.NotNil(t, plugin.requests[2])
require.Len(t, plugin.requests[2].coil, 1, "coil 2")
require.Len(t, plugin.requests[2].holding, 2, "holding 2")
require.Len(t, plugin.requests[2].discrete, 1, "discrete 2")
require.Len(t, plugin.requests[2].input, 2, "input 2")
}
func TestMetricResult(t *testing.T) {
data := []byte{
0x00, 0x0A, // 10
0x00, 0x2A, // 42
0x00, 0x00, 0x08, 0x98, // 2200
0x00, 0x00, 0x08, 0x99, // 2201
0x00, 0x00, 0x08, 0x9A, // 2202
0x40, 0x49, 0x0f, 0xdb, // float32 of 3.1415927410125732421875
0x4d, 0x6f, 0x64, 0x62, 0x75, 0x73, 0x20, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x00, // String "Modbus String"
}
// Write the data to a fake server
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)
quantity := uint16(len(data) / 2)
_, err := client.WriteMultipleRegisters(1, quantity, data)
require.NoError(t, err)
// Setup the plugin
plugin := Modbus{
Name: "FAKEMETER",
Controller: "tcp://localhost:1502",
ConfigurationType: "metric",
Log: testutil.Logger{},
}
plugin.Metrics = []metricDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
Measurement: "machine",
Fields: []metricFieldDefinition{
{
Name: "hours",
Address: uint16(1),
InputType: "UINT16",
RegisterType: "holding",
},
{
Name: "temperature",
Address: uint16(2),
InputType: "INT16",
RegisterType: "holding",
},
{
Name: "comment",
Address: uint16(11),
Length: 7,
InputType: "STRING",
RegisterType: "holding",
},
},
Tags: map[string]string{
"location": "main building",
"device": "machine A",
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
Measurement: "machine",
Fields: []metricFieldDefinition{
{
Name: "hours",
Address: uint16(3),
InputType: "UINT32",
Scale: 0.01,
},
{
Name: "temperature",
Address: uint16(5),
InputType: "INT32",
Scale: 0.02,
},
{
Name: "output",
Address: uint16(7),
InputType: "UINT32",
},
},
Tags: map[string]string{
"location": "main building",
"device": "machine B",
},
},
{
SlaveID: 1,
Fields: []metricFieldDefinition{
{
Name: "pi",
Address: uint16(9),
InputType: "FLOAT32",
},
},
},
{
SlaveID: 1,
Measurement: "bitvalues",
Fields: []metricFieldDefinition{
{
Name: "bit 0",
Address: uint16(1),
InputType: "BIT",
Bit: 0,
},
{
Name: "bit 1",
Address: uint16(1),
InputType: "BIT",
Bit: 1,
},
{
Name: "bit 2",
Address: uint16(1),
InputType: "BIT",
Bit: 2,
},
{
Name: "bit 3",
Address: uint16(1),
InputType: "BIT",
Bit: 3,
},
},
},
}
require.NoError(t, plugin.Init())
// Check the generated requests
require.Len(t, plugin.requests, 1)
require.NotNil(t, plugin.requests[1])
require.Len(t, plugin.requests[1].holding, 5)
require.Empty(t, plugin.requests[1].coil)
require.Empty(t, plugin.requests[1].discrete)
require.Empty(t, plugin.requests[1].input)
// Gather the data and verify the resulting metrics
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
expected := []telegraf.Metric{
metric.New(
"machine",
map[string]string{
"name": "FAKEMETER",
"location": "main building",
"device": "machine A",
"slave_id": "1",
"type": "holding_register",
},
map[string]interface{}{
"hours": uint64(10),
"temperature": int64(42),
"comment": "Modbus String",
},
time.Unix(0, 0),
),
metric.New(
"machine",
map[string]string{
"name": "FAKEMETER",
"location": "main building",
"device": "machine B",
"slave_id": "1",
"type": "holding_register",
},
map[string]interface{}{
"hours": float64(22.0),
"temperature": float64(44.02),
"output": uint64(2202),
},
time.Unix(0, 0),
),
metric.New(
"modbus",
map[string]string{
"name": "FAKEMETER",
"slave_id": "1",
"type": "holding_register",
},
map[string]interface{}{"pi": float64(3.1415927410125732421875)},
time.Unix(0, 0),
),
metric.New(
"bitvalues",
map[string]string{
"name": "FAKEMETER",
"slave_id": "1",
"type": "holding_register",
},
map[string]interface{}{
"bit 0": uint64(0),
"bit 1": uint64(1),
"bit 2": uint64(0),
"bit 3": uint64(1),
},
time.Unix(0, 0),
),
}
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
}
func TestMetricAddressOverflow(t *testing.T) {
logger := &testutil.CaptureLogger{}
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "metric",
Log: logger,
Workarounds: workarounds{ReadCoilsStartingAtZero: true},
}
plugin.Metrics = []metricDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
Measurement: "test",
Fields: []metricFieldDefinition{
{
Name: "field",
Address: uint16(65534),
InputType: "UINT64",
RegisterType: "holding",
},
},
},
}
require.ErrorIs(t, plugin.Init(), errAddressOverflow)
}

View file

@ -0,0 +1,348 @@
package modbus
import (
_ "embed"
"fmt"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
)
//go:embed sample_register.conf
var sampleConfigPartPerRegister string
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"`
Bit uint8 `toml:"bit"`
}
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"`
workarounds workarounds
logger telegraf.Logger
}
func (*configurationOriginal) sampleConfigPart() string {
return sampleConfigPartPerRegister
}
func (c *configurationOriginal) check() error {
switch c.workarounds.StringRegisterLocation {
case "", "both", "lower", "upper":
// Do nothing as those are valid
default:
return fmt.Errorf("invalid 'string_register_location' %q", c.workarounds.StringRegisterLocation)
}
if err := validateFieldDefinitions(c.DiscreteInputs, cDiscreteInputs); err != nil {
return err
}
if err := validateFieldDefinitions(c.Coils, cCoils); err != nil {
return err
}
if err := validateFieldDefinitions(c.HoldingRegisters, cHoldingRegisters); err != nil {
return err
}
return validateFieldDefinitions(c.InputRegisters, cInputRegisters)
}
func (c *configurationOriginal) process() (map[byte]requestSet, error) {
maxQuantity := uint16(1)
if !c.workarounds.OnRequestPerField {
maxQuantity = maxQuantityCoils
}
coil, err := c.initRequests(c.Coils, maxQuantity, false)
if err != nil {
return nil, err
}
if !c.workarounds.OnRequestPerField {
maxQuantity = maxQuantityDiscreteInput
}
discrete, err := c.initRequests(c.DiscreteInputs, maxQuantity, false)
if err != nil {
return nil, err
}
if !c.workarounds.OnRequestPerField {
maxQuantity = maxQuantityHoldingRegisters
}
holding, err := c.initRequests(c.HoldingRegisters, maxQuantity, true)
if err != nil {
return nil, err
}
if !c.workarounds.OnRequestPerField {
maxQuantity = maxQuantityInputRegisters
}
input, err := c.initRequests(c.InputRegisters, maxQuantity, true)
if err != nil {
return nil, err
}
return map[byte]requestSet{
c.SlaveID: {
coil: coil,
discrete: discrete,
holding: holding,
input: input,
},
}, nil
}
func (c *configurationOriginal) initRequests(fieldDefs []fieldDefinition, maxQuantity uint16, typed bool) ([]request, error) {
fields, err := c.initFields(fieldDefs, typed)
if err != nil {
return nil, err
}
params := groupingParams{
maxBatchSize: maxQuantity,
optimization: "none",
enforceFromZero: c.workarounds.ReadCoilsStartingAtZero,
log: c.logger,
}
return groupFieldsToRequests(fields, params), nil
}
func (c *configurationOriginal) initFields(fieldDefs []fieldDefinition, typed bool) ([]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)
if err != nil {
return nil, fmt.Errorf("initializing field %q failed: %w", def.Name, err)
}
fields = append(fields, f)
}
return fields, nil
}
func (c *configurationOriginal) newFieldFromDefinition(def fieldDefinition, typed bool) (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,
address: def.Address[0],
length: uint16(len(def.Address)),
}
// Handle coil and discrete registers which do have a limited datatype set
if !typed {
var err error
f.converter, err = determineUntypedConverter(def.DataType)
if err != nil {
return field{}, err
}
return f, nil
}
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, def.Bit, c.workarounds.StringRegisterLocation)
if err != nil {
return f, err
}
}
return f, nil
}
func validateFieldDefinitions(fieldDefs []fieldDefinition, registerType string) error {
nameEncountered := make(map[string]bool, len(fieldDefs))
for _, item := range fieldDefs {
// check empty name
if item.Name == "" {
return fmt.Errorf("empty name in %q", registerType)
}
// search name duplicate
canonicalName := item.Measurement + "." + item.Name
if nameEncountered[canonicalName] {
return fmt.Errorf("name %q is duplicated in measurement %q %q - %q", 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 %q in %q - %q", item.ByteOrder, registerType, item.Name)
}
// search data type
switch item.DataType {
case "INT8L", "INT8H", "UINT8L", "UINT8H",
"UINT16", "INT16", "UINT32", "INT32", "UINT64", "INT64",
"FLOAT16-IEEE", "FLOAT32-IEEE", "FLOAT64-IEEE", "FLOAT32", "FIXED", "UFIXED":
// Check scale
if item.Scale == 0.0 {
return fmt.Errorf("invalid scale '%f' in %q - %q", item.Scale, registerType, item.Name)
}
case "BIT", "STRING":
default:
return fmt.Errorf("invalid data type %q in %q - %q", item.DataType, registerType, item.Name)
}
} else {
// Bit-registers do have less data types
switch item.DataType {
case "", "UINT16", "BOOL":
default:
return fmt.Errorf("invalid data type %q in %q - %q", item.DataType, registerType, item.Name)
}
}
// Special address checking for special types
switch item.DataType {
case "STRING":
continue
case "BIT":
if len(item.Address) != 1 {
return fmt.Errorf("address '%v' has length '%v' bit should be one in %q - %q", item.Address, len(item.Address), registerType, item.Name)
}
continue
}
// Check address
if len(item.Address) != 1 && len(item.Address) != 2 && len(item.Address) != 4 {
return fmt.Errorf("invalid address '%v' length '%v' in %q - %q", 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 %q and address '%v' in %q - %q", item.ByteOrder, item.Address, registerType, item.Name)
}
// Check for the request size corresponding to the data-type
var requiredAddresses int
switch item.DataType {
case "INT8L", "INT8H", "UINT8L", "UINT8H", "UINT16", "INT16", "FLOAT16-IEEE":
requiredAddresses = 1
case "UINT32", "INT32", "FLOAT32-IEEE":
requiredAddresses = 2
case "UINT64", "INT64", "FLOAT64-IEEE":
requiredAddresses = 4
}
if requiredAddresses > 0 && len(item.Address) != requiredAddresses {
return fmt.Errorf(
"invalid address '%v' length '%v'in %q - %q, expecting %d entries for datatype",
item.Address, len(item.Address), registerType, item.Name, requiredAddresses,
)
}
// search duplicated
if len(item.Address) > len(removeDuplicates(item.Address)) {
return fmt.Errorf("duplicate address '%v' in %q - %q", item.Address, registerType, item.Name)
}
} else if len(item.Address) != 1 {
return fmt.Errorf("invalid address '%v' length '%v'in %q - %q", item.Address, len(item.Address), registerType, item.Name)
}
}
return nil
}
func (*configurationOriginal) normalizeInputDatatype(dataType string, words int) (string, error) {
if dataType == "FLOAT32" {
config.PrintOptionValueDeprecationNotice("input.modbus", "data_type", "FLOAT32", telegraf.DeprecationInfo{
Since: "1.16.0",
RemovalIn: "1.35.0",
Notice: "Use 'UFIXED' instead",
})
}
// 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 "FLOAT16-IEEE":
return "FLOAT16", nil
case "FLOAT32-IEEE":
return "FLOAT32", nil
case "FLOAT64-IEEE":
return "FLOAT64", nil
case "STRING":
return "STRING", nil
case "BIT":
return "BIT", nil
}
return normalizeInputDatatype(dataType)
}
func (*configurationOriginal) normalizeOutputDatatype(dataType string) (string, error) {
// Handle our special types
switch dataType {
case "FIXED", "FLOAT32", "UFIXED":
return "FLOAT64", nil
}
return normalizeOutputDatatype("native")
}
func (*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)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,439 @@
package modbus
import (
_ "embed"
"errors"
"fmt"
"hash/maphash"
"math"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
)
//go:embed sample_request.conf
var sampleConfigPartPerRequest string
type requestFieldDefinition struct {
Address uint16 `toml:"address"`
Name string `toml:"name"`
InputType string `toml:"type"`
Length uint16 `toml:"length"`
Scale float64 `toml:"scale"`
OutputType string `toml:"output"`
Measurement string `toml:"measurement"`
Omit bool `toml:"omit"`
Bit uint8 `toml:"bit"`
}
type requestDefinition struct {
SlaveID byte `toml:"slave_id"`
ByteOrder string `toml:"byte_order"`
RegisterType string `toml:"register"`
Measurement string `toml:"measurement"`
Optimization string `toml:"optimization"`
MaxExtraRegisters uint16 `toml:"optimization_max_register_fill"`
Fields []requestFieldDefinition `toml:"fields"`
Tags map[string]string `toml:"tags"`
}
type configurationPerRequest struct {
Requests []requestDefinition `toml:"request"`
workarounds workarounds
excludeRegisterType bool
logger telegraf.Logger
}
func (*configurationPerRequest) sampleConfigPart() string {
return sampleConfigPartPerRequest
}
func (c *configurationPerRequest) check() error {
switch c.workarounds.StringRegisterLocation {
case "", "both", "lower", "upper":
// Do nothing as those are valid
default:
return fmt.Errorf("invalid 'string_register_location' %q", c.workarounds.StringRegisterLocation)
}
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)
}
// Check for valid optimization
switch def.Optimization {
case "", "none", "shrink", "rearrange":
case "aggressive":
config.PrintOptionValueDeprecationNotice(
"inputs.modbus",
"optimization",
"aggressive",
telegraf.DeprecationInfo{
Since: "1.28.2",
RemovalIn: "1.30.0",
Notice: `use "max_insert" instead`,
},
)
case "max_insert":
switch def.RegisterType {
case "coil":
if def.MaxExtraRegisters <= 0 || def.MaxExtraRegisters > maxQuantityCoils {
return fmt.Errorf("optimization_max_register_fill has to be between 1 and %d", maxQuantityCoils)
}
case "discrete":
if def.MaxExtraRegisters <= 0 || def.MaxExtraRegisters > maxQuantityDiscreteInput {
return fmt.Errorf("optimization_max_register_fill has to be between 1 and %d", maxQuantityDiscreteInput)
}
case "holding":
if def.MaxExtraRegisters <= 0 || def.MaxExtraRegisters > maxQuantityHoldingRegisters {
return fmt.Errorf("optimization_max_register_fill has to be between 1 and %d", maxQuantityHoldingRegisters)
}
case "input":
if def.MaxExtraRegisters <= 0 || def.MaxExtraRegisters > maxQuantityInputRegisters {
return fmt.Errorf("optimization_max_register_fill has to be between 1 and %d", maxQuantityInputRegisters)
}
}
default:
return fmt.Errorf("unknown optimization %q", def.Optimization)
}
// Set the default for measurement if required
if def.Measurement == "" {
def.Measurement = "modbus"
}
// Reject any configuration without fields as it
// makes no sense to not define anything but a request.
if len(def.Fields) == 0 {
return errors.New("found request section without fields")
}
// 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 == "holding" || def.RegisterType == "input" {
switch f.InputType {
case "":
case "INT8L", "INT8H", "INT16", "INT32", "INT64",
"UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64",
"FLOAT16", "FLOAT32", "FLOAT64":
if f.Length != 0 {
return fmt.Errorf("length option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.Bit != 0 {
return fmt.Errorf("bit option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.OutputType == "STRING" {
return fmt.Errorf("cannot output field %q as string", f.Name)
}
case "STRING":
if f.Length < 1 {
return fmt.Errorf("missing length for string field %q", f.Name)
}
if f.Bit != 0 {
return fmt.Errorf("bit option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.Scale != 0.0 {
return fmt.Errorf("scale option cannot be used for string field %q", f.Name)
}
if f.OutputType != "" && f.OutputType != "STRING" {
return fmt.Errorf("invalid output type %q for string field %q", f.OutputType, f.Name)
}
case "BIT":
if f.Length != 0 {
return fmt.Errorf("length option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.OutputType == "STRING" {
return fmt.Errorf("cannot output field %q as string", f.Name)
}
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 output type
if def.RegisterType == "holding" || def.RegisterType == "input" {
switch f.OutputType {
case "", "INT64", "UINT64", "FLOAT64", "STRING":
default:
return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name)
}
} else {
// Bit register types can only be UINT64 or BOOL
switch f.OutputType {
case "", "UINT16", "BOOL":
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 := c.fieldID(seed, def, f)
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 := make(map[byte]requestSet, len(c.Requests))
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{}
}
params := groupingParams{
maxExtraRegisters: def.MaxExtraRegisters,
optimization: def.Optimization,
tags: def.Tags,
log: c.logger,
}
switch def.RegisterType {
case "coil":
params.maxBatchSize = maxQuantityCoils
if c.workarounds.OnRequestPerField {
params.maxBatchSize = 1
}
params.enforceFromZero = c.workarounds.ReadCoilsStartingAtZero
requests := groupFieldsToRequests(fields, params)
set.coil = append(set.coil, requests...)
case "discrete":
params.maxBatchSize = maxQuantityDiscreteInput
if c.workarounds.OnRequestPerField {
params.maxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.discrete = append(set.discrete, requests...)
case "holding":
params.maxBatchSize = maxQuantityHoldingRegisters
if c.workarounds.OnRequestPerField {
params.maxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.holding = append(set.holding, requests...)
case "input":
params.maxBatchSize = maxQuantityInputRegisters
if c.workarounds.OnRequestPerField {
params.maxBatchSize = 1
}
requests := groupFieldsToRequests(fields, params)
set.input = append(set.input, requests...)
default:
return nil, fmt.Errorf("unknown register type %q", def.RegisterType)
}
if !set.empty() {
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: %w", 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 = determineFieldLength(def.InputType, def.Length); err != nil {
return field{}, err
}
}
// Check for address overflow
if def.Address > math.MaxUint16-fieldLength {
return field{}, fmt.Errorf("%w for field %q", errAddressOverflow, def.Name)
}
// Initialize the field
f := field{
measurement: def.Measurement,
name: def.Name,
address: def.Address,
length: fieldLength,
omit: def.Omit,
}
// Handle type conversions for coil and discrete registers
if !typed {
f.converter, err = determineUntypedConverter(def.OutputType)
if err != nil {
return field{}, err
}
}
// 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 = determineOutputDatatype(def.InputType); err != nil {
return field{}, err
}
} else {
// For scaling cases we always want FLOAT64 by default except for
// string fields
if def.InputType != "STRING" {
def.OutputType = "FLOAT64"
} else {
def.OutputType = "STRING"
}
}
}
// 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, def.Bit, c.workarounds.StringRegisterLocation)
if err != nil {
return field{}, err
}
return f, nil
}
func (c *configurationPerRequest) fieldID(seed maphash.Seed, def requestDefinition, field requestFieldDefinition) uint64 {
var mh maphash.Hash
mh.SetSeed(seed)
mh.WriteByte(def.SlaveID)
mh.WriteByte(0)
if !c.excludeRegisterType {
mh.WriteString(def.RegisterType)
mh.WriteByte(0)
}
mh.WriteString(field.Measurement)
mh.WriteByte(0)
mh.WriteString(field.Name)
mh.WriteByte(0)
// tags
for k, v := range def.Tags {
mh.WriteString(k)
mh.WriteByte('=')
mh.WriteString(v)
mh.WriteByte(':')
}
mh.WriteByte(0)
return mh.Sum64()
}
func determineOutputDatatype(input string) (string, error) {
// Handle our special types
switch input {
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
return "INT64", nil
case "BIT", "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
return "UINT64", nil
case "FLOAT16", "FLOAT32", "FLOAT64":
return "FLOAT64", nil
case "STRING":
return "STRING", nil
}
return "unknown", fmt.Errorf("invalid input datatype %q for determining output", input)
}
func determineFieldLength(input string, length uint16) (uint16, error) {
// Handle our special types
switch input {
case "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H":
return 1, nil
case "INT16", "UINT16", "FLOAT16":
return 1, nil
case "INT32", "UINT32", "FLOAT32":
return 2, nil
case "INT64", "UINT64", "FLOAT64":
return 4, nil
case "STRING":
return length, nil
}
return 0, fmt.Errorf("invalid input datatype %q for determining field length", input)
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,566 @@
//go:generate ../../../tools/readme_config_includer/generator
package modbus
import (
_ "embed"
"errors"
"fmt"
"net"
"net/url"
"path/filepath"
"strconv"
"time"
mb "github.com/grid-x/modbus"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/plugins/inputs"
)
//go:embed sample_general_begin.conf
var sampleConfigStart string
//go:embed sample_general_end.conf
var sampleConfigEnd string
var errAddressOverflow = errors.New("address overflow")
const (
cDiscreteInputs = "discrete_input"
cCoils = "coil"
cHoldingRegisters = "holding_register"
cInputRegisters = "input_register"
)
type Modbus struct {
Name string `toml:"name"`
Controller string `toml:"controller"`
TransmissionMode string `toml:"transmission_mode"`
BaudRate int `toml:"baud_rate"`
DataBits int `toml:"data_bits"`
Parity string `toml:"parity"`
StopBits int `toml:"stop_bits"`
RS485 *rs485Config `toml:"rs485"`
Timeout config.Duration `toml:"timeout"`
Retries int `toml:"busy_retries"`
RetriesWaitTime config.Duration `toml:"busy_retries_wait"`
DebugConnection bool `toml:"debug_connection" deprecated:"1.35.0;use 'log_level' 'trace' instead"`
Workarounds workarounds `toml:"workarounds"`
ConfigurationType string `toml:"configuration_type"`
ExcludeRegisterTypeTag bool `toml:"exclude_register_type_tag"`
Log telegraf.Logger `toml:"-"`
// configuration type specific settings
configurationOriginal
configurationPerRequest
configurationPerMetric
// Connection handling
client mb.Client
handler mb.ClientHandler
isConnected bool
// Request handling
requests map[byte]requestSet
}
type workarounds struct {
AfterConnectPause config.Duration `toml:"pause_after_connect"`
PollPause config.Duration `toml:"pause_between_requests"`
CloseAfterGather bool `toml:"close_connection_after_gather"`
OnRequestPerField bool `toml:"one_request_per_field"`
ReadCoilsStartingAtZero bool `toml:"read_coils_starting_at_zero"`
StringRegisterLocation string `toml:"string_register_location"`
}
// According to github.com/grid-x/serial
type rs485Config struct {
DelayRtsBeforeSend config.Duration `toml:"delay_rts_before_send"`
DelayRtsAfterSend config.Duration `toml:"delay_rts_after_send"`
RtsHighDuringSend bool `toml:"rts_high_during_send"`
RtsHighAfterSend bool `toml:"rts_high_after_send"`
RxDuringTx bool `toml:"rx_during_tx"`
}
type fieldConverterFunc func(bytes []byte) interface{}
type requestSet struct {
coil []request
discrete []request
holding []request
input []request
}
func (r requestSet) empty() bool {
l := len(r.coil)
l += len(r.discrete)
l += len(r.holding)
l += len(r.input)
return l == 0
}
type field struct {
measurement string
name string
address uint16
length uint16
omit bool
converter fieldConverterFunc
value interface{}
tags map[string]string
}
func (m *Modbus) SampleConfig() string {
configs := []configuration{
&m.configurationOriginal,
&m.configurationPerRequest,
&m.configurationPerMetric,
}
totalConfig := sampleConfigStart
for _, c := range configs {
totalConfig += c.sampleConfigPart() + "\n"
}
totalConfig += "\n"
totalConfig += sampleConfigEnd
return totalConfig
}
func (m *Modbus) Init() error {
// check device name
if m.Name == "" {
return errors.New("device name is empty")
}
if m.Retries < 0 {
return fmt.Errorf("retries cannot be negative in device %q", m.Name)
}
// Determine the configuration style
var cfg configuration
switch m.ConfigurationType {
case "", "register":
m.configurationOriginal.workarounds = m.Workarounds
m.configurationOriginal.logger = m.Log
cfg = &m.configurationOriginal
case "request":
m.configurationPerRequest.workarounds = m.Workarounds
m.configurationPerRequest.excludeRegisterType = m.ExcludeRegisterTypeTag
m.configurationPerRequest.logger = m.Log
cfg = &m.configurationPerRequest
case "metric":
m.configurationPerMetric.workarounds = m.Workarounds
m.configurationPerMetric.excludeRegisterType = m.ExcludeRegisterTypeTag
m.configurationPerMetric.logger = m.Log
cfg = &m.configurationPerMetric
default:
return fmt.Errorf("unknown configuration type %q in device %q", m.ConfigurationType, m.Name)
}
// Check and process the configuration
if err := cfg.check(); err != nil {
return fmt.Errorf("configuration invalid for device %q: %w", m.Name, err)
}
r, err := cfg.process()
if err != nil {
return fmt.Errorf("cannot process configuration for device %q: %w", m.Name, err)
}
m.requests = r
// Setup client
if err := m.initClient(); err != nil {
return fmt.Errorf("initializing client failed for controller %q: %w", m.Controller, err)
}
for slaveID, rqs := range m.requests {
var nHoldingRegs, nInputsRegs, nDiscreteRegs, nCoilRegs uint16
var nHoldingFields, nInputsFields, nDiscreteFields, nCoilFields int
for _, r := range rqs.holding {
nHoldingRegs += r.length
nHoldingFields += len(r.fields)
}
for _, r := range rqs.input {
nInputsRegs += r.length
nInputsFields += len(r.fields)
}
for _, r := range rqs.discrete {
nDiscreteRegs += r.length
nDiscreteFields += len(r.fields)
}
for _, r := range rqs.coil {
nCoilRegs += r.length
nCoilFields += len(r.fields)
}
m.Log.Infof("Got %d request(s) touching %d holding registers for %d fields (slave %d) on device %q",
len(rqs.holding), nHoldingRegs, nHoldingFields, slaveID, m.Name)
for i, r := range rqs.holding {
m.Log.Debugf(" #%d: @%d with length %d", i+1, r.address, r.length)
}
m.Log.Infof("Got %d request(s) touching %d inputs registers for %d fields (slave %d) on device %q",
len(rqs.input), nInputsRegs, nInputsFields, slaveID, m.Name)
for i, r := range rqs.input {
m.Log.Debugf(" #%d: @%d with length %d", i+1, r.address, r.length)
}
m.Log.Infof("Got %d request(s) touching %d discrete registers for %d fields (slave %d) on device %q",
len(rqs.discrete), nDiscreteRegs, nDiscreteFields, slaveID, m.Name)
for i, r := range rqs.discrete {
m.Log.Debugf(" #%d: @%d with length %d", i+1, r.address, r.length)
}
m.Log.Infof("Got %d request(s) touching %d coil registers for %d fields (slave %d) on device %q",
len(rqs.coil), nCoilRegs, nCoilFields, slaveID, m.Name)
for i, r := range rqs.coil {
m.Log.Debugf(" #%d: @%d with length %d", i+1, r.address, r.length)
}
}
return nil
}
func (m *Modbus) Gather(acc telegraf.Accumulator) error {
if !m.isConnected {
if err := m.connect(); err != nil {
return err
}
}
for slaveID, requests := range m.requests {
m.Log.Debugf("Reading slave %d for %s...", slaveID, m.Controller)
if err := m.readSlaveData(slaveID, requests); err != nil {
acc.AddError(fmt.Errorf("slave %d on controller %q: %w", slaveID, m.Controller, err))
var mbErr *mb.Error
if !errors.As(err, &mbErr) || mbErr.ExceptionCode != mb.ExceptionCodeServerDeviceBusy {
m.Log.Debugf("Reconnecting to %s...", m.Controller)
if err := m.disconnect(); err != nil {
return fmt.Errorf("disconnecting failed for controller %q: %w", m.Controller, err)
}
if err := m.connect(); err != nil {
return fmt.Errorf("slave %d on controller %q: connecting failed: %w", slaveID, m.Controller, err)
}
}
continue
}
timestamp := time.Now()
grouper := metric.NewSeriesGrouper()
tags := map[string]string{
"name": m.Name,
"slave_id": strconv.Itoa(int(slaveID)),
}
if !m.ExcludeRegisterTypeTag {
tags["type"] = cCoils
}
collectFields(grouper, timestamp, tags, requests.coil)
if !m.ExcludeRegisterTypeTag {
tags["type"] = cDiscreteInputs
}
collectFields(grouper, timestamp, tags, requests.discrete)
if !m.ExcludeRegisterTypeTag {
tags["type"] = cHoldingRegisters
}
collectFields(grouper, timestamp, tags, requests.holding)
if !m.ExcludeRegisterTypeTag {
tags["type"] = cInputRegisters
}
collectFields(grouper, timestamp, tags, requests.input)
// Add the metrics grouped by series to the accumulator
for _, x := range grouper.Metrics() {
acc.AddMetric(x)
}
}
// Disconnect after read if configured
if m.Workarounds.CloseAfterGather {
return m.disconnect()
}
return nil
}
func (m *Modbus) initClient() error {
u, err := url.Parse(m.Controller)
if err != nil {
return err
}
var tracelog mb.Logger
if m.Log.Level().Includes(telegraf.Trace) || m.DebugConnection { // for backward compatibility
tracelog = m
}
switch u.Scheme {
case "tcp":
host, port, err := net.SplitHostPort(u.Host)
if err != nil {
return err
}
switch m.TransmissionMode {
case "", "auto", "TCP":
handler := mb.NewTCPClientHandler(host + ":" + port)
handler.Timeout = time.Duration(m.Timeout)
handler.Logger = tracelog
m.handler = handler
case "RTUoverTCP":
handler := mb.NewRTUOverTCPClientHandler(host + ":" + port)
handler.Timeout = time.Duration(m.Timeout)
handler.Logger = tracelog
m.handler = handler
case "ASCIIoverTCP":
handler := mb.NewASCIIOverTCPClientHandler(host + ":" + port)
handler.Timeout = time.Duration(m.Timeout)
handler.Logger = tracelog
m.handler = handler
default:
return fmt.Errorf("invalid transmission mode %q for %q on device %q", m.TransmissionMode, u.Scheme, m.Name)
}
case "", "file":
path := filepath.Join(u.Host, u.Path)
if path == "" {
return fmt.Errorf("invalid path for controller %q", m.Controller)
}
switch m.TransmissionMode {
case "", "auto", "RTU":
handler := mb.NewRTUClientHandler(path)
handler.Timeout = time.Duration(m.Timeout)
handler.BaudRate = m.BaudRate
handler.DataBits = m.DataBits
handler.Parity = m.Parity
handler.StopBits = m.StopBits
handler.Logger = tracelog
if m.RS485 != nil {
handler.RS485.Enabled = true
handler.RS485.DelayRtsBeforeSend = time.Duration(m.RS485.DelayRtsBeforeSend)
handler.RS485.DelayRtsAfterSend = time.Duration(m.RS485.DelayRtsAfterSend)
handler.RS485.RtsHighDuringSend = m.RS485.RtsHighDuringSend
handler.RS485.RtsHighAfterSend = m.RS485.RtsHighAfterSend
handler.RS485.RxDuringTx = m.RS485.RxDuringTx
}
m.handler = handler
case "ASCII":
handler := mb.NewASCIIClientHandler(path)
handler.Timeout = time.Duration(m.Timeout)
handler.BaudRate = m.BaudRate
handler.DataBits = m.DataBits
handler.Parity = m.Parity
handler.StopBits = m.StopBits
handler.Logger = tracelog
if m.RS485 != nil {
handler.RS485.Enabled = true
handler.RS485.DelayRtsBeforeSend = time.Duration(m.RS485.DelayRtsBeforeSend)
handler.RS485.DelayRtsAfterSend = time.Duration(m.RS485.DelayRtsAfterSend)
handler.RS485.RtsHighDuringSend = m.RS485.RtsHighDuringSend
handler.RS485.RtsHighAfterSend = m.RS485.RtsHighAfterSend
handler.RS485.RxDuringTx = m.RS485.RxDuringTx
}
m.handler = handler
default:
return fmt.Errorf("invalid transmission mode %q for %q on device %q", m.TransmissionMode, u.Scheme, m.Name)
}
default:
return fmt.Errorf("invalid controller %q", m.Controller)
}
m.client = mb.NewClient(m.handler)
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
if m.isConnected && m.Workarounds.AfterConnectPause != 0 {
nextRequest := time.Now().Add(time.Duration(m.Workarounds.AfterConnectPause))
time.Sleep(time.Until(nextRequest))
}
return err
}
func (m *Modbus) disconnect() error {
err := m.handler.Close()
m.isConnected = false
return err
}
func (m *Modbus) readSlaveData(slaveID byte, requests requestSet) error {
m.handler.SetSlave(slaveID)
for retry := 0; retry < m.Retries; retry++ {
err := m.gatherFields(requests)
if err == nil {
// Reading was successful
return nil
}
// Exit in case a non-recoverable error occurred
var mbErr *mb.Error
if !errors.As(err, &mbErr) || mbErr.ExceptionCode != mb.ExceptionCodeServerDeviceBusy {
return err
}
// Wait some time and try again reading the slave.
m.Log.Infof("Device busy! Retrying %d more time(s) on controller %q...", m.Retries-retry, m.Controller)
time.Sleep(time.Duration(m.RetriesWaitTime))
}
return m.gatherFields(requests)
}
func (m *Modbus) gatherFields(requests requestSet) error {
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
}
return m.gatherRequestsInput(requests.input)
}
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
}
nextRequest := time.Now().Add(time.Duration(m.Workarounds.PollPause))
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
v := (bytes[idx] >> bit) & 0x01
request.fields[i].value = field.converter([]byte{v})
m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, v, request.fields[i].value)
}
// Some (serial) devices require a pause between requests...
time.Sleep(time.Until(nextRequest))
}
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
}
nextRequest := time.Now().Add(time.Duration(m.Workarounds.PollPause))
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
v := (bytes[idx] >> bit) & 0x01
request.fields[i].value = field.converter([]byte{v})
m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, v, request.fields[i].value)
}
// Some (serial) devices require a pause between requests...
time.Sleep(time.Until(nextRequest))
}
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
}
nextRequest := time.Now().Add(time.Duration(m.Workarounds.PollPause))
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 * uint32(field.address-request.address) // registers are 16bit = 2 byte
length := 2 * uint32(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)
}
// Some (serial) devices require a pause between requests...
time.Sleep(time.Until(nextRequest))
}
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
}
nextRequest := time.Now().Add(time.Duration(m.Workarounds.PollPause))
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 * uint32(field.address-request.address) // registers are 16bit = 2 byte
length := 2 * uint32(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)
}
// Some (serial) devices require a pause between requests...
time.Sleep(time.Until(nextRequest))
}
return nil
}
func collectFields(grouper *metric.SeriesGrouper, timestamp time.Time, tags map[string]string, requests []request) {
for _, request := range requests {
for _, field := range request.fields {
// Collect tags from global and per-request
ftags := make(map[string]string, len(tags)+len(field.tags))
for k, v := range tags {
ftags[k] = v
}
for k, v := range field.tags {
ftags[k] = v
}
// In case no measurement was specified we use "modbus" as default
measurement := "modbus"
if field.measurement != "" {
measurement = field.measurement
}
// Group the data by series
grouper.Add(measurement, ftags, timestamp, field.name, field.value)
}
}
}
// Printf implements the logger interface of the modbus client
func (m *Modbus) Printf(format string, v ...interface{}) {
m.Log.Tracef(format, v...)
}
// Add this plugin to telegraf
func init() {
inputs.Add("modbus", func() telegraf.Input { return &Modbus{} })
}

View file

@ -0,0 +1,744 @@
package modbus
import (
"encoding/binary"
"fmt"
"os"
"path/filepath"
"strconv"
"testing"
"time"
"github.com/google/go-cmp/cmp"
mb "github.com/grid-x/modbus"
"github.com/stretchr/testify/require"
"github.com/tbrandon/mbserver"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric"
"github.com/influxdata/telegraf/plugins/inputs"
"github.com/influxdata/telegraf/plugins/parsers/influx"
"github.com/influxdata/telegraf/testutil"
)
func TestControllers(t *testing.T) {
var tests = []struct {
name string
controller string
mode string
errmsg string
}{
{
name: "TCP host",
controller: "tcp://localhost:502",
},
{
name: "TCP mode auto",
controller: "tcp://localhost:502",
mode: "auto",
},
{
name: "TCP mode TCP",
controller: "tcp://localhost:502",
mode: "TCP",
},
{
name: "TCP mode RTUoverTCP",
controller: "tcp://localhost:502",
mode: "RTUoverTCP",
},
{
name: "TCP mode ASCIIoverTCP",
controller: "tcp://localhost:502",
mode: "ASCIIoverTCP",
},
{
name: "TCP invalid host",
controller: "tcp://localhost",
errmsg: "address localhost: missing port in address",
},
{
name: "TCP invalid mode RTU",
controller: "tcp://localhost:502",
mode: "RTU",
errmsg: "invalid transmission mode",
},
{
name: "TCP invalid mode ASCII",
controller: "tcp://localhost:502",
mode: "ASCII",
errmsg: "invalid transmission mode",
},
{
name: "absolute file path",
controller: "file:///dev/ttyUSB0",
},
{
name: "relative file path",
controller: "file://dev/ttyUSB0",
},
{
name: "relative file path with dot",
controller: "file://./dev/ttyUSB0",
},
{
name: "Windows COM-port",
controller: "COM2",
},
{
name: "Windows COM-port file path",
controller: "file://com2",
},
{
name: "serial mode auto",
controller: "file:///dev/ttyUSB0",
mode: "auto",
},
{
name: "serial mode RTU",
controller: "file:///dev/ttyUSB0",
mode: "RTU",
},
{
name: "serial mode ASCII",
controller: "file:///dev/ttyUSB0",
mode: "ASCII",
},
{
name: "empty file path",
controller: "file://",
errmsg: "invalid path for controller",
},
{
name: "empty controller",
controller: "",
errmsg: "invalid path for controller",
},
{
name: "invalid scheme",
controller: "foo://bar",
errmsg: "invalid controller",
},
{
name: "serial invalid mode TCP",
controller: "file:///dev/ttyUSB0",
mode: "TCP",
errmsg: "invalid transmission mode",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := Modbus{
Name: "dummy",
Controller: tt.controller,
TransmissionMode: tt.mode,
Log: testutil.Logger{Quiet: true},
}
err := plugin.Init()
if tt.errmsg != "" {
require.ErrorContains(t, err, tt.errmsg)
} else {
require.NoError(t, err)
}
})
}
}
func TestRetrySuccessful(t *testing.T) {
retries := 0
maxretries := 2
value := 1
serv := mbserver.NewServer()
require.NoError(t, serv.ListenTCP("localhost:1502"))
defer serv.Close()
// Make read on coil-registers fail for some trials by making the device to appear busy
serv.RegisterFunctionHandler(1,
func(*mbserver.Server, mbserver.Framer) ([]byte, *mbserver.Exception) {
data := make([]byte, 2)
data[0] = byte(1)
data[1] = byte(value)
except := &mbserver.SlaveDeviceBusy
if retries >= maxretries {
except = &mbserver.Success
}
retries++
return data, except
})
modbus := Modbus{
Name: "TestRetry",
Controller: "tcp://localhost:1502",
Retries: maxretries,
Log: testutil.Logger{Quiet: true},
}
modbus.SlaveID = 1
modbus.Coils = []fieldDefinition{
{
Name: "retry_success",
Address: []uint16{0},
},
}
expected := []telegraf.Metric{
testutil.MustMetric(
"modbus",
map[string]string{
"type": cCoils,
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
"name": modbus.Name,
},
map[string]interface{}{"retry_success": uint16(value)},
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 TestRetryFailExhausted(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 busy
serv.RegisterFunctionHandler(1,
func(*mbserver.Server, mbserver.Framer) ([]byte, *mbserver.Exception) {
data := make([]byte, 2)
data[0] = byte(1)
data[1] = byte(0)
return data, &mbserver.SlaveDeviceBusy
})
modbus := Modbus{
Name: "TestRetryFailExhausted",
Controller: "tcp://localhost:1502",
Retries: maxretries,
Log: testutil.Logger{Quiet: true},
}
modbus.SlaveID = 1
modbus.Coils = []fieldDefinition{
{
Name: "retry_fail",
Address: []uint16{0},
},
}
var acc testutil.Accumulator
require.NoError(t, modbus.Init())
require.NotEmpty(t, modbus.requests)
require.NoError(t, modbus.Gather(&acc))
require.Len(t, acc.Errors, 1)
require.ErrorContains(t, acc.FirstError(), `slave 1 on controller "tcp://localhost:1502": modbus: exception '6' (server device busy)`)
}
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
counter := 0
serv.RegisterFunctionHandler(1,
func(*mbserver.Server, mbserver.Framer) ([]byte, *mbserver.Exception) {
counter++
data := make([]byte, 2)
data[0] = byte(1)
data[1] = byte(0)
return data, &mbserver.IllegalFunction
},
)
modbus := Modbus{
Name: "TestRetryFailExhausted",
Controller: "tcp://localhost:1502",
Retries: maxretries,
Log: testutil.Logger{Quiet: true},
}
modbus.SlaveID = 1
modbus.Coils = []fieldDefinition{
{
Name: "retry_fail",
Address: []uint16{0},
},
}
var acc testutil.Accumulator
require.NoError(t, modbus.Init())
require.NotEmpty(t, modbus.requests)
require.NoError(t, modbus.Gather(&acc))
require.Len(t, acc.Errors, 1)
require.ErrorContains(t, acc.FirstError(), `slave 1 on controller "tcp://localhost:1502": modbus: exception '1' (illegal function)`)
require.Equal(t, 1, counter)
}
func TestCases(t *testing.T) {
// Get all directories in testdata
folders, err := os.ReadDir("testcases")
require.NoError(t, err)
// Prepare the influx parser for expectations
parser := &influx.Parser{}
require.NoError(t, parser.Init())
// Compare options
options := []cmp.Option{
testutil.IgnoreTime(),
testutil.SortMetrics(),
}
// Register the plugin
inputs.Add("modbus", func() telegraf.Input { return &Modbus{} })
// Define a function to return the register value as data
readFunc := func(_ *mbserver.Server, frame mbserver.Framer) ([]byte, *mbserver.Exception) {
data := frame.GetData()
register := binary.BigEndian.Uint16(data[0:2])
numRegs := binary.BigEndian.Uint16(data[2:4])
// Add the length in bytes and the register to the returned data
buf := make([]byte, 2*numRegs+1)
buf[0] = byte(2 * numRegs)
switch numRegs {
case 1: // 16-bit
binary.BigEndian.PutUint16(buf[1:], register)
case 2: // 32-bit
binary.BigEndian.PutUint32(buf[1:], uint32(register))
case 4: // 64-bit
binary.BigEndian.PutUint64(buf[1:], uint64(register))
}
return buf, &mbserver.Success
}
// Setup a Modbus server to test against
serv := mbserver.NewServer()
serv.RegisterFunctionHandler(mb.FuncCodeReadInputRegisters, readFunc)
serv.RegisterFunctionHandler(mb.FuncCodeReadHoldingRegisters, readFunc)
require.NoError(t, serv.ListenTCP("localhost:1502"))
defer serv.Close()
// Run the test cases
for _, f := range folders {
// Only handle folders
if !f.IsDir() {
continue
}
testcasePath := filepath.Join("testcases", f.Name())
configFilename := filepath.Join(testcasePath, "telegraf.conf")
expectedOutputFilename := filepath.Join(testcasePath, "expected.out")
expectedErrorFilename := filepath.Join(testcasePath, "expected.err")
initErrorFilename := filepath.Join(testcasePath, "init.err")
t.Run(f.Name(), func(t *testing.T) {
// Read the expected error for the init call if any
var expectedInitError string
if _, err := os.Stat(initErrorFilename); err == nil {
e, err := testutil.ParseLinesFromFile(initErrorFilename)
require.NoError(t, err)
require.Len(t, e, 1)
expectedInitError = e[0]
}
// Read the expected output if any
var expected []telegraf.Metric
if _, err := os.Stat(expectedOutputFilename); err == nil {
var err error
expected, err = testutil.ParseMetricsFromFile(expectedOutputFilename, parser)
require.NoError(t, err)
}
// Read the expected error if any
var expectedErrors []string
if _, err := os.Stat(expectedErrorFilename); err == nil {
e, err := testutil.ParseLinesFromFile(expectedErrorFilename)
require.NoError(t, err)
require.NotEmpty(t, e)
expectedErrors = e
}
// Configure the plugin
cfg := config.NewConfig()
require.NoError(t, cfg.LoadConfig(configFilename))
require.Len(t, cfg.Inputs, 1)
// Extract the plugin and make sure it connects to our dummy
// server
plugin := cfg.Inputs[0].Input.(*Modbus)
plugin.Controller = "tcp://localhost:1502"
// Init the plugin.
err := plugin.Init()
if expectedInitError != "" {
require.ErrorContains(t, err, expectedInitError)
return
}
require.NoError(t, err)
// Gather data
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
if len(acc.Errors) > 0 {
var actualErrorMsgs []string
for _, err := range acc.Errors {
actualErrorMsgs = append(actualErrorMsgs, err.Error())
}
require.ElementsMatch(t, actualErrorMsgs, expectedErrors)
}
// Check the metric nevertheless as we might get some metrics despite errors.
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, options...)
})
}
}
type rangeDefinition struct {
start uint16
count uint16
increment uint16
length uint16
dtype string
omit bool
}
type requestExpectation struct {
fields []rangeDefinition
req request
}
func generateRequestDefinitions(ranges []rangeDefinition) []requestFieldDefinition {
var fields []requestFieldDefinition
id := 0
for _, r := range ranges {
if r.increment == 0 {
r.increment = r.length
}
for i := uint16(0); i < r.count; i++ {
f := requestFieldDefinition{
Name: fmt.Sprintf("holding-%d", id),
Address: r.start + i*r.increment,
InputType: r.dtype,
Omit: r.omit,
}
fields = append(fields, f)
id++
}
}
return fields
}
func generateExpectation(defs []requestExpectation) []request {
requests := make([]request, 0, len(defs))
for _, def := range defs {
r := def.req
r.fields = make([]field, 0)
for _, d := range def.fields {
if d.increment == 0 {
d.increment = d.length
}
for i := uint16(0); i < d.count; i++ {
f := field{
address: d.start + i*d.increment,
length: d.length,
}
r.fields = append(r.fields, f)
}
}
requests = append(requests, r)
}
return requests
}
func requireEqualRequests(t *testing.T, expected, actual []request) {
require.Len(t, actual, len(expected), "request size mismatch")
for i, e := range expected {
a := actual[i]
require.Equalf(t, e.address, a.address, "address mismatch in request %d", i)
require.Equalf(t, e.length, a.length, "length mismatch in request %d", i)
require.Lenf(t, a.fields, len(e.fields), "no. fields mismatch in request %d", i)
for j, ef := range e.fields {
af := a.fields[j]
require.Equalf(t, ef.address, af.address, "address mismatch in field %d of request %d", j, i)
require.Equalf(t, ef.length, af.length, "length mismatch in field %d of request %d", j, i)
}
}
}
func TestRegisterWorkaroundsOneRequestPerField(t *testing.T) {
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "register",
Log: testutil.Logger{Quiet: true},
Workarounds: workarounds{OnRequestPerField: true},
}
plugin.SlaveID = 1
plugin.HoldingRegisters = []fieldDefinition{
{
ByteOrder: "AB",
DataType: "INT16",
Name: "holding-1",
Address: []uint16{1},
Scale: 1.0,
},
{
ByteOrder: "AB",
DataType: "INT16",
Name: "holding-2",
Address: []uint16{2},
Scale: 1.0,
},
{
ByteOrder: "AB",
DataType: "INT16",
Name: "holding-3",
Address: []uint16{3},
Scale: 1.0,
},
{
ByteOrder: "AB",
DataType: "INT16",
Name: "holding-4",
Address: []uint16{4},
Scale: 1.0,
},
{
ByteOrder: "AB",
DataType: "INT16",
Name: "holding-5",
Address: []uint16{5},
Scale: 1.0,
},
}
require.NoError(t, plugin.Init())
require.Len(t, plugin.requests[1].holding, len(plugin.HoldingRegisters))
}
func TestRequestsWorkaroundsReadCoilsStartingAtZeroRegister(t *testing.T) {
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "register",
Log: testutil.Logger{Quiet: true},
Workarounds: workarounds{ReadCoilsStartingAtZero: true},
}
plugin.SlaveID = 1
plugin.Coils = []fieldDefinition{
{
Name: "coil-8",
Address: []uint16{8},
},
{
Name: "coil-new-group",
Address: []uint16{maxQuantityCoils},
},
}
require.NoError(t, plugin.Init())
require.Len(t, plugin.requests[1].coil, 2)
// First group should now start at zero and have the cumulated length
require.Equal(t, uint16(0), plugin.requests[1].coil[0].address)
require.Equal(t, uint16(9), plugin.requests[1].coil[0].length)
// The second field should form a new group as the previous request
// is now too large (beyond max-coils-per-read) after zero enforcement.
require.Equal(t, maxQuantityCoils, plugin.requests[1].coil[1].address)
require.Equal(t, uint16(1), plugin.requests[1].coil[1].length)
}
func TestWorkaroundsStringRegisterLocation(t *testing.T) {
tests := []struct {
name string
location string
order string
content []byte
expected string
}{
{
name: "default big-endian",
order: "ABCD",
content: []byte{
0x4d, 0x6f, 0x64, 0x62, 0x75, 0x73, 0x20, 0x53,
0x74, 0x72, 0x69, 0x6e, 0x67, 0x00,
},
expected: "Modbus String",
},
{
name: "default little-endian",
order: "DCBA",
content: []byte{
0x6f, 0x4d, 0x62, 0x64, 0x73, 0x75, 0x53, 0x20,
0x72, 0x74, 0x6e, 0x69, 0x00, 0x67,
},
expected: "Modbus String",
},
{
name: "both big-endian",
location: "both",
order: "ABCD",
content: []byte{
0x4d, 0x6f, 0x64, 0x62, 0x75, 0x73, 0x20, 0x53,
0x74, 0x72, 0x69, 0x6e, 0x67, 0x00,
},
expected: "Modbus String",
},
{
name: "both little-endian",
location: "both",
order: "DCBA",
content: []byte{
0x6f, 0x4d, 0x62, 0x64, 0x73, 0x75, 0x53, 0x20,
0x72, 0x74, 0x6e, 0x69, 0x00, 0x67,
},
expected: "Modbus String",
},
{
name: "lower big-endian",
location: "lower",
order: "ABCD",
content: []byte{
0x00, 0x4d, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x62,
0x00, 0x75, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53,
0x00, 0x74, 0x00, 0x72, 0x00, 0x69, 0x00, 0x6e,
0x00, 0x67, 0x00, 0x00,
},
expected: "Modbus String",
},
{
name: "lower little-endian",
location: "lower",
order: "DCBA",
content: []byte{
0x4d, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x62, 0x00,
0x75, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00,
0x74, 0x00, 0x72, 0x00, 0x69, 0x00, 0x6e, 0x00,
0x67, 0x00, 0x00, 0x00,
},
expected: "Modbus String",
},
{
name: "upper big-endian",
location: "upper",
order: "ABCD",
content: []byte{
0x4d, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x62, 0x00,
0x75, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53, 0x00,
0x74, 0x00, 0x72, 0x00, 0x69, 0x00, 0x6e, 0x00,
0x67, 0x00, 0x00, 0x00,
},
expected: "Modbus String",
},
{
name: "upper little-endian",
location: "upper",
order: "DCBA",
content: []byte{
0x00, 0x4d, 0x00, 0x6f, 0x00, 0x64, 0x00, 0x62,
0x00, 0x75, 0x00, 0x73, 0x00, 0x20, 0x00, 0x53,
0x00, 0x74, 0x00, 0x72, 0x00, 0x69, 0x00, 0x6e,
0x00, 0x67, 0x00, 0x00,
},
expected: "Modbus String",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
const addr = uint16(8)
length := uint16(len(tt.content) / 2)
expected := []telegraf.Metric{
metric.New(
"modbus",
map[string]string{
"name": "Test",
"slave_id": "1",
"type": "holding_register",
},
map[string]interface{}{
"value": tt.expected,
},
time.Unix(0, 0),
),
}
plugin := &Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{Quiet: true},
Workarounds: workarounds{StringRegisterLocation: tt.location},
configurationPerRequest: configurationPerRequest{
Requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: tt.order,
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Address: addr,
Name: "value",
InputType: "STRING",
Length: length,
},
},
},
},
},
}
require.NoError(t, plugin.Init())
// Create a mock server and fill in the data
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)
_, err := client.WriteMultipleRegisters(addr, length, tt.content)
require.NoError(t, err)
// Gather the data
var acc testutil.Accumulator
require.NoError(t, plugin.Gather(&acc))
// Compare
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
})
}
}
func TestWorkaroundsStringRegisterLocationInvalid(t *testing.T) {
plugin := &Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{Quiet: true},
Workarounds: workarounds{StringRegisterLocation: "foo"},
}
require.ErrorContains(t, plugin.Init(), `invalid 'string_register_location'`)
}

View file

@ -0,0 +1,305 @@
package modbus
import (
"math"
"sort"
"github.com/influxdata/telegraf"
)
type request struct {
address uint16
length uint16
fields []field
}
func countRegisters(requests []request) uint64 {
var l uint64
for _, r := range requests {
l += uint64(r.length)
}
return l
}
// Only split too-large groups, but ignore all optimization potential
func splitMaxBatchSize(g request, maxBatchSize uint16) []request {
var requests []request
idx := 0
for start := g.address; start < g.address+g.length; {
current := request{
address: start,
}
// Initialize the end to a safe value avoiding infinite loops
end := g.address + g.length
var batchEnd uint16
if start >= math.MaxUint16-maxBatchSize {
batchEnd = math.MaxUint16
} else {
batchEnd = start + maxBatchSize
}
for _, f := range g.fields[idx:] {
// If the current field exceeds the batch size we need to split
// the request here
if f.address+f.length > batchEnd {
break
}
// End of field still fits into the batch so add it to the request
current.fields = append(current.fields, f)
end = f.address + f.length
idx++
}
if end > g.address+g.length {
end = g.address + g.length
}
if idx >= len(g.fields) || g.fields[idx].address >= end {
current.length = end - start
} else {
current.length = g.fields[idx].address - start
}
start = end
if len(current.fields) > 0 {
requests = append(requests, current)
}
}
return requests
}
func shrinkGroup(g request, maxBatchSize uint16) []request {
var requests []request
var current request
for _, f := range g.fields {
// Just add the field and update length if we are still
// within the maximum batch-size
if current.length > 0 && f.address+f.length <= current.address+maxBatchSize {
current.fields = append(current.fields, f)
current.length = f.address - current.address + f.length
continue
}
// Ignore completely empty requests
if len(current.fields) > 0 {
requests = append(requests, current)
}
// Create a new request
current = request{
fields: []field{f},
address: f.address,
length: f.length,
}
}
if len(current.fields) > 0 {
requests = append(requests, current)
}
return requests
}
func optimizeGroup(g request, maxBatchSize uint16) []request {
if len(g.fields) == 0 {
return nil
}
requests := shrinkGroup(g, maxBatchSize)
length := countRegisters(requests)
for i := 1; i < len(g.fields)-1; i++ {
// Always keep consecutive fields as they are known to be optimal
if g.fields[i-1].address+g.fields[i-1].length == g.fields[i].address {
continue
}
// Perform the split and check if it is better
// Note: This involves recursive optimization of the right side of the split.
current := shrinkGroup(request{fields: g.fields[:i]}, maxBatchSize)
current = append(current, optimizeGroup(request{fields: g.fields[i:]}, maxBatchSize)...)
currentLength := countRegisters(current)
// Do not allow for more requests
if len(current) > len(requests) {
continue
}
// Try to reduce the number of registers we are trying to access
if currentLength >= length {
continue
}
// We found a better solution
requests = current
length = currentLength
}
return requests
}
func optimizeGroupWithinLimits(g request, params groupingParams) []request {
if len(g.fields) == 0 {
return nil
}
var requests []request
currentRequest := request{
fields: []field{g.fields[0]},
address: g.fields[0].address,
length: g.fields[0].length,
}
for i := 1; i <= len(g.fields)-1; i++ {
// Check if we need to interrupt the current chunk and require a new one
holeSize := g.fields[i].address - (g.fields[i-1].address + g.fields[i-1].length)
if g.fields[i].address < g.fields[i-1].address+g.fields[i-1].length {
params.log.Warnf(
"Request at %d with length %d overlaps with next request at %d",
g.fields[i-1].address, g.fields[i-1].length, g.fields[i].address,
)
holeSize = 0
}
needInterrupt := holeSize > params.maxExtraRegisters // too far apart
needInterrupt = needInterrupt || currentRequest.length+holeSize+g.fields[i].length > params.maxBatchSize // too large
if !needInterrupt {
// Still safe to add the field to the current request
currentRequest.length = g.fields[i].address + g.fields[i].length - currentRequest.address
currentRequest.fields = append(currentRequest.fields, g.fields[i])
continue
}
// Finish the current request, add it to the list and construct a new one
requests = append(requests, currentRequest)
currentRequest = request{
fields: []field{g.fields[i]},
address: g.fields[i].address,
length: g.fields[i].length,
}
}
requests = append(requests, currentRequest)
return requests
}
type groupingParams struct {
// Maximum size of a request in registers
maxBatchSize uint16
// optimization to use for grouping register groups to requests, Also put potential optimization parameters here
optimization string
maxExtraRegisters uint16
// Will force reads to start at zero (if possible) while respecting the max-batch size.
enforceFromZero bool
// tags to add for the requests
tags map[string]string
// log facility to inform the user
log telegraf.Logger
}
func groupFieldsToRequests(fields []field, params groupingParams) []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 groups []request
var current request
for _, f := range fields {
// Add tags from higher up
if f.tags == nil {
f.tags = make(map[string]string, len(params.tags))
}
for k, v := range params.tags {
f.tags[k] = v
}
// Check if we need to interrupt the current chunk and require a new one
if current.length > 0 && f.address == current.address+current.length {
// Still safe to add the field to the current request
current.length += f.length
if !f.omit {
current.fields = append(current.fields, f)
}
continue
}
// Finish the current request, add it to the list and construct a new one
if current.length > 0 && len(fields) > 0 {
groups = append(groups, current)
}
current = request{
address: f.address,
length: f.length,
}
if !f.omit {
current.fields = append(current.fields, f)
}
}
if current.length > 0 && len(fields) > 0 {
groups = append(groups, current)
}
if len(groups) == 0 {
return nil
}
// Enforce the first read to start at zero if the option is set
if params.enforceFromZero {
groups[0].length += groups[0].address
groups[0].address = 0
}
var requests []request
switch params.optimization {
case "shrink":
// Shrink request by striping leading and trailing fields with an omit flag set
for _, g := range groups {
if len(g.fields) > 0 {
requests = append(requests, shrinkGroup(g, params.maxBatchSize)...)
}
}
case "rearrange":
// Allow rearranging fields between request in order to reduce the number of touched
// registers while keeping the number of requests
for _, g := range groups {
if len(g.fields) > 0 {
requests = append(requests, optimizeGroup(g, params.maxBatchSize)...)
}
}
case "aggressive":
// Allow rearranging fields similar to "rearrange" but allow mixing of groups
// This might reduce the number of requests at the cost of more registers being touched.
var total request
for _, g := range groups {
if len(g.fields) > 0 {
total.fields = append(total.fields, g.fields...)
}
}
requests = optimizeGroup(total, params.maxBatchSize)
case "max_insert":
// Similar to aggressive but keeps the number of touched registers below a threshold
var total request
for _, g := range groups {
if len(g.fields) > 0 {
total.fields = append(total.fields, g.fields...)
}
}
requests = optimizeGroupWithinLimits(total, params)
default:
// no optimization
for _, g := range groups {
if len(g.fields) > 0 {
requests = append(requests, splitMaxBatchSize(g, params.maxBatchSize)...)
}
}
}
return requests
}

View file

@ -0,0 +1,57 @@
# Retrieve data from MODBUS slave devices
[[inputs.modbus]]
## Connection Configuration
##
## The plugin supports connections to PLCs via MODBUS/TCP, RTU over TCP, ASCII over TCP or
## via serial line communication in binary (RTU) or readable (ASCII) encoding
##
## Device name
name = "Device"
## Slave ID - addresses a MODBUS device on the bus
## Range: 0 - 255 [0 = broadcast; 248 - 255 = reserved]
slave_id = 1
## Timeout for each request
timeout = "1s"
## Maximum number of retries and the time to wait between retries
## when a slave-device is busy.
# busy_retries = 0
# busy_retries_wait = "100ms"
# TCP - connect via Modbus/TCP
controller = "tcp://localhost:502"
## Serial (RS485; RS232)
## For RS485 specific setting check the end of the configuration.
## For unix-like operating systems use:
# controller = "file:///dev/ttyUSB0"
## For Windows operating systems use:
# controller = "COM1"
# baud_rate = 9600
# data_bits = 8
# parity = "N"
# stop_bits = 1
## Transmission mode for Modbus packets depending on the controller type.
## For Modbus over TCP you can choose between "TCP" , "RTUoverTCP" and
## "ASCIIoverTCP".
## For Serial controllers you can choose between "RTU" and "ASCII".
## By default this is set to "auto" selecting "TCP" for ModbusTCP connections
## and "RTU" for serial connections.
# transmission_mode = "auto"
## Trace the connection to the modbus device
# log_level = "trace"
## 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
## |---metric -- define fields on a metric base
configuration_type = "register"
## Exclude the register type tag
## Please note, this will also influence the grouping of metrics as you won't
## see one metric per register type anymore!
# exclude_register_type_tag = false

View file

@ -0,0 +1,51 @@
## RS485 specific settings. Only take effect for serial controllers.
## Note: This has to be at the end of the modbus configuration due to
## TOML constraints.
# [inputs.modbus.rs485]
## Delay RTS prior to sending
# delay_rts_before_send = "0ms"
## Delay RTS after to sending
# delay_rts_after_send = "0ms"
## Pull RTS line to high during sending
# rts_high_during_send = false
## Pull RTS line to high after sending
# rts_high_after_send = false
## Enabling receiving (Rx) during transmission (Tx)
# rx_during_tx = false
## Enable workarounds required by some devices to work correctly
# [inputs.modbus.workarounds]
## Pause after connect delays the first request by the specified time.
## This might be necessary for (slow) devices.
# pause_after_connect = "0ms"
## Pause between read requests sent to the device.
## This might be necessary for (slow) serial devices.
# pause_between_requests = "0ms"
## Close the connection after every gather cycle.
## Usually the plugin closes the connection after a certain idle-timeout,
## however, if you query a device with limited simultaneous connectivity
## (e.g. serial devices) from multiple instances you might want to only
## stay connected during gather and disconnect afterwards.
# close_connection_after_gather = false
## Force the plugin to read each field in a separate request.
## This might be necessary for devices not conforming to the spec,
## see https://github.com/influxdata/telegraf/issues/12071.
# one_request_per_field = false
## Enforce the starting address to be zero for the first request on
## coil registers. This is necessary for some devices see
## https://github.com/influxdata/telegraf/issues/8905
# read_coils_starting_at_zero = false
## String byte-location in registers AFTER byte-order conversion
## Some device (e.g. EM340) place the string byte in only the upper or
## lower byte location of a register see
## https://github.com/influxdata/telegraf/issues/14748
## Available settings:
## lower -- use only lower byte of the register i.e. 00XX 00XX 00XX 00XX
## upper -- use only upper byte of the register i.e. XX00 XX00 XX00 XX00
## By default both bytes of the register are used i.e. XXXX XXXX.
# string_register_location = ""

View file

@ -0,0 +1,73 @@
## --- "metric" configuration style ---
## Per metric definition
##
## Request optimization algorithm across metrics
## |---none -- Do not perform any optimization and just group requests
## | within metrics (default)
## |---max_insert -- Collate registers across all defined metrics and fill in
## holes to optimize the number of requests.
# optimization = "none"
## Maximum number of registers the optimizer is allowed to insert between
## non-consecutive registers to save requests.
## This option is only used for the 'max_insert' optimization strategy and
## effectively denotes the hole size between registers to fill.
# optimization_max_register_fill = 50
## Define a metric produced by the requests to the device
## Multiple of those metrics can be defined. The referenced registers will
## be collated into requests send to the device
[[inputs.modbus.metric]]
## ID of the modbus slave device to query
## If you need to query multiple slave-devices, create several "metric" 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"
## Name of the measurement
# measurement = "modbus"
## Field definitions
## register - type of the modbus register, can be "coil", "discrete",
## "holding" or "input". Defaults to "holding".
## address - address of the register to query. For coil and discrete inputs this is the bit address.
## name - field name
## type *1 - type of the modbus field, can be
## BIT (single bit of a register)
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
## STRING (byte-sequence converted to string)
## length *1 - (optional) number of registers, ONLY valid for STRING type
## bit *1,2 - (optional) bit of the register, ONLY valid for BIT type
## scale *1,3 - (optional) factor to scale the variable with
## output *2,3 - (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).
##
## *1: These fields are ignored for both "coil" and "discrete"-input type of registers.
## *2: This field can only be "UINT16" or "BOOL" if specified for both "coil"
## and "discrete"-input type of registers. By default the fields are
## output as zero or one in UINT16 format unless "BOOL" is used.
## *3: These fields cannot be used with "STRING"-type fields.
fields = [
{ register="coil", address=0, name="door_open"},
{ register="coil", address=1, name="status_ok"},
{ register="holding", address=0, name="voltage", type="INT16" },
{ address=1, name="current", type="INT32", scale=0.001 },
{ address=5, name="energy", type="FLOAT32", scale=0.001 },
{ address=7, name="frequency", type="UINT32", scale=0.1 },
{ address=8, name="power_factor", type="INT64", scale=0.01 },
{ address=9, name="firmware", type="STRING", length=8 },
]
## Tags assigned to the metric
# [inputs.modbus.metric.tags]
# machine = "impresser"
# location = "main building"

View file

@ -0,0 +1,55 @@
## --- "register" configuration style ---
## Measurements
##
## Digital Variables, Discrete Inputs and Coils
## measurement - the (optional) measurement name, defaults to "modbus"
## name - the variable name
## data_type - the (optional) output type, can be BOOL or UINT16 (default)
## 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 - BIT (single bit of a register)
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64,
## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation)
## FIXED, UFIXED (fixed-point representation on input)
## STRING (byte-sequence converted to string)
## bit - (optional) bit of the register, ONLY valid for BIT type
## 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]},
{ name = "firmware", byte_order = "AB", data_type = "STRING", address = [5, 6, 7, 8, 9, 10, 11, 12]},
]
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]},
]

View file

@ -0,0 +1,120 @@
## --- "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 overridden by the individual field definitions. Defaults to "modbus"
# measurement = "modbus"
## Request optimization algorithm.
## |---none -- Do not perform any optimization and use the given layout(default)
## |---shrink -- Shrink requests to actually requested fields
## | by stripping leading and trailing omits
## |---rearrange -- Rearrange request boundaries within consecutive address ranges
## | to reduce the number of requested registers by keeping
## | the number of requests.
## |---max_insert -- Rearrange request keeping the number of extra fields below the value
## provided in "optimization_max_register_fill". It is not necessary to define 'omitted'
## fields as the optimisation will add such field only where needed.
# optimization = "none"
## Maximum number register the optimizer is allowed to insert between two fields to
## save requests.
## This option is only used for the 'max_insert' optimization strategy.
## NOTE: All omitted fields are ignored, so this option denotes the effective hole
## size to fill.
# optimization_max_register_fill = 50
## 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
## BIT (single bit of a register)
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
## STRING (byte-sequence converted to string)
## length *1,2 - (optional) number of registers, ONLY valid for STRING type
## bit *1,2 - (optional) bit of the register, ONLY valid for BIT type
## scale *1,2,4 - (optional) factor to scale the variable with
## output *1,3,4 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64.
## Defaults to FLOAT64 for numeric fields if "scale" is provided.
## Otherwise the input "type" class is used (e.g. INT* -> INT64).
## 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: These fields are ignored if field is omitted ("omit"=true)
## *2: These fields are ignored for both "coil" and "discrete"-input type of registers.
## *3: This field can only be "UINT16" or "BOOL" if specified for both "coil"
## and "discrete"-input type of registers. By default the fields are
## output as zero or one in UINT16 format unless "BOOL" is used.
## *4: These fields cannot be used with "STRING"-type fields.
## 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", output="BOOL" },
{ address=4, name="firmware", type="STRING", length=8 },
]
[inputs.modbus.request.tags]
machine = "impresser"
location = "main building"
[[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.tags]
machine = "impresser"
location = "main building"
[[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
]
[inputs.modbus.request.tags]
machine = "impresser"
location = "main building"

View file

@ -0,0 +1 @@
duplicated in measurement "modbus"

View file

@ -0,0 +1,24 @@
[[inputs.modbus]]
name = "Device"
controller = "tcp://localhost:502"
configuration_type = "request"
exclude_register_type_tag = true
[[inputs.modbus.request]]
slave_id = 1
register = "holding"
fields = [
{ name = "humidity", type = "INT16", scale=1.0, address = 1},
{ name = "temperature", type = "INT16", scale=1.0, address = 4},
{ name = "active", type = "INT16", scale=1.0, address = 7},
]
[[inputs.modbus.request]]
slave_id = 1
register = "input"
fields = [
{ name = "humidity", type = "INT16", scale=1.0, address = 2},
{ name = "temperature", type = "INT16", scale=1.0, address = 5},
{ name = "active", type = "INT16", scale=1.0, address = 8},
]

View file

@ -0,0 +1,2 @@
V,slave_id=1,element=EleMeter\ 1,name=Device,type=input_register Voltage=200i
V,slave_id=2,element=EleMeter\ 2,name=Device,type=input_register Voltage=200i

View file

@ -0,0 +1,28 @@
[[inputs.modbus]]
name = "Device"
timeout = "1s"
controller = "tcp://localhost:502"
configuration_type = "request"
[[inputs.modbus.request]]
slave_id = 1
byte_order = "ABCD"
register = "input"
fields = [
{address=200, name="Voltage", type="INT32", measurement="V", omit=false},
]
[inputs.modbus.request.tags]
element = "EleMeter 1"
[[inputs.modbus.request]]
slave_id = 2
byte_order = "ABCD"
register = "input"
fields = [
{address=200, name="Voltage", type="INT32", measurement="V", omit=false},
]
[inputs.modbus.request.tags]
element = "EleMeter 2"

View file

@ -0,0 +1,3 @@
modbus,slave_id=1,name=Device,type=holding_register,location=Zone\ 1 humidity=1,temperature=4,active=7
modbus,slave_id=1,name=Device,type=holding_register,location=Zone\ 2 humidity=2,temperature=5,active=8
modbus,slave_id=1,name=Device,type=holding_register,location=Zone\ 3 humidity=3,temperature=6,active=9

View file

@ -0,0 +1,40 @@
[[inputs.modbus]]
name = "Device"
controller = "tcp://localhost:502"
configuration_type = "request"
[[inputs.modbus.request]]
slave_id = 1
register = "holding"
fields = [
{ name = "humidity", type = "INT16", scale=1.0, address = 1},
{ name = "temperature", type = "INT16", scale=1.0, address = 4},
{ name = "active", type = "INT16", scale=1.0, address = 7},
]
[inputs.modbus.request.tags]
location = 'Zone 1'
[[inputs.modbus.request]]
slave_id = 1
register = "holding"
fields = [
{ name = "humidity", type = "INT16", scale=1.0, address = 2},
{ name = "temperature", type = "INT16", scale=1.0, address = 5},
{ name = "active", type = "INT16", scale=1.0, address = 8},
]
[inputs.modbus.request.tags]
location = 'Zone 2'
[[inputs.modbus.request]]
slave_id = 1
register = "holding"
fields = [
{ name = "humidity", type = "INT16", scale=1.0, address = 3},
{ name = "temperature", type = "INT16", scale=1.0, address = 6},
{ name = "active", type = "INT16", scale=1.0, address = 9},
]
[inputs.modbus.request.tags]
location = 'Zone 3'

View file

@ -0,0 +1,7 @@
Bitfield,name=EAP,resource=30KTL,slave_id=9,type=holding_register Alarm1=0u,Alarm2=0u,Alarm3=0u,State1=32000u,State2=0u,State3=0u
Current,name=EAP,resource=30KTL,slave_id=9,type=holding_register PV1=0,PV2=0,PV3=0,PV4=0
Power,name=EAP,resource=30KTL,slave_id=9,type=holding_register AC=32080,DC=32064,Efficiency=0
Resistance,name=EAP,resource=30KTL,slave_id=9,type=holding_register Insulation=0
Status,name=EAP,resource=30KTL,slave_id=9,type=holding_register Device=32086u
Temp,name=EAP,resource=30KTL,slave_id=9,type=holding_register Internal=0
Voltage,name=EAP,resource=30KTL,slave_id=9,type=holding_register PV1=0,PV2=0,PV3=0,PV4=0

View file

@ -0,0 +1,71 @@
# Retrieve data from MODBUS slave devices
[[inputs.modbus]]
# Hiermit wird auch _time auf 60s (also ganze Minuten) gerundet
# Sollten (nach der Timestamp Rundung) mehrere Abfragen auf denselben Timestamp (_time)
# kommen wird kein Fehler geworfen, sondern der bestehende Wert wird einfach upgedated!
precision = "60s"
## Connection Configuration
##
## The plugin supports connections to PLCs via MODBUS/TCP, RTU over TCP, ASCII over TCP or
## via serial line communication in binary (RTU) or readable (ASCII) encoding
##
## Device name
name = "EAP"
## Timeout for each request
timeout = "500ms"
## Maximum number of retries and the time to wait between retries
## when a slave-device is busy.
busy_retries = 3
busy_retries_wait = "200ms"
# TCP - connect via Modbus/TCP
controller = "tcp://192.168.254.223:502"
## Trace the connection to the modbus device as debug messages
## Note: You have to enable telegraf's debug mode to see those messages!
debug_connection = true
## 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 = "request"
## --- "request" configuration style ---
## Per request definition
##
## Define a request sent to the device
[[inputs.modbus.request]]
slave_id = 9
byte_order = "ABCD"
register = "holding"
fields = [
{ address=32000, measurement="Bitfield", name="State1", type="UINT16" },
{ address=32002, measurement="Bitfield", name="State2", type="UINT16" },
{ address=32003, measurement="Bitfield", name="State3", type="UINT32" },
{ address=32008, measurement="Bitfield", name="Alarm1", type="UINT16" },
{ address=32009, measurement="Bitfield", name="Alarm2", type="UINT16" },
{ address=32010, measurement="Bitfield", name="Alarm3", type="UINT16" },
{ address=32016, measurement="Voltage", name="PV1", type="INT16", scale=0.1, output="FLOAT64" },
{ address=32017, measurement="Current", name="PV1", type="INT16", scale=0.01, output="FLOAT64" },
{ address=32018, measurement="Voltage", name="PV2", type="INT16", scale=0.1, output="FLOAT64" },
{ address=32019, measurement="Current", name="PV2", type="INT16", scale=0.01, output="FLOAT64" },
{ address=32020, measurement="Voltage", name="PV3", type="INT16", scale=0.1, output="FLOAT64" },
{ address=32021, measurement="Current", name="PV3", type="INT16", scale=0.01, output="FLOAT64" },
{ address=32022, measurement="Voltage", name="PV4", type="INT16", scale=0.1, output="FLOAT64" },
{ address=32023, measurement="Current", name="PV4", type="INT16", scale=0.01, output="FLOAT64" },
{ address=32064, measurement="Power", name="DC", type="INT32", output="FLOAT64" },
{ address=32080, measurement="Power", name="AC", type="INT32", output="FLOAT64" },
{ address=32086, measurement="Power", name="Efficiency", type="UINT16", scale=0.01, output="FLOAT64" },
{ address=32087, measurement="Temp", name="Internal", type="INT16", scale=0.1, output="FLOAT64" },
{ address=32088, measurement="Resistance", name="Insulation", type="UINT16", scale=0.001, output="FLOAT64" },
{ address=32089, measurement="Status", name="Device", type="UINT16" },
]
[inputs.modbus.request.tags]
resource = "30KTL"

View file

@ -0,0 +1 @@
field "Voltage" duplicated in measurement "V"

View file

@ -0,0 +1,18 @@
[[inputs.modbus]]
name = "Device"
timeout = "1s"
controller = "tcp://localhost:1502"
configuration_type = "request"
[[inputs.modbus.request]]
slave_id = 1
byte_order = "ABCD"
register = "input"
fields = [
{address=200, name="Voltage", type="FLOAT32", measurement="V", omit=false},
{address=200, name="Voltage", type="FLOAT32", measurement="V", omit=false},
]
[inputs.modbus.request.tags]
element = "EleMeter 1"

View file

@ -0,0 +1 @@
duplicated in measurement "modbus"

View file

@ -0,0 +1,28 @@
[[inputs.modbus]]
name = "Device"
controller = "tcp://localhost:502"
configuration_type = "request"
[[inputs.modbus.request]]
slave_id = 1
register = "holding"
fields = [
{ name = "humidity", type = "INT16", scale=1.0, address = 1},
{ name = "temperature", type = "INT16", scale=1.0, address = 4},
{ name = "active", type = "INT16", scale=1.0, address = 7},
]
[inputs.modbus.request.tags]
location = 'Zone 1'
[[inputs.modbus.request]]
slave_id = 1
register = "holding"
fields = [
{ name = "humidity", type = "INT16", scale=1.0, address = 2},
{ name = "temperature", type = "INT16", scale=1.0, address = 5},
{ name = "active", type = "INT16", scale=1.0, address = 8},
]
[inputs.modbus.request.tags]
location = 'Zone 1'

View file

@ -0,0 +1 @@
invalid address

View file

@ -0,0 +1,9 @@
[[inputs.modbus]]
name = "Device"
slave_id = 1
timeout = "1s"
controller = "tcp://localhost:502"
holding_registers = [
{ name = "data", byte_order = "DCBA", data_type = "UINT64", scale=0.01, address = [0, 1]}
]

View file

@ -0,0 +1 @@
modbus,name=modbus,slave_id=1 3x0135:INT=134i,4x0102:INT=0i,4x0103:INT=101i 1729239973009490185

View file

@ -0,0 +1,15 @@
[[inputs.modbus]]
name_override = "modbus"
name = "modbus"
timeout = "1s"
controller = "tcp://172.16.2.31:502"
configuration_type = "metric"
exclude_register_type_tag = true
[[inputs.modbus.metric]]
slave_id = 1
byte_order = "ABCD"
fields = [
{register = "holding", address = 101, name = '4x0102:INT', type = 'INT16'},
{register = "holding", address = 102, name = '4x0103:INT', type = 'INT16'},
{register = "input", address = 134, name = '3x0135:INT', type = 'INT16'},
]

View file

@ -0,0 +1 @@
modbus,name=device-fqdn.com,slave_id=1,type=holding_register energy-10=0,energy-100=0,energy-102=0,energy-104=0,energy-106=0,energy-108=0,energy-110=0,energy-112=0,energy-114=0,energy-116=0,energy-118=0,energy-12=0,energy-120=0,energy-122=0,energy-124=0,energy-126=0,energy-128=0,energy-130=0,energy-132=0,energy-134=0,energy-136=0,energy-138=0,energy-14=0,energy-140=0,energy-142=0,energy-144=0,energy-146=0,energy-148=0,energy-150=0,energy-152=0,energy-154=0,energy-156=0,energy-158=0,energy-16=0,energy-160=0,energy-162=0,energy-164=0,energy-166=0,energy-168=0,energy-170=0,energy-172=0,energy-174=0,energy-176=0,energy-178=0,energy-18=0,energy-180=0,energy-182=0,energy-184=0,energy-186=0,energy-188=0,energy-190=0,energy-192=0,energy-194=0,energy-196=0,energy-198=0,energy-2=0,energy-20=0,energy-200=0,energy-202=0,energy-204=0,energy-206=0,energy-208=0,energy-210=0,energy-212=0,energy-214=0,energy-216=0,energy-22=0,energy-24=0,energy-26=0,energy-28=0,energy-30=0,energy-32=0,energy-34=0,energy-36=0,energy-38=0,energy-4=0,energy-40=0,energy-42=0,energy-44=0,energy-46=0,energy-48=0,energy-50=0,energy-52=0,energy-54=0,energy-56=0,energy-58=0,energy-6=0,energy-60=0,energy-62=0,energy-64=0,energy-66=0,energy-68=0,energy-70=0,energy-72=0,energy-74=0,energy-76=0,energy-78=0,energy-8=0,energy-80=0,energy-82=0,energy-84=0,energy-86=0,energy-88=0,energy-90=0,energy-92=0,energy-94=0,energy-96=0,energy-98=0 1701777274026591864

View file

@ -0,0 +1,125 @@
[[inputs.modbus]]
name="device-fqdn.com"
timeout = "1s"
interval = "900s"
busy_retries = 0
busy_retries_wait = "100ms"
controller = "tcp://localhost:502"
configuration_type = "request"
[[inputs.modbus.request]]
slave_id = 1
byte_order = "ABCD"
register = "holding"
fields = [
{ address=12800, type="UINT32", name="energy-2", scale=10.0 },
{ address=12802, type="UINT32", name="energy-4", scale=10.0 },
{ address=12804, type="UINT32", name="energy-6", scale=10.0 },
{ address=12806, type="UINT32", name="energy-8", scale=10.0 },
{ address=12808, type="UINT32", name="energy-10", scale=10.0 },
{ address=12810, type="UINT32", name="energy-12", scale=10.0 },
{ address=12812, type="UINT32", name="energy-14", scale=10.0 },
{ address=12814, type="UINT32", name="energy-16", scale=10.0 },
{ address=12816, type="UINT32", name="energy-18", scale=10.0 },
{ address=12818, type="UINT32", name="energy-20", scale=10.0 },
{ address=12820, type="UINT32", name="energy-22", scale=10.0 },
{ address=12822, type="UINT32", name="energy-24", scale=10.0 },
{ address=12824, type="UINT32", name="energy-26", scale=10.0 },
{ address=12826, type="UINT32", name="energy-28", scale=10.0 },
{ address=12828, type="UINT32", name="energy-178", scale=10.0 },
{ address=12830, type="UINT32", name="energy-30", scale=10.0 },
{ address=12832, type="UINT32", name="energy-32", scale=10.0 },
{ address=12834, type="UINT32", name="energy-34", scale=10.0 },
{ address=12836, type="UINT32", name="energy-36", scale=10.0 },
{ address=12838, type="UINT32", name="energy-38", scale=10.0 },
{ address=12840, type="UINT32", name="energy-40", scale=10.0 },
{ address=12842, type="UINT32", name="energy-42", scale=10.0 },
{ address=12844, type="UINT32", name="energy-44", scale=10.0 },
{ address=12846, type="UINT32", name="energy-46", scale=10.0 },
{ address=12848, type="UINT32", name="energy-48", scale=10.0 },
{ address=12850, type="UINT32", name="energy-50", scale=10.0 },
{ address=12852, type="UINT32", name="energy-52", scale=10.0 },
{ address=12854, type="UINT32", name="energy-54", scale=10.0 },
{ address=12856, type="UINT32", name="energy-56", scale=10.0 },
{ address=12858, type="UINT32", name="energy-58", scale=10.0 },
{ address=12860, type="UINT32", name="energy-60", scale=10.0 },
{ address=12862, type="UINT32", name="energy-62", scale=10.0 },
{ address=12864, type="UINT32", name="energy-64", scale=10.0 },
{ address=12866, type="UINT32", name="energy-66", scale=10.0 },
{ address=12868, type="UINT32", name="energy-68", scale=10.0 },
{ address=12870, type="UINT32", name="energy-70", scale=10.0 },
{ address=12872, type="UINT32", name="energy-72", scale=10.0 },
{ address=12874, type="UINT32", name="energy-74", scale=10.0 },
{ address=12876, type="UINT32", name="energy-76", scale=10.0 },
{ address=12878, type="UINT32", name="energy-78", scale=10.0 },
{ address=12880, type="UINT32", name="energy-80", scale=10.0 },
{ address=12882, type="UINT32", name="energy-82", scale=10.0 },
{ address=12884, type="UINT32", name="energy-84", scale=10.0 },
{ address=12886, type="UINT32", name="energy-86", scale=10.0 },
{ address=12888, type="UINT32", name="energy-88", scale=10.0 },
{ address=12890, type="UINT32", name="energy-90", scale=10.0 },
{ address=12892, type="UINT32", name="energy-92", scale=10.0 },
{ address=12894, type="UINT32", name="energy-94", scale=10.0 },
{ address=12896, type="UINT32", name="energy-96", scale=10.0 },
{ address=12898, type="UINT32", name="energy-98", scale=10.0 },
{ address=12900, type="UINT32", name="energy-100", scale=10.0 },
{ address=12902, type="UINT32", name="energy-102", scale=10.0 },
{ address=12904, type="UINT32", name="energy-104", scale=10.0 },
{ address=12906, type="UINT32", name="energy-106", scale=10.0 },
{ address=12908, type="UINT32", name="energy-108", scale=10.0 },
{ address=12910, type="UINT32", name="energy-110", scale=10.0 },
{ address=12912, type="UINT32", name="energy-112", scale=10.0 },
{ address=12914, type="UINT32", name="energy-114", scale=10.0 },
{ address=12916, type="UINT32", name="energy-116", scale=10.0 },
{ address=12918, type="UINT32", name="energy-118", scale=10.0 },
{ address=12920, type="UINT32", name="energy-120", scale=10.0 },
{ address=12922, type="UINT32", name="energy-122", scale=10.0 },
{ address=12924, type="UINT32", name="energy-124", scale=10.0 },
{ address=12926, type="UINT32", name="energy-126", scale=10.0 },
{ address=12928, type="UINT32", name="energy-128", scale=10.0 },
{ address=12930, type="UINT32", name="energy-130", scale=10.0 },
{ address=12932, type="UINT32", name="energy-132", scale=10.0 },
{ address=12934, type="UINT32", name="energy-134", scale=10.0 },
{ address=12936, type="UINT32", name="energy-136", scale=10.0 },
{ address=12938, type="UINT32", name="energy-138", scale=10.0 },
{ address=12940, type="UINT32", name="energy-140", scale=10.0 },
{ address=12942, type="UINT32", name="energy-142", scale=10.0 },
{ address=12944, type="UINT32", name="energy-144", scale=10.0 },
{ address=12946, type="UINT32", name="energy-146", scale=10.0 },
{ address=12948, type="UINT32", name="energy-148", scale=10.0 },
{ address=12950, type="UINT32", name="energy-150", scale=10.0 },
{ address=12952, type="UINT32", name="energy-152", scale=10.0 },
{ address=12954, type="UINT32", name="energy-154", scale=10.0 },
{ address=12956, type="UINT32", name="energy-156", scale=10.0 },
{ address=12958, type="UINT32", name="energy-158", scale=10.0 },
{ address=12960, type="UINT32", name="energy-160", scale=10.0 },
{ address=12962, type="UINT32", name="energy-162", scale=10.0 },
{ address=12964, type="UINT32", name="energy-164", scale=10.0 },
{ address=12966, type="UINT32", name="energy-166", scale=10.0 },
{ address=12968, type="UINT32", name="energy-168", scale=10.0 },
{ address=12970, type="UINT32", name="energy-170", scale=10.0 },
{ address=12972, type="UINT32", name="energy-172", scale=10.0 },
{ address=12974, type="UINT32", name="energy-174", scale=10.0 },
{ address=12976, type="UINT32", name="energy-176", scale=10.0 },
{ address=12978, type="UINT32", name="energy-180", scale=10.0 },
{ address=12980, type="UINT32", name="energy-182", scale=10.0 },
{ address=12982, type="UINT32", name="energy-184", scale=10.0 },
{ address=12984, type="UINT32", name="energy-186", scale=10.0 },
{ address=12986, type="UINT32", name="energy-188", scale=10.0 },
{ address=12988, type="UINT32", name="energy-190", scale=10.0 },
{ address=12990, type="UINT32", name="energy-192", scale=10.0 },
{ address=12992, type="UINT32", name="energy-194", scale=10.0 },
{ address=12994, type="UINT32", name="energy-196", scale=10.0 },
{ address=12996, type="UINT32", name="energy-198", scale=10.0 },
{ address=12998, type="UINT32", name="energy-200", scale=10.0 },
{ address=13000, type="UINT32", name="energy-202", scale=10.0 },
{ address=13002, type="UINT32", name="energy-204", scale=10.0 },
{ address=13004, type="UINT32", name="energy-206", scale=10.0 },
{ address=13006, type="UINT32", name="energy-208", scale=10.0 },
{ address=13008, type="UINT32", name="energy-210", scale=10.0 },
{ address=13010, type="UINT32", name="energy-212", scale=10.0 },
{ address=13012, type="UINT32", name="energy-214", scale=10.0 },
{ address=13014, type="UINT32", name="energy-216", scale=10.0 },
]

View file

@ -0,0 +1,104 @@
package modbus
import (
"fmt"
)
func determineUntypedConverter(outType string) (fieldConverterFunc, error) {
switch outType {
case "", "UINT16":
return func(b []byte) interface{} {
return uint16(b[0])
}, nil
case "BOOL":
return func(b []byte) interface{} {
return b[0] != 0
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
func determineConverter(inType, byteOrder, outType string, scale float64, bit uint8, strloc string) (fieldConverterFunc, error) {
switch inType {
case "STRING":
switch strloc {
case "", "both":
return determineConverterString(byteOrder)
case "lower":
return determineConverterStringLow(byteOrder)
case "upper":
return determineConverterStringHigh(byteOrder)
}
case "BIT":
return determineConverterBit(byteOrder, bit)
}
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 "INT8L":
return determineConverterI8LScale(outType, byteOrder, scale)
case "INT8H":
return determineConverterI8HScale(outType, byteOrder, scale)
case "UINT8L":
return determineConverterU8LScale(outType, byteOrder, scale)
case "UINT8H":
return determineConverterU8HScale(outType, byteOrder, scale)
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 "FLOAT16":
return determineConverterF16Scale(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 "INT8L":
return determineConverterI8L(outType, byteOrder)
case "INT8H":
return determineConverterI8H(outType, byteOrder)
case "UINT8L":
return determineConverterU8L(outType, byteOrder)
case "UINT8H":
return determineConverterU8H(outType, byteOrder)
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 "FLOAT16":
return determineConverterF16(outType, byteOrder)
case "FLOAT32":
return determineConverterF32(outType, byteOrder)
case "FLOAT64":
return determineConverterF64(outType, byteOrder)
}
return nil, fmt.Errorf("invalid input data-type: %s", inType)
}

View file

@ -0,0 +1,187 @@
package modbus
import (
"encoding/binary"
"fmt"
"github.com/x448/float16"
)
type convert16 func([]byte) uint16
func endiannessConverter16(byteOrder string) (convert16, error) {
switch byteOrder {
case "ABCD", "CDAB": // Big endian (Motorola)
return binary.BigEndian.Uint16, nil
case "DCBA", "BADC": // 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 := endiannessConverter16(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 := endiannessConverter16(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)
}
// F16 - no scale
func determineConverterF16(outType, byteOrder string) (fieldConverterFunc, error) {
tohost, err := endiannessConverter16(byteOrder)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
raw := tohost(b)
return float16.Frombits(raw).Float32()
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
raw := tohost(b)
in := float16.Frombits(raw).Float32()
return float64(in)
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
// I16 - scale
func determineConverterI16Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
tohost, err := endiannessConverter16(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 := endiannessConverter16(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)
}
// F16 - scale
func determineConverterF16Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
tohost, err := endiannessConverter16(byteOrder)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
raw := tohost(b)
in := float16.Frombits(raw)
return in.Float32() * float32(scale)
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
raw := tohost(b)
in := float16.Frombits(raw)
return float64(in.Float32()) * scale
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}

View file

@ -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 endiannessConverter32(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 := endiannessConverter32(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 := endiannessConverter32(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 := endiannessConverter32(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 := endiannessConverter32(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 := endiannessConverter32(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 := endiannessConverter32(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)
}

View file

@ -0,0 +1,184 @@
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 endiannessConverter64(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 := endiannessConverter64(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 := endiannessConverter64(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 := endiannessConverter64(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 := endiannessConverter64(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 := endiannessConverter64(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 := endiannessConverter64(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)
}

View file

@ -0,0 +1,253 @@
package modbus
import (
"fmt"
)
func endiannessIndex8(byteOrder string, low bool) (int, error) {
switch byteOrder {
case "ABCD": // Big endian (Motorola)
if low {
return 1, nil
}
return 0, nil
case "DCBA": // Little endian (Intel)
if low {
return 0, nil
}
return 1, nil
}
return -1, fmt.Errorf("invalid byte-order: %s", byteOrder)
}
// I8 lower byte - no scale
func determineConverterI8L(outType, byteOrder string) (fieldConverterFunc, error) {
idx, err := endiannessIndex8(byteOrder, true)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
return int8(b[idx])
}, nil
case "INT64":
return func(b []byte) interface{} {
return int64(int8(b[idx]))
}, nil
case "UINT64":
return func(b []byte) interface{} {
return uint64(int8(b[idx]))
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
return float64(int8(b[idx]))
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
// I8 higher byte - no scale
func determineConverterI8H(outType, byteOrder string) (fieldConverterFunc, error) {
idx, err := endiannessIndex8(byteOrder, false)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
return int8(b[idx])
}, nil
case "INT64":
return func(b []byte) interface{} {
return int64(int8(b[idx]))
}, nil
case "UINT64":
return func(b []byte) interface{} {
return uint64(int8(b[idx]))
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
return float64(int8(b[idx]))
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
// U8 lower byte - no scale
func determineConverterU8L(outType, byteOrder string) (fieldConverterFunc, error) {
idx, err := endiannessIndex8(byteOrder, true)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
return b[idx]
}, nil
case "INT64":
return func(b []byte) interface{} {
return int64(b[idx])
}, nil
case "UINT64":
return func(b []byte) interface{} {
return uint64(b[idx])
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
return float64(b[idx])
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
// U8 higher byte - no scale
func determineConverterU8H(outType, byteOrder string) (fieldConverterFunc, error) {
idx, err := endiannessIndex8(byteOrder, false)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
return b[idx]
}, nil
case "INT64":
return func(b []byte) interface{} {
return int64(b[idx])
}, nil
case "UINT64":
return func(b []byte) interface{} {
return uint64(b[idx])
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
return float64(b[idx])
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
// I8 lower byte - scale
func determineConverterI8LScale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
idx, err := endiannessIndex8(byteOrder, true)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
in := int8(b[idx])
return int8(float64(in) * scale)
}, nil
case "INT64":
return func(b []byte) interface{} {
in := int8(b[idx])
return int64(float64(in) * scale)
}, nil
case "UINT64":
return func(b []byte) interface{} {
in := int8(b[idx])
return uint64(float64(in) * scale)
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
in := int8(b[idx])
return float64(in) * scale
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
// I8 higher byte - scale
func determineConverterI8HScale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
idx, err := endiannessIndex8(byteOrder, false)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
in := int8(b[idx])
return int8(float64(in) * scale)
}, nil
case "INT64":
return func(b []byte) interface{} {
in := int8(b[idx])
return int64(float64(in) * scale)
}, nil
case "UINT64":
return func(b []byte) interface{} {
in := int8(b[idx])
return uint64(float64(in) * scale)
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
in := int8(b[idx])
return float64(in) * scale
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
// U8 lower byte - scale
func determineConverterU8LScale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
idx, err := endiannessIndex8(byteOrder, true)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
return uint8(float64(b[idx]) * scale)
}, nil
case "INT64":
return func(b []byte) interface{} {
return int64(float64(b[idx]) * scale)
}, nil
case "UINT64":
return func(b []byte) interface{} {
return uint64(float64(b[idx]) * scale)
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
return float64(b[idx]) * scale
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}
// U8 higher byte - scale
func determineConverterU8HScale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
idx, err := endiannessIndex8(byteOrder, false)
if err != nil {
return nil, err
}
switch outType {
case "native":
return func(b []byte) interface{} {
return uint8(float64(b[idx]) * scale)
}, nil
case "INT64":
return func(b []byte) interface{} {
return int64(float64(b[idx]) * scale)
}, nil
case "UINT64":
return func(b []byte) interface{} {
return uint64(float64(b[idx]) * scale)
}, nil
case "FLOAT64":
return func(b []byte) interface{} {
return float64(b[idx]) * scale
}, nil
}
return nil, fmt.Errorf("invalid output data-type: %s", outType)
}

View file

@ -0,0 +1,14 @@
package modbus
func determineConverterBit(byteOrder string, bit uint8) (fieldConverterFunc, error) {
tohost, err := endiannessConverter16(byteOrder)
if err != nil {
return nil, err
}
return func(b []byte) interface{} {
// Swap the bytes according to endianness
v := tohost(b)
return uint8(v >> bit & 0x01)
}, nil
}

View file

@ -0,0 +1,63 @@
package modbus
import (
"bytes"
)
func determineConverterString(byteOrder string) (fieldConverterFunc, error) {
tohost, err := endiannessConverter16(byteOrder)
if err != nil {
return nil, err
}
return func(b []byte) interface{} {
// Swap the bytes according to endianness
var buf bytes.Buffer
for i := 0; i < len(b); i += 2 {
v := tohost(b[i : i+2])
buf.WriteByte(byte(v >> 8))
buf.WriteByte(byte(v & 0xFF))
}
// Remove everything after null-termination
s, _ := bytes.CutSuffix(buf.Bytes(), []byte{0x00})
return string(s)
}, nil
}
func determineConverterStringLow(byteOrder string) (fieldConverterFunc, error) {
tohost, err := endiannessConverter16(byteOrder)
if err != nil {
return nil, err
}
return func(b []byte) interface{} {
// Swap the bytes according to endianness
var buf bytes.Buffer
for i := 0; i < len(b); i += 2 {
v := tohost(b[i : i+2])
buf.WriteByte(byte(v & 0xFF))
}
// Remove everything after null-termination
s, _ := bytes.CutSuffix(buf.Bytes(), []byte{0x00})
return string(s)
}, nil
}
func determineConverterStringHigh(byteOrder string) (fieldConverterFunc, error) {
tohost, err := endiannessConverter16(byteOrder)
if err != nil {
return nil, err
}
return func(b []byte) interface{} {
// Swap the bytes according to endianness
var buf bytes.Buffer
for i := 0; i < len(b); i += 2 {
v := tohost(b[i : i+2])
buf.WriteByte(byte(v >> 8))
}
// Remove everything after null-termination
s, _ := bytes.CutSuffix(buf.Bytes(), []byte{0x00})
return string(s)
}, nil
}