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

This commit is contained in:
Sven Rebhan 2023-11-07 09:48:23 +01:00 committed by GitHub
parent d15ea5178c
commit 247a808769
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 273 additions and 95 deletions

View File

@ -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
@ -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]},
@ -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".
@ -189,6 +194,7 @@ 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 = [
@ -196,6 +202,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
{ 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]
@ -282,14 +289,17 @@ 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 - (optional) factor to scale the variable with
## output *2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if
## 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"},
@ -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
@ -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`
@ -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
@ -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.
@ -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.

View File

@ -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)
@ -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)

View File

@ -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"`
@ -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)
}
@ -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
}
}
@ -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
// For scaling cases we always want FLOAT64 by default except for
// string fields
if def.InputType != "STRING" {
def.OutputType = "FLOAT64"
} else {
def.OutputType = "STRING"
}
}
}
@ -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":
@ -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)
}

View File

@ -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
@ -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",
@ -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),
),

View File

@ -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":
default:
return fmt.Errorf("invalid data type %q in %q - %q", item.DataType, registerType, item.Name)
}
// check scale
// 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)
}
} else {
// Bit-registers do have less data types
switch item.DataType {
@ -220,6 +220,7 @@ func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefini
}
// check address
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)
}
@ -255,6 +256,7 @@ func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefini
return fmt.Errorf("invalid address '%v' length '%v'in %q - %q", item.Address, len(item.Address), registerType, item.Name)
}
}
}
return nil
}
@ -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)
}

View File

@ -841,6 +841,24 @@ func TestRegisterHoldingRegisters(t *testing.T) {
write: []byte{0x14, 0xb8},
read: float64(-0.509765625),
},
{
name: "register250_abcd_string",
address: []uint16{250, 251, 252, 253, 254, 255, 256},
quantity: 7,
byteOrder: "AB",
dataType: "STRING",
write: []byte{0x4d, 0x6f, 0x64, 0x62, 0x75, 0x73, 0x20, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x00},
read: "Modbus String",
},
{
name: "register250_dcba_string",
address: []uint16{250, 251, 252, 253, 254, 255, 256},
quantity: 7,
byteOrder: "BA",
dataType: "STRING",
write: []byte{0x6f, 0x4d, 0x62, 0x64, 0x73, 0x75, 0x53, 0x20, 0x72, 0x74, 0x6e, 0x69, 0x00, 0x67},
read: "Modbus String",
},
}
serv := mbserver.NewServer()

View File

