Skip to content

Commit

Permalink
feat(inputs.modbus): Allow reading single bits of input and holding r…
Browse files Browse the repository at this point in the history
…egisters (#15648)
  • Loading branch information
srebhan authored Jul 29, 2024
1 parent f9900f8 commit fe7321e
Show file tree
Hide file tree
Showing 13 changed files with 204 additions and 45 deletions.
14 changes: 13 additions & 1 deletion plugins/inputs/modbus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,14 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## |---BA, DCBA - Little Endian
## |---BADC - Mid-Big Endian
## |---CDAB - Mid-Little Endian
## data_type - INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## data_type - BIT (single bit of a register)
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
## INT16, UINT16, INT32, UINT32, INT64, UINT64,
## 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)
## bit - (optional) bit of the register, ONLY valid for BIT type
## scale - the final numeric variable representation
## address - variable address

Expand Down Expand Up @@ -176,11 +178,13 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## address - address of the register to query. For coil and discrete inputs this is the bit address.
## name *1 - field name
## type *1,2 - type of the modbus field, can be
## BIT (single bit of a register)
## 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,2 - (optional) number of registers, ONLY valid for STRING type
## bit *1,2 - (optional) bit of the register, ONLY valid for BIT 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.
Expand Down Expand Up @@ -286,11 +290,13 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## 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
## BIT (single bit of a register)
## 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
## bit *1,2 - (optional) bit of the register, ONLY valid for BIT 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).
Expand Down Expand Up @@ -461,6 +467,12 @@ 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.

##### Bit: `BIT`

This type is used to query a single bit of a register specified in the `address`
setting and convert the value to an unsigned integer. This type __requires__ the
`bit` setting to be specified.

---

### `request` configuration style
Expand Down
2 changes: 1 addition & 1 deletion plugins/inputs/modbus/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func removeDuplicates(elements []uint16) []uint16 {

func normalizeInputDatatype(dataType string) (string, error) {
switch dataType {
case "INT8L", "INT8H", "UINT8L", "UINT8H",
case "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H",
"INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64",
"FLOAT16", "FLOAT32", "FLOAT64", "STRING":
return dataType, nil
Expand Down
20 changes: 17 additions & 3 deletions plugins/inputs/modbus/configuration_metric.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type metricFieldDefinition struct {
InputType string `toml:"type"`
Scale float64 `toml:"scale"`
OutputType string `toml:"output"`
Bit uint8 `toml:"bit"`
}

type metricDefinition struct {
Expand Down Expand Up @@ -116,19 +117,32 @@ func (c *ConfigurationPerMetric) Check() error {
if f.Length != 0 {
return fmt.Errorf("length option cannot be used for type %q of field %q", f.InputType, f.Name)
}
if f.Bit != 0 {
return fmt.Errorf("bit 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.Bit != 0 {
return fmt.Errorf("bit option cannot be used for type %q of field %q", f.InputType, 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)
}
case "BIT":
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)
}
default:
return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name)
}
Expand Down Expand Up @@ -315,7 +329,7 @@ func (c *ConfigurationPerMetric) newField(def metricFieldDefinition, mdef metric
return field{}, err
}

f.converter, err = determineConverter(inType, order, outType, def.Scale, c.workarounds.StringRegisterLocation)
f.converter, err = determineConverter(inType, order, outType, def.Scale, def.Bit, c.workarounds.StringRegisterLocation)
if err != nil {
return field{}, err
}
Expand Down Expand Up @@ -353,7 +367,7 @@ func (c *ConfigurationPerMetric) determineOutputDatatype(input string) (string,
switch input {
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
return "INT64", nil
case "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
case "BIT", "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
return "UINT64", nil
case "FLOAT16", "FLOAT32", "FLOAT64":
return "FLOAT64", nil
Expand All @@ -366,7 +380,7 @@ func (c *ConfigurationPerMetric) determineOutputDatatype(input string) (string,
func (c *ConfigurationPerMetric) determineFieldLength(input string, length uint16) (uint16, error) {
// Handle our special types
switch input {
case "INT8L", "INT8H", "UINT8L", "UINT8H":
case "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H":
return 1, nil
case "INT16", "UINT16", "FLOAT16":
return 1, nil
Expand Down
49 changes: 47 additions & 2 deletions plugins/inputs/modbus/configuration_metric_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,13 +255,43 @@ func TestMetricResult(t *testing.T) {
},
},
},
{
SlaveID: 1,
Measurement: "bitvalues",
Fields: []metricFieldDefinition{
{
Name: "bit 0",
Address: uint16(1),
InputType: "BIT",
Bit: 0,
},
{
Name: "bit 1",
Address: uint16(1),
InputType: "BIT",
Bit: 1,
},
{
Name: "bit 2",
Address: uint16(1),
InputType: "BIT",
Bit: 2,
},
{
Name: "bit 3",
Address: uint16(1),
InputType: "BIT",
Bit: 3,
},
},
},
}
require.NoError(t, plugin.Init())

// Check the generated requests
require.Len(t, plugin.requests, 1)
require.NotNil(t, plugin.requests[1])
require.Len(t, plugin.requests[1].holding, 1)
require.Len(t, plugin.requests[1].holding, 5)
require.Empty(t, plugin.requests[1].coil)
require.Empty(t, plugin.requests[1].discrete)
require.Empty(t, plugin.requests[1].input)
Expand Down Expand Up @@ -313,10 +343,25 @@ func TestMetricResult(t *testing.T) {
map[string]interface{}{"pi": float64(3.1415927410125732421875)},
time.Unix(0, 0),
),
metric.New(
"bitvalues",
map[string]string{
"name": "FAKEMETER",
"slave_id": "1",
"type": "holding_register",
},
map[string]interface{}{
"bit 0": uint64(0),
"bit 1": uint64(1),
"bit 2": uint64(0),
"bit 3": uint64(1),
},
time.Unix(0, 0),
),
}

actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
}

func TestMetricAddressOverflow(t *testing.T) {
Expand Down
75 changes: 43 additions & 32 deletions plugins/inputs/modbus/configuration_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type fieldDefinition struct {
DataType string `toml:"data_type"`
Scale float64 `toml:"scale"`
Address []uint16 `toml:"address"`
Bit uint8 `toml:"bit"`
}

type ConfigurationOriginal struct {
Expand Down Expand Up @@ -172,7 +173,7 @@ func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition, type
return f, err
}

f.converter, err = determineConverter(inType, byteOrder, outType, def.Scale, c.workarounds.StringRegisterLocation)
f.converter, err = determineConverter(inType, byteOrder, outType, def.Scale, def.Bit, c.workarounds.StringRegisterLocation)
if err != nil {
return f, err
}
Expand Down Expand Up @@ -213,7 +214,7 @@ 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)
}
case "STRING":
case "BIT", "STRING":
default:
return fmt.Errorf("invalid data type %q in %q - %q", item.DataType, registerType, item.Name)
}
Expand All @@ -226,42 +227,50 @@ 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)
// Special address checking for special types
switch item.DataType {
case "STRING":
continue
case "BIT":
if len(item.Address) != 1 {
return fmt.Errorf("address '%v' has length '%v' bit should be one in %q - %q", item.Address, len(item.Address), registerType, item.Name)
}
continue
}

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

// 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
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)
}

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,
)
}
// 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)
// 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)
}
}
return nil
Expand Down Expand Up @@ -308,6 +317,8 @@ func (c *ConfigurationOriginal) normalizeInputDatatype(dataType string, words in
return "FLOAT64", nil
case "STRING":
return "STRING", nil
case "BIT":
return "BIT", nil
}
return normalizeInputDatatype(dataType)
}
Expand Down
22 changes: 22 additions & 0 deletions plugins/inputs/modbus/configuration_register_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,33 @@ func TestRegisterHoldingRegisters(t *testing.T) {
name string
address []uint16
quantity uint16
bit uint8
byteOrder string
dataType string
scale float64
write []byte
read interface{}
}{
{
name: "register5_bit3",
address: []uint16{5},
quantity: 1,
byteOrder: "AB",
dataType: "BIT",
bit: 3,
write: []byte{0x18, 0x0d},
read: uint8(1),
},
{
name: "register5_bit14",
address: []uint16{5},
quantity: 1,
byteOrder: "AB",
dataType: "BIT",
bit: 14,
write: []byte{0x18, 0x0d},
read: uint8(0),
},
{
name: "register0_ab_float32",
address: []uint16{0},
Expand Down Expand Up @@ -888,6 +909,7 @@ func TestRegisterHoldingRegisters(t *testing.T) {
DataType: hrt.dataType,
Scale: hrt.scale,
Address: hrt.address,
Bit: hrt.bit,
},
}

Expand Down
Loading

0 comments on commit fe7321e

Please sign in to comment.