diff --git a/plugins/inputs/modbus/README.md b/plugins/inputs/modbus/README.md index 71c60dc0f5636..e49c84bf32408 100644 --- a/plugins/inputs/modbus/README.md +++ b/plugins/inputs/modbus/README.md @@ -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,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] @@ -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"}, @@ -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. diff --git a/plugins/inputs/modbus/configuration.go b/plugins/inputs/modbus/configuration.go index 0ddd71844aed5..0e40e79fbbd59 100644 --- a/plugins/inputs/modbus/configuration.go +++ b/plugins/inputs/modbus/configuration.go @@ -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) diff --git a/plugins/inputs/modbus/configuration_metric.go b/plugins/inputs/modbus/configuration_metric.go index abd534f7f3bd8..e406467091740 100644 --- a/plugins/inputs/modbus/configuration_metric.go +++ b/plugins/inputs/modbus/configuration_metric.go @@ -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 - 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" + } } } @@ -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) } diff --git a/plugins/inputs/modbus/configuration_metric_test.go b/plugins/inputs/modbus/configuration_metric_test.go index e3aac2c8ef920..7418ef189d7cb 100644 --- a/plugins/inputs/modbus/configuration_metric_test.go +++ b/plugins/inputs/modbus/configuration_metric_test.go @@ -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), ), diff --git a/plugins/inputs/modbus/configuration_register.go b/plugins/inputs/modbus/configuration_register.go index cfdf097862e07..8266e59b4b997 100644 --- a/plugins/inputs/modbus/configuration_register.go +++ b/plugins/inputs/modbus/configuration_register.go @@ -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 { @@ -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 @@ -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) } diff --git a/plugins/inputs/modbus/configuration_register_test.go b/plugins/inputs/modbus/configuration_register_test.go index 0b65a60a3fcb3..2d0cfcf9b9de4 100644 --- a/plugins/inputs/modbus/configuration_register_test.go +++ b/plugins/inputs/modbus/configuration_register_test.go @@ -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() diff --git a/plugins/inputs/modbus/configuration_request.go b/plugins/inputs/modbus/configuration_request.go index 3384ab75e2ab9..68d95a60d38fd 100644 --- a/plugins/inputs/modbus/configuration_request.go +++ b/plugins/inputs/modbus/configuration_request.go @@ -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 - 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" + } } } @@ -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) } diff --git a/plugins/inputs/modbus/configuration_request_test.go b/plugins/inputs/modbus/configuration_request_test.go index 5faf5d4f5a16d..86c6600c8ecd4 100644 --- a/plugins/inputs/modbus/configuration_request_test.go +++ b/plugins/inputs/modbus/configuration_request_test.go @@ -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,8 +1624,13 @@ 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)) - for i := len(hrt.write) - 1; i >= 0; i-- { - invert = append(invert, hrt.write[i]) + 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, }, }, }, diff --git a/plugins/inputs/modbus/sample_metric.conf b/plugins/inputs/modbus/sample_metric.conf index ff2d8abfbed03..5eb10979b4312 100644 --- a/plugins/inputs/modbus/sample_metric.conf +++ b/plugins/inputs/modbus/sample_metric.conf @@ -35,22 +35,25 @@ # 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"}, @@ -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 diff --git a/plugins/inputs/modbus/sample_register.conf b/plugins/inputs/modbus/sample_register.conf index fb4567b86882e..4da7aaffdc8a0 100644 --- a/plugins/inputs/modbus/sample_register.conf +++ b/plugins/inputs/modbus/sample_register.conf @@ -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]}, diff --git a/plugins/inputs/modbus/sample_request.conf b/plugins/inputs/modbus/sample_request.conf index adab9b8152d92..8500b547f4ee7 100644 --- a/plugins/inputs/modbus/sample_request.conf +++ b/plugins/inputs/modbus/sample_request.conf @@ -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,13 +67,15 @@ ## *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] diff --git a/plugins/inputs/modbus/type_conversions.go b/plugins/inputs/modbus/type_conversions.go index 795b549c4de21..f8e9f99ce8009 100644 --- a/plugins/inputs/modbus/type_conversions.go +++ b/plugins/inputs/modbus/type_conversions.go @@ -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) } diff --git a/plugins/inputs/modbus/type_conversions_string.go b/plugins/inputs/modbus/type_conversions_string.go new file mode 100644 index 0000000000000..8397ae9f0e41a --- /dev/null +++ b/plugins/inputs/modbus/type_conversions_string.go @@ -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 +}