Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inputs.modbus): Allow reading single bits of input and holding registers #15648

Merged
merged 1 commit into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading