Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat(inputs.modbus): Add support for string-fields #14145

Merged
merged 3 commits into from
Nov 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 52 additions & 22 deletions plugins/inputs/modbus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation)
## FIXED, UFIXED (fixed-point representation on input)
## FLOAT32 is a deprecated alias for UFIXED for historic reasons, should be avoided
## STRING (byte-sequence converted to string)
## scale - the final numeric variable representation
## address - variable address

Expand All @@ -116,6 +117,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
{ 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]},
Expand Down Expand Up @@ -177,9 +179,12 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
## scale *1,2 - (optional) factor to scale the variable with
## output *1,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).
## STRING (byte-sequence converted to string)
## length *1,2 - (optional) number of registers, ONLY valid for STRING 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".
Expand All @@ -189,13 +194,15 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## *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=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]
Expand Down Expand Up @@ -274,22 +281,25 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
# 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
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
## scale *1 - (optional) factor to scale the variable with
## output *2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if
## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc).
## 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
## 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
## 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"},
Expand All @@ -298,6 +308,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
{ 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
Expand Down Expand Up @@ -388,10 +399,10 @@ configuration for a single slave-device.
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 one of string, integer or
floating-point-number. The size of the output type is assumed to be large enough
for all supported input types. The mapping from the input type to the output
type is fixed and cannot be configured.
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`

Expand Down Expand Up @@ -433,6 +444,13 @@ like 'int32 containing fixed-point representation with N decimal places'.
(`FLOAT32` is deprecated and should not be used. `UFIXED` provides the same
conversion from unsigned values).

##### 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.

---

### `request` configuration style
Expand Down Expand Up @@ -563,6 +581,12 @@ 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.
Expand Down Expand Up @@ -722,6 +746,12 @@ 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.

Expand Down
4 changes: 2 additions & 2 deletions plugins/inputs/modbus/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func normalizeInputDatatype(dataType string) (string, error) {
switch dataType {
case "INT8L", "INT8H", "UINT8L", "UINT8H",
"INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64",
"FLOAT16", "FLOAT32", "FLOAT64":
"FLOAT16", "FLOAT32", "FLOAT64", "STRING":
return dataType, nil
}
return "unknown", fmt.Errorf("unknown input type %q", dataType)
Expand All @@ -43,7 +43,7 @@ func normalizeOutputDatatype(dataType string) (string, error) {
switch dataType {
case "", "native":
return "native", nil
case "INT64", "UINT64", "FLOAT64":
case "INT64", "UINT64", "FLOAT64", "STRING":
return dataType, nil
}
return "unknown", fmt.Errorf("unknown output type %q", dataType)
Expand Down
42 changes: 34 additions & 8 deletions plugins/inputs/modbus/configuration_metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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"`
Expand Down Expand Up @@ -101,16 +102,32 @@ func (c *ConfigurationPerMetric) Check() error {
// Check the input type
switch f.InputType {
case "":
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
case "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
case "FLOAT16", "FLOAT32", "FLOAT64":
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.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.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)
}
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":
case "", "INT64", "UINT64", "FLOAT64", "STRING":
default:
return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name)
}
Expand Down Expand Up @@ -223,7 +240,7 @@ func (c *ConfigurationPerMetric) newField(def metricFieldDefinition, mdef metric
fieldLength := uint16(1)
if typed {
var err error
if fieldLength, err = c.determineFieldLength(def.InputType); err != nil {
if fieldLength, err = c.determineFieldLength(def.InputType, def.Length); err != nil {
return field{}, err
}
}
Expand Down Expand Up @@ -258,8 +275,13 @@ func (c *ConfigurationPerMetric) newField(def metricFieldDefinition, mdef metric
return field{}, err
}
} else {
// For scaling cases we always want FLOAT64 by default
def.OutputType = "FLOAT64"
// For scaling cases we always want FLOAT64 by default except for
// string fields
if def.InputType != "STRING" {
def.OutputType = "FLOAT64"
} else {
def.OutputType = "STRING"
}
}
}

Expand Down Expand Up @@ -351,11 +373,13 @@ func (c *ConfigurationPerMetric) determineOutputDatatype(input string) (string,
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 (c *ConfigurationPerMetric) determineFieldLength(input string) (uint16, error) {
func (c *ConfigurationPerMetric) determineFieldLength(input string, length uint16) (uint16, error) {
// Handle our special types
switch input {
case "INT8L", "INT8H", "UINT8L", "UINT8H":
Expand All @@ -366,6 +390,8 @@ func (c *ConfigurationPerMetric) determineFieldLength(input string) (uint16, err
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)
}
9 changes: 9 additions & 0 deletions plugins/inputs/modbus/configuration_metric_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ func TestMetricResult(t *testing.T) {
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
Expand Down Expand Up @@ -203,6 +204,13 @@ func TestMetricResult(t *testing.T) {
InputType: "INT16",
RegisterType: "holding",
},
{
Name: "comment",
Address: uint16(11),
Length: 7,
InputType: "STRING",
RegisterType: "holding",
},
},
Tags: map[string]string{
"location": "main building",
Expand Down Expand Up @@ -275,6 +283,7 @@ func TestMetricResult(t *testing.T) {
map[string]interface{}{
"hours": uint64(10),
"temperature": int64(42),
"comment": "Modbus String",
},
time.Unix(0, 0),
),
Expand Down
74 changes: 39 additions & 35 deletions plugins/inputs/modbus/configuration_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,14 +202,14 @@ func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefini
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 "STRING":
default:
return fmt.Errorf("invalid data type %q in %q - %q", item.DataType, registerType, item.Name)
}

// check scale
if item.Scale == 0.0 {
return fmt.Errorf("invalid scale '%f' in %q - %q", item.Scale, registerType, item.Name)
}
} else {
// Bit-registers do have less data types
switch item.DataType {
Expand All @@ -220,39 +220,41 @@ func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefini
}

// 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,
)
if item.DataType != "STRING" {
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)
}

// search duplicated
if len(item.Address) > len(removeDuplicates(item.Address)) {
return fmt.Errorf("duplicate address '%v' in %q - %q", 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)
}
} 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
Expand Down Expand Up @@ -297,6 +299,8 @@ func (c *ConfigurationOriginal) normalizeInputDatatype(dataType string, words in
return "FLOAT32", nil
case "FLOAT64-IEEE":
return "FLOAT64", nil
case "STRING":
return "STRING", nil
}
return normalizeInputDatatype(dataType)
}
Expand Down
Loading
Loading