@ -17,6 +17,7 @@ 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"`
@ -121,9 +122,25 @@ func (c *ConfigurationPerRequest) Check() error {
if def.RegisterType == "holding" || def.RegisterType == "input" {
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)
}
@ -142,7 +159,7 @@ func (c *ConfigurationPerRequest) Check() error {
// Check output type
if def.RegisterType == "holding" || def.RegisterType == "input" {
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)
}
@ -269,7 +286,7 @@ func (c *ConfigurationPerRequest) newFieldFromDefinition(def requestFieldDefinit
fieldLength := uint16(1)
if typed {
if fieldLength, err = c.determineFieldLength(def.InputType); err != nil {
if fieldLength, err = c.determineFieldLength(def.InputType, def.Length); err != nil {
return field{}, err
}
}
@ -306,8 +323,13 @@ func (c *ConfigurationPerRequest) newFieldFromDefinition(def requestFieldDefinit
return field{}, err
}
} else {
// For scaling cases we always want FLOAT64 by default
// For scaling cases we always want FLOAT64 by default except for
// string fields
if def.InputType != "STRING" {
def.OutputType = "FLOAT64"
} else {
def.OutputType = "STRING"
}
}
}
@ -398,11 +420,13 @@ func (c *ConfigurationPerRequest) 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 *ConfigurationPerRequest) determineFieldLength(input string) (uint16, error) {
func (c *ConfigurationPerRequest) determineFieldLength(input string, length uint16) (uint16, error) {
// Handle our special types
switch input {
case "INT8L", "INT8H", "UINT8L", "UINT8H":
@ -413,6 +437,8 @@ func (c *ConfigurationPerRequest) determineFieldLength(input string) (uint16, er
return 2, nil
case "INT64", "UINT64", "FLOAT64":
return 4, nil
case "STRING":
return length, nil
}
return 0, fmt.Errorf("invalid input datatype %q for determining field length", input)
}

View File

@ -457,6 +457,7 @@ func TestRequestTypesHoldingABCD(t *testing.T) {
tests := []struct {
name string
address uint16
length uint16
byteOrder string
dataTypeIn string
dataTypeOut string
@ -989,6 +990,14 @@ func TestRequestTypesHoldingABCD(t *testing.T) {
write: []byte{0xb8, 0x14},
read: float64(-0.509765625),
},
{
name: "register110_string",
address: 110,
dataTypeIn: "STRING",
length: 7,
write: []byte{0x4d, 0x6f, 0x64, 0x62, 0x75, 0x73, 0x20, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x00},
read: "Modbus String",
},
}
serv := mbserver.NewServer()
@ -1024,6 +1033,7 @@ func TestRequestTypesHoldingABCD(t *testing.T) {
OutputType: hrt.dataTypeOut,
Scale: hrt.scale,
Address: hrt.address,
Length: hrt.length,
},
},
},
@ -1058,6 +1068,7 @@ func TestRequestTypesHoldingDCBA(t *testing.T) {
tests := []struct {
name string
address uint16
length uint16
byteOrder string
dataTypeIn string
dataTypeOut string
@ -1590,6 +1601,14 @@ func TestRequestTypesHoldingDCBA(t *testing.T) {
write: []byte{0xb8, 0x14},
read: float64(-0.509765625),
},
{
name: "register110_string",
address: 110,
dataTypeIn: "STRING",
length: 7,
write: []byte{0x6f, 0x4d, 0x62, 0x64, 0x73, 0x75, 0x53, 0x20, 0x72, 0x74, 0x6e, 0x69, 0x00, 0x67},
read: "Modbus String",
},
}
serv := mbserver.NewServer()
@ -1605,9 +1624,14 @@ func TestRequestTypesHoldingDCBA(t *testing.T) {
t.Run(hrt.name, func(t *testing.T) {
quantity := uint16(len(hrt.write) / 2)
invert := make([]byte, 0, len(hrt.write))
if hrt.dataTypeIn != "STRING" {
for i := len(hrt.write) - 1; i >= 0; i-- {
invert = append(invert, hrt.write[i])
}
} else {
// Put in raw data for strings
invert = append(invert, hrt.write...)
}
_, err := client.WriteMultipleRegisters(hrt.address, quantity, invert)
require.NoError(t, err)
@ -1629,6 +1653,7 @@ func TestRequestTypesHoldingDCBA(t *testing.T) {
OutputType: hrt.dataTypeOut,
Scale: hrt.scale,
Address: hrt.address,
Length: hrt.length,
},
},
},

View File

@ -43,14 +43,17 @@
## 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
## 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"},
@ -59,6 +62,7 @@
{ 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

View File

@ -34,6 +34,7 @@
## 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
@ -44,6 +45,7 @@
{ 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]},

View File

@ -52,9 +52,12 @@
## 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".
@ -64,6 +67,7 @@
## *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 = [
@ -71,6 +75,7 @@
{ 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]

View File

@ -1,6 +1,8 @@
package modbus
import "fmt"
import (
"fmt"
)
func determineUntypedConverter(outType string) (fieldConverterFunc, error) {
switch outType {
@ -17,7 +19,7 @@ func determineUntypedConverter(outType string) (fieldConverterFunc, error) {
}
func determineConverter(inType, byteOrder, outType string, scale float64) (fieldConverterFunc, error) {
if scale != 0.0 {
if scale != 0.0 && inType != "STRING" {
return determineConverterScale(inType, byteOrder, outType, scale)
}
return determineConverterNoScale(inType, byteOrder, outType)
@ -83,6 +85,8 @@ func determineConverterNoScale(inType, byteOrder, outType string) (fieldConverte
return determineConverterF32(outType, byteOrder)
case "FLOAT64":
return determineConverterF64(outType, byteOrder)
case "STRING":
return determineConverterString(byteOrder)
}
return nil, fmt.Errorf("invalid input data-type: %s", inType)
}

View File

@ -0,0 +1,25 @@
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
}