Adding upstream version 1.34.4.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
e393c3af3f
commit
4978089aab
4963 changed files with 677545 additions and 0 deletions
885
plugins/inputs/modbus/README.md
Normal file
885
plugins/inputs/modbus/README.md
Normal 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
|
||||
```
|
64
plugins/inputs/modbus/configuration.go
Normal file
64
plugins/inputs/modbus/configuration.go
Normal 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)
|
||||
}
|
399
plugins/inputs/modbus/configuration_metric.go
Normal file
399
plugins/inputs/modbus/configuration_metric.go
Normal 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)
|
||||
}
|
393
plugins/inputs/modbus/configuration_metric_test.go
Normal file
393
plugins/inputs/modbus/configuration_metric_test.go
Normal 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)
|
||||
}
|
348
plugins/inputs/modbus/configuration_register.go
Normal file
348
plugins/inputs/modbus/configuration_register.go
Normal 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)
|
||||
}
|
1284
plugins/inputs/modbus/configuration_register_test.go
Normal file
1284
plugins/inputs/modbus/configuration_register_test.go
Normal file
File diff suppressed because it is too large
Load diff
439
plugins/inputs/modbus/configuration_request.go
Normal file
439
plugins/inputs/modbus/configuration_request.go
Normal 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)
|
||||
}
|
3339
plugins/inputs/modbus/configuration_request_test.go
Normal file
3339
plugins/inputs/modbus/configuration_request_test.go
Normal file
File diff suppressed because it is too large
Load diff
566
plugins/inputs/modbus/modbus.go
Normal file
566
plugins/inputs/modbus/modbus.go
Normal 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{} })
|
||||
}
|
744
plugins/inputs/modbus/modbus_test.go
Normal file
744
plugins/inputs/modbus/modbus_test.go
Normal 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'`)
|
||||
}
|
305
plugins/inputs/modbus/request.go
Normal file
305
plugins/inputs/modbus/request.go
Normal 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
|
||||
}
|
57
plugins/inputs/modbus/sample_general_begin.conf
Normal file
57
plugins/inputs/modbus/sample_general_begin.conf
Normal 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
|
51
plugins/inputs/modbus/sample_general_end.conf
Normal file
51
plugins/inputs/modbus/sample_general_end.conf
Normal 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 = ""
|
73
plugins/inputs/modbus/sample_metric.conf
Normal file
73
plugins/inputs/modbus/sample_metric.conf
Normal 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"
|
55
plugins/inputs/modbus/sample_register.conf
Normal file
55
plugins/inputs/modbus/sample_register.conf
Normal 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]},
|
||||
]
|
120
plugins/inputs/modbus/sample_request.conf
Normal file
120
plugins/inputs/modbus/sample_request.conf
Normal 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"
|
|
@ -0,0 +1 @@
|
|||
duplicated in measurement "modbus"
|
|
@ -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},
|
||||
]
|
||||
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -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'
|
|
@ -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
|
|
@ -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"
|
|
@ -0,0 +1 @@
|
|||
field "Voltage" duplicated in measurement "V"
|
|
@ -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"
|
|
@ -0,0 +1 @@
|
|||
duplicated in measurement "modbus"
|
|
@ -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'
|
|
@ -0,0 +1 @@
|
|||
invalid address
|
|
@ -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]}
|
||||
]
|
|
@ -0,0 +1 @@
|
|||
modbus,name=modbus,slave_id=1 3x0135:INT=134i,4x0102:INT=0i,4x0103:INT=101i 1729239973009490185
|
|
@ -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'},
|
||||
]
|
|
@ -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
|
|
@ -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 },
|
||||
]
|
104
plugins/inputs/modbus/type_conversions.go
Normal file
104
plugins/inputs/modbus/type_conversions.go
Normal 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)
|
||||
}
|
187
plugins/inputs/modbus/type_conversions16.go
Normal file
187
plugins/inputs/modbus/type_conversions16.go
Normal 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)
|
||||
}
|
200
plugins/inputs/modbus/type_conversions32.go
Normal file
200
plugins/inputs/modbus/type_conversions32.go
Normal 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)
|
||||
}
|
184
plugins/inputs/modbus/type_conversions64.go
Normal file
184
plugins/inputs/modbus/type_conversions64.go
Normal 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)
|
||||
}
|
253
plugins/inputs/modbus/type_conversions8.go
Normal file
253
plugins/inputs/modbus/type_conversions8.go
Normal 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)
|
||||
}
|
14
plugins/inputs/modbus/type_conversions_bit.go
Normal file
14
plugins/inputs/modbus/type_conversions_bit.go
Normal 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
|
||||
}
|
63
plugins/inputs/modbus/type_conversions_string.go
Normal file
63
plugins/inputs/modbus/type_conversions_string.go
Normal 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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue