feat(inputs.modbus): Add support for string-fields (#14145)
This commit is contained in:
parent
d15ea5178c
commit
247a808769
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]},
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
Loading…
Reference in New Issue