diff --git a/plugins/inputs/modbus/README.md b/plugins/inputs/modbus/README.md index 70a12596c84d3..92aff653f940a 100644 --- a/plugins/inputs/modbus/README.md +++ b/plugins/inputs/modbus/README.md @@ -76,6 +76,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. ## Digital Variables, Discrete Inputs and Coils ## measurement - the (optional) measurement name, defaults to "modbus" ## name - the variable name + ## data_type - the (optional) output type, can be BOOL or UINT16 (default) ## address - variable address discrete_inputs = [ @@ -178,23 +179,24 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details. ## 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,2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if + ## 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). ## measurement *1 - (optional) measurement name, defaults to the setting of the request ## omit - (optional) omit this field. Useful to leave out single values when querying many registers ## with a single request. Defaults to "false". ## - ## *1: Those fields are ignored if field is omitted ("omit"=true) - ## - ## *2: Thise fields are ignored for both "coil" and "discrete"-input type of registers. For those register types - ## the fields are output as zero or one in UINT64 format by default. + ## *1: These fields are ignored if field is omitted ("omit"=true) + ## *2: These fields are ignored for both "coil" and "discrete"-input type of registers. + ## *3: This field can only be "UINT16" or "BOOL" if specified for both "coil" + ## and "discrete"-input type of registers. By default the fields are + ## output as zero or one in UINT16 format unless "BOOL" is used. ## 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"}, + { address=3, name="motor1_overheating", output="BOOL"}, ] [inputs.modbus.request.tags] @@ -320,6 +322,11 @@ floating-point-number. The size of the output type is assumed to be large enough for all supported input types. The mapping from the input type to the output type is fixed and cannot be configured. +##### Booleans: `BOOL` + +This type is only valid for _coil_ and _discrete_ registers. The value will be +`true` if the register has a non-zero (ON) value and `false` otherwise. + ##### Integers: `INT8L`, `INT8H`, `UINT8L`, `UINT8H` These types are used for 8-bit integer values. Select the one that matches your @@ -329,7 +336,7 @@ the register respectively. ##### Integers: `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64` These types are used for integer input values. Select the one that matches your -modbus data source. +modbus data source. For _coil_ and _discrete_ registers only `UINT16` is valid. ##### Floating Point: `FLOAT16-IEEE`, `FLOAT32-IEEE`, `FLOAT64-IEEE` @@ -512,10 +519,11 @@ non-zero value, the output type is `FLOAT64`. Otherwise, the output type corresponds to the register datatype _class_, i.e. `INT*` will result in `INT64`, `UINT*` in `UINT64` and `FLOAT*` in `FLOAT64`. -This setting is ignored if the field's `omit` is set to `true` or if the -`register` type is a bit-type (`coil` or `discrete`) and can be omitted in these -cases. For `coil` and `discrete` registers the field-value is output as zero or -one in `UINT16` format. +This setting is ignored if the field's `omit` is set to `true` and can be +omitted. In case the `register` type is a bit-type (`coil` or `discrete`) only +`UINT16` or `BOOL` are valid with the former being the default if omitted. +For `coil` and `discrete` registers the field-value is output as zero or one in +`UINT16` format or as `true` and `false` in `BOOL` format. #### per-field measurement setting diff --git a/plugins/inputs/modbus/configuration_register.go b/plugins/inputs/modbus/configuration_register.go index 914eedfb05708..d48c8b869e4cf 100644 --- a/plugins/inputs/modbus/configuration_register.go +++ b/plugins/inputs/modbus/configuration_register.go @@ -51,7 +51,7 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) { if !c.workarounds.OnRequestPerField { maxQuantity = maxQuantityCoils } - coil, err := c.initRequests(c.Coils, maxQuantity) + coil, err := c.initRequests(c.Coils, maxQuantity, false) if err != nil { return nil, err } @@ -59,7 +59,7 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) { if !c.workarounds.OnRequestPerField { maxQuantity = maxQuantityDiscreteInput } - discrete, err := c.initRequests(c.DiscreteInputs, maxQuantity) + discrete, err := c.initRequests(c.DiscreteInputs, maxQuantity, false) if err != nil { return nil, err } @@ -67,7 +67,7 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) { if !c.workarounds.OnRequestPerField { maxQuantity = maxQuantityHoldingRegisters } - holding, err := c.initRequests(c.HoldingRegisters, maxQuantity) + holding, err := c.initRequests(c.HoldingRegisters, maxQuantity, true) if err != nil { return nil, err } @@ -75,7 +75,7 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) { if !c.workarounds.OnRequestPerField { maxQuantity = maxQuantityInputRegisters } - input, err := c.initRequests(c.InputRegisters, maxQuantity) + input, err := c.initRequests(c.InputRegisters, maxQuantity, true) if err != nil { return nil, err } @@ -90,8 +90,8 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) { }, nil } -func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, maxQuantity uint16) ([]request, error) { - fields, err := c.initFields(fieldDefs) +func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, maxQuantity uint16, typed bool) ([]request, error) { + fields, err := c.initFields(fieldDefs, typed) if err != nil { return nil, err } @@ -104,11 +104,11 @@ func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, maxQua return groupFieldsToRequests(fields, params), nil } -func (c *ConfigurationOriginal) initFields(fieldDefs []fieldDefinition) ([]field, error) { +func (c *ConfigurationOriginal) initFields(fieldDefs []fieldDefinition, typed bool) ([]field, error) { // Construct the fields from the field definitions fields := make([]field, 0, len(fieldDefs)) for _, def := range fieldDefs { - f, err := c.newFieldFromDefinition(def) + f, err := c.newFieldFromDefinition(def, typed) if err != nil { return nil, fmt.Errorf("initializing field %q failed: %w", def.Name, err) } @@ -118,7 +118,7 @@ func (c *ConfigurationOriginal) initFields(fieldDefs []fieldDefinition) ([]field return fields, nil } -func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition) (field, error) { +func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition, typed bool) (field, error) { // Check if the addresses are consecutive expected := def.Address[0] for _, current := range def.Address[1:] { @@ -135,6 +135,17 @@ func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition) (fie address: def.Address[0], length: uint16(len(def.Address)), } + + // Handle coil and discrete registers which do have a limited datatype set + if !typed { + var err error + f.converter, err = determineUntypedConverter(def.DataType) + if err != nil { + return field{}, err + } + return f, nil + } + if def.DataType != "" { inType, err := c.normalizeInputDatatype(def.DataType, len(def.Address)) if err != nil { @@ -194,6 +205,13 @@ func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefini 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 { + case "", "UINT16", "BOOL": + default: + return fmt.Errorf("invalid data type %q in %q - %q", item.DataType, registerType, item.Name) + } } // check address diff --git a/plugins/inputs/modbus/configuration_request.go b/plugins/inputs/modbus/configuration_request.go index 7adf3571642f5..8ea039466b52d 100644 --- a/plugins/inputs/modbus/configuration_request.go +++ b/plugins/inputs/modbus/configuration_request.go @@ -102,9 +102,12 @@ func (c *ConfigurationPerRequest) Check() error { for fidx, f := range def.Fields { // Check the input type for all fields except the bit-field ones. // We later need the type (even for omitted fields) to determine the length. - if def.RegisterType == cHoldingRegisters || def.RegisterType == cInputRegisters { + if def.RegisterType == "holding" || def.RegisterType == "input" { switch f.InputType { - case "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64", "FLOAT32", "FLOAT64": + case "": + case "INT8L", "INT8H", "INT16", "INT32", "INT64": + case "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64": + case "FLOAT16", "FLOAT32", "FLOAT64": default: return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name) } @@ -120,14 +123,20 @@ func (c *ConfigurationPerRequest) Check() error { return fmt.Errorf("empty field name in request for slave %d", def.SlaveID) } - // Check fields only relevant for non-bit register types - if def.RegisterType == cHoldingRegisters || def.RegisterType == cInputRegisters { - // Check output type + // Check output type + if def.RegisterType == "holding" || def.RegisterType == "input" { switch f.OutputType { case "", "INT64", "UINT64", "FLOAT64": default: return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name) } + } else { + // Bit register types can only be UINT64 or BOOL + switch f.OutputType { + case "", "UINT16", "BOOL": + default: + return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name) + } } // Handle the default for measurement @@ -257,6 +266,14 @@ func (c *ConfigurationPerRequest) newFieldFromDefinition(def requestFieldDefinit omit: def.Omit, } + // Handle type conversions for coil and discrete registers + if !typed { + f.converter, err = determineUntypedConverter(def.OutputType) + if err != nil { + return field{}, err + } + } + // No more processing for un-typed (coil and discrete registers) or omitted fields if !typed || def.Omit { return f, nil diff --git a/plugins/inputs/modbus/modbus.go b/plugins/inputs/modbus/modbus.go index 28426af6e6f01..5e8b7cbc4a819 100644 --- a/plugins/inputs/modbus/modbus.go +++ b/plugins/inputs/modbus/modbus.go @@ -406,8 +406,9 @@ func (m *Modbus) gatherRequestsCoil(requests []request) error { idx := offset / 8 bit := offset % 8 - request.fields[i].value = uint16((bytes[idx] >> bit) & 0x01) - m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, (bytes[idx]>>bit)&0x01, request.fields[i].value) + v := (bytes[idx] >> bit) & 0x01 + request.fields[i].value = field.converter([]byte{v}) + m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, v, request.fields[i].value) } // Some (serial) devices require a pause between requests... @@ -432,8 +433,9 @@ func (m *Modbus) gatherRequestsDiscrete(requests []request) error { idx := offset / 8 bit := offset % 8 - request.fields[i].value = uint16((bytes[idx] >> bit) & 0x01) - m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, (bytes[idx]>>bit)&0x01, request.fields[i].value) + v := (bytes[idx] >> bit) & 0x01 + request.fields[i].value = field.converter([]byte{v}) + m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, v, request.fields[i].value) } // Some (serial) devices require a pause between requests... diff --git a/plugins/inputs/modbus/modbus_test.go b/plugins/inputs/modbus/modbus_test.go index e2866e0e966ee..cf40a05002784 100644 --- a/plugins/inputs/modbus/modbus_test.go +++ b/plugins/inputs/modbus/modbus_test.go @@ -21,6 +21,11 @@ import ( "github.com/influxdata/telegraf/testutil" ) +func TestMain(m *testing.M) { + telegraf.Debug = false + os.Exit(m.Run()) +} + func TestControllers(t *testing.T) { var tests = []struct { name string @@ -149,65 +154,96 @@ func TestCoils(t *testing.T) { var coilTests = []struct { name string address uint16 + dtype string quantity uint16 write []byte - read uint16 + read interface{} }{ { name: "coil0_turn_off", address: 0, quantity: 1, write: []byte{0x00}, - read: 0, + read: uint16(0), }, { name: "coil0_turn_on", address: 0, quantity: 1, write: []byte{0x01}, - read: 1, + read: uint16(1), }, { name: "coil1_turn_on", address: 1, quantity: 1, write: []byte{0x01}, - read: 1, + read: uint16(1), }, { name: "coil2_turn_on", address: 2, quantity: 1, write: []byte{0x01}, - read: 1, + read: uint16(1), }, { name: "coil3_turn_on", address: 3, quantity: 1, write: []byte{0x01}, - read: 1, + read: uint16(1), }, { name: "coil1_turn_off", address: 1, quantity: 1, write: []byte{0x00}, - read: 0, + read: uint16(0), }, { name: "coil2_turn_off", address: 2, quantity: 1, write: []byte{0x00}, - read: 0, + read: uint16(0), }, { name: "coil3_turn_off", address: 3, quantity: 1, write: []byte{0x00}, - read: 0, + read: uint16(0), + }, + { + name: "coil4_turn_off", + address: 4, + quantity: 1, + write: []byte{0x00}, + read: uint16(0), + }, + { + name: "coil4_turn_on", + address: 4, + quantity: 1, + write: []byte{0x01}, + read: uint16(1), + }, + { + name: "coil4_turn_off_bool", + address: 4, + quantity: 1, + dtype: "BOOL", + write: []byte{0x00}, + read: false, + }, + { + name: "coil4_turn_on_bool", + address: 4, + quantity: 1, + dtype: "BOOL", + write: []byte{0x01}, + read: true, }, } @@ -233,8 +269,9 @@ func TestCoils(t *testing.T) { modbus.SlaveID = 1 modbus.Coils = []fieldDefinition{ { - Name: ct.name, - Address: []uint16{ct.address}, + Name: ct.name, + Address: []uint16{ct.address}, + DataType: ct.dtype, }, } @@ -262,6 +299,101 @@ func TestCoils(t *testing.T) { } } +func TestRequestTypesCoil(t *testing.T) { + tests := []struct { + name string + address uint16 + dataTypeOut string + write uint16 + read interface{} + }{ + { + name: "coil-1-off", + address: 1, + write: 0, + read: uint16(0), + }, + { + name: "coil-2-on", + address: 2, + write: 0xFF00, + read: uint16(1), + }, + { + name: "coil-3-false", + address: 3, + dataTypeOut: "BOOL", + write: 0, + read: false, + }, + { + name: "coil-4-true", + address: 4, + dataTypeOut: "BOOL", + write: 0xFF00, + read: true, + }, + } + + serv := mbserver.NewServer() + require.NoError(t, serv.ListenTCP("localhost:1502")) + defer serv.Close() + + handler := mb.NewTCPClientHandler("localhost:1502") + require.NoError(t, handler.Connect()) + defer handler.Close() + client := mb.NewClient(handler) + + for _, hrt := range tests { + t.Run(hrt.name, func(t *testing.T) { + _, err := client.WriteSingleCoil(hrt.address, hrt.write) + require.NoError(t, err) + + modbus := Modbus{ + Name: "TestRequestTypesCoil", + Controller: "tcp://localhost:1502", + ConfigurationType: "request", + Log: testutil.Logger{}, + } + modbus.Requests = []requestDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + RegisterType: "coil", + Fields: []requestFieldDefinition{ + { + Name: hrt.name, + OutputType: hrt.dataTypeOut, + Address: hrt.address, + }, + }, + }, + } + + expected := []telegraf.Metric{ + testutil.MustMetric( + "modbus", + map[string]string{ + "type": cCoils, + "slave_id": "1", + "name": modbus.Name, + }, + map[string]interface{}{hrt.name: hrt.read}, + time.Unix(0, 0), + ), + } + + var acc testutil.Accumulator + require.NoError(t, modbus.Init()) + require.NotEmpty(t, modbus.requests) + require.NoError(t, modbus.Gather(&acc)) + acc.Wait(len(expected)) + + testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime()) + }) + } +} + func TestHoldingRegisters(t *testing.T) { var holdingRegisterTests = []struct { name string @@ -2651,7 +2783,15 @@ func TestConfigurationPerRequest(t *testing.T) { Address: uint16(2), InputType: "INT64", Scale: 1.2, - OutputType: "FLOAT64", + OutputType: "UINT16", + Measurement: "modbus", + }, + { + Name: "coil-3", + Address: uint16(3), + InputType: "INT64", + Scale: 1.2, + OutputType: "BOOL", Measurement: "modbus", }, }, @@ -2661,20 +2801,28 @@ func TestConfigurationPerRequest(t *testing.T) { RegisterType: "coil", Fields: []requestFieldDefinition{ { - Name: "coil-3", + Name: "coil-4", Address: uint16(6), }, { - Name: "coil-4", + Name: "coil-5", Address: uint16(7), Omit: true, }, { - Name: "coil-5", + Name: "coil-6", Address: uint16(8), InputType: "INT64", Scale: 1.2, - OutputType: "FLOAT64", + OutputType: "UINT16", + Measurement: "modbus", + }, + { + Name: "coil-7", + Address: uint16(9), + InputType: "INT64", + Scale: 1.2, + OutputType: "BOOL", Measurement: "modbus", }, }, @@ -2698,7 +2846,15 @@ func TestConfigurationPerRequest(t *testing.T) { Address: uint16(2), InputType: "INT64", Scale: 1.2, - OutputType: "FLOAT64", + OutputType: "UINT16", + Measurement: "modbus", + }, + { + Name: "discrete-3", + Address: uint16(3), + InputType: "INT64", + Scale: 1.2, + OutputType: "BOOL", Measurement: "modbus", }, }, @@ -2793,7 +2949,7 @@ func TestConfigurationPerRequestWithTags(t *testing.T) { Address: uint16(2), InputType: "INT64", Scale: 1.2, - OutputType: "FLOAT64", + OutputType: "UINT16", Measurement: "modbus", }, }, @@ -2821,7 +2977,7 @@ func TestConfigurationPerRequestWithTags(t *testing.T) { Address: uint16(8), InputType: "INT64", Scale: 1.2, - OutputType: "FLOAT64", + OutputType: "UINT16", Measurement: "modbus", }, }, @@ -2850,7 +3006,7 @@ func TestConfigurationPerRequestWithTags(t *testing.T) { Address: uint16(2), InputType: "INT64", Scale: 1.2, - OutputType: "FLOAT64", + OutputType: "UINT16", Measurement: "modbus", }, }, @@ -3151,7 +3307,7 @@ func TestConfigurationPerRequestFail(t *testing.T) { }, }, }, - errormsg: "cannot process configuration: initializing field \"holding-0\" failed: unknown output type \"UINT8\"", + errormsg: `configuration invalid: unknown output data-type "UINT8" for field "holding-0"`, }, { name: "duplicate fields (holding)", @@ -3263,7 +3419,7 @@ func TestConfigurationPerRequestFail(t *testing.T) { }, }, }, - errormsg: "cannot process configuration: initializing field \"input-0\" failed: unknown output type \"UINT8\"", + errormsg: `configuration invalid: unknown output data-type "UINT8" for field "input-0"`, }, { name: "duplicate fields (input)", diff --git a/plugins/inputs/modbus/sample_register.conf b/plugins/inputs/modbus/sample_register.conf index 4727f4096bc46..471b6716db229 100644 --- a/plugins/inputs/modbus/sample_register.conf +++ b/plugins/inputs/modbus/sample_register.conf @@ -6,6 +6,7 @@ ## Digital Variables, Discrete Inputs and Coils ## measurement - the (optional) measurement name, defaults to "modbus" ## name - the variable name + ## data_type - the (optional) output type, can be BOOL or UINT16 (default) ## address - variable address discrete_inputs = [ diff --git a/plugins/inputs/modbus/sample_request.conf b/plugins/inputs/modbus/sample_request.conf index ac0c2ead4e752..d48c612b786a0 100644 --- a/plugins/inputs/modbus/sample_request.conf +++ b/plugins/inputs/modbus/sample_request.conf @@ -57,23 +57,24 @@ ## 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,2 - (optional) type of resulting field, can be INT64, UINT64 or FLOAT64. Defaults to FLOAT64 if + ## 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). ## measurement *1 - (optional) measurement name, defaults to the setting of the request ## omit - (optional) omit this field. Useful to leave out single values when querying many registers ## with a single request. Defaults to "false". ## - ## *1: Those fields are ignored if field is omitted ("omit"=true) - ## - ## *2: Thise fields are ignored for both "coil" and "discrete"-input type of registers. For those register types - ## the fields are output as zero or one in UINT64 format by default. + ## *1: These fields are ignored if field is omitted ("omit"=true) + ## *2: These fields are ignored for both "coil" and "discrete"-input type of registers. + ## *3: This field can only be "UINT16" or "BOOL" if specified for both "coil" + ## and "discrete"-input type of registers. By default the fields are + ## output as zero or one in UINT16 format unless "BOOL" is used. ## 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"}, + { address=3, name="motor1_overheating", output="BOOL"}, ] [inputs.modbus.request.tags] diff --git a/plugins/inputs/modbus/type_conversions.go b/plugins/inputs/modbus/type_conversions.go index 2b763c87d4370..795b549c4de21 100644 --- a/plugins/inputs/modbus/type_conversions.go +++ b/plugins/inputs/modbus/type_conversions.go @@ -2,6 +2,20 @@ package modbus import "fmt" +func determineUntypedConverter(outType string) (fieldConverterFunc, error) { + switch outType { + case "", "UINT16": + return func(b []byte) interface{} { + return uint16(b[0]) + }, nil + case "BOOL": + return func(b []byte) interface{} { + return b[0] != 0 + }, nil + } + return nil, fmt.Errorf("invalid output data-type: %s", outType) +} + func determineConverter(inType, byteOrder, outType string, scale float64) (fieldConverterFunc, error) { if scale != 0.0 { return determineConverterScale(inType, byteOrder, outType, scale)