feat(inputs.modbus): Allow reading single bits of input and holding registers (#15648)
This commit is contained in:
parent
f9900f8787
commit
fe7321e35b
|
|
@ -101,12 +101,14 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
|
||||||
## |---BA, DCBA - Little Endian
|
## |---BA, DCBA - Little Endian
|
||||||
## |---BADC - Mid-Big Endian
|
## |---BADC - Mid-Big Endian
|
||||||
## |---CDAB - Mid-Little 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,
|
## INT16, UINT16, INT32, UINT32, INT64, UINT64,
|
||||||
## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation)
|
## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation)
|
||||||
## FIXED, UFIXED (fixed-point representation on input)
|
## FIXED, UFIXED (fixed-point representation on input)
|
||||||
## FLOAT32 is a deprecated alias for UFIXED for historic reasons, should be avoided
|
## FLOAT32 is a deprecated alias for UFIXED for historic reasons, should be avoided
|
||||||
## STRING (byte-sequence converted to string)
|
## STRING (byte-sequence converted to string)
|
||||||
|
## bit - (optional) bit of the register, ONLY valid for BIT type
|
||||||
## scale - the final numeric variable representation
|
## scale - the final numeric variable representation
|
||||||
## address - variable address
|
## address - variable address
|
||||||
|
|
||||||
|
|
@ -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.
|
## address - address of the register to query. For coil and discrete inputs this is the bit address.
|
||||||
## name *1 - field name
|
## name *1 - field name
|
||||||
## type *1,2 - type of the modbus field, can be
|
## 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)
|
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
|
||||||
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
|
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
|
||||||
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
|
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
|
||||||
## STRING (byte-sequence converted to string)
|
## STRING (byte-sequence converted to string)
|
||||||
## length *1,2 - (optional) number of registers, ONLY valid for STRING type
|
## 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
|
## 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.
|
## 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.
|
## Defaults to FLOAT64 for numeric fields if "scale" is provided.
|
||||||
|
|
@ -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.
|
## address - address of the register to query. For coil and discrete inputs this is the bit address.
|
||||||
## name - field name
|
## name - field name
|
||||||
## type *1 - type of the modbus field, can be
|
## type *1 - type of the modbus field, can be
|
||||||
|
## BIT (single bit of a register)
|
||||||
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
|
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
|
||||||
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
|
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
|
||||||
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
|
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
|
||||||
## STRING (byte-sequence converted to string)
|
## STRING (byte-sequence converted to string)
|
||||||
## length *1 - (optional) number of registers, ONLY valid for STRING type
|
## 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
|
## 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
|
## 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).
|
## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc).
|
||||||
|
|
@ -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.
|
byte-sequence contains a `null` byte, the string is truncated at this position.
|
||||||
You cannot use the `scale` setting for string fields.
|
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
|
### `request` configuration style
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ func removeDuplicates(elements []uint16) []uint16 {
|
||||||
|
|
||||||
func normalizeInputDatatype(dataType string) (string, error) {
|
func normalizeInputDatatype(dataType string) (string, error) {
|
||||||
switch dataType {
|
switch dataType {
|
||||||
case "INT8L", "INT8H", "UINT8L", "UINT8H",
|
case "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H",
|
||||||
"INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64",
|
"INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64",
|
||||||
"FLOAT16", "FLOAT32", "FLOAT64", "STRING":
|
"FLOAT16", "FLOAT32", "FLOAT64", "STRING":
|
||||||
return dataType, nil
|
return dataType, nil
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ type metricFieldDefinition struct {
|
||||||
InputType string `toml:"type"`
|
InputType string `toml:"type"`
|
||||||
Scale float64 `toml:"scale"`
|
Scale float64 `toml:"scale"`
|
||||||
OutputType string `toml:"output"`
|
OutputType string `toml:"output"`
|
||||||
|
Bit uint8 `toml:"bit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type metricDefinition struct {
|
type metricDefinition struct {
|
||||||
|
|
@ -116,6 +117,9 @@ func (c *ConfigurationPerMetric) Check() error {
|
||||||
if f.Length != 0 {
|
if f.Length != 0 {
|
||||||
return fmt.Errorf("length option cannot be used for type %q of field %q", f.InputType, f.Name)
|
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" {
|
if f.OutputType == "STRING" {
|
||||||
return fmt.Errorf("cannot output field %q as string", f.Name)
|
return fmt.Errorf("cannot output field %q as string", f.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -123,12 +127,22 @@ func (c *ConfigurationPerMetric) Check() error {
|
||||||
if f.Length < 1 {
|
if f.Length < 1 {
|
||||||
return fmt.Errorf("missing length for string field %q", f.Name)
|
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 {
|
if f.Scale != 0.0 {
|
||||||
return fmt.Errorf("scale option cannot be used for string field %q", f.Name)
|
return fmt.Errorf("scale option cannot be used for string field %q", f.Name)
|
||||||
}
|
}
|
||||||
if f.OutputType != "" && f.OutputType != "STRING" {
|
if f.OutputType != "" && f.OutputType != "STRING" {
|
||||||
return fmt.Errorf("invalid output type %q for string field %q", f.OutputType, f.Name)
|
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:
|
default:
|
||||||
return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name)
|
return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -315,7 +329,7 @@ func (c *ConfigurationPerMetric) newField(def metricFieldDefinition, mdef metric
|
||||||
return field{}, err
|
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 {
|
if err != nil {
|
||||||
return field{}, err
|
return field{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -353,7 +367,7 @@ func (c *ConfigurationPerMetric) determineOutputDatatype(input string) (string,
|
||||||
switch input {
|
switch input {
|
||||||
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
|
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
|
||||||
return "INT64", nil
|
return "INT64", nil
|
||||||
case "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
|
case "BIT", "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
|
||||||
return "UINT64", nil
|
return "UINT64", nil
|
||||||
case "FLOAT16", "FLOAT32", "FLOAT64":
|
case "FLOAT16", "FLOAT32", "FLOAT64":
|
||||||
return "FLOAT64", nil
|
return "FLOAT64", nil
|
||||||
|
|
@ -366,7 +380,7 @@ func (c *ConfigurationPerMetric) determineOutputDatatype(input string) (string,
|
||||||
func (c *ConfigurationPerMetric) determineFieldLength(input string, length uint16) (uint16, error) {
|
func (c *ConfigurationPerMetric) determineFieldLength(input string, length uint16) (uint16, error) {
|
||||||
// Handle our special types
|
// Handle our special types
|
||||||
switch input {
|
switch input {
|
||||||
case "INT8L", "INT8H", "UINT8L", "UINT8H":
|
case "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H":
|
||||||
return 1, nil
|
return 1, nil
|
||||||
case "INT16", "UINT16", "FLOAT16":
|
case "INT16", "UINT16", "FLOAT16":
|
||||||
return 1, nil
|
return 1, nil
|
||||||
|
|
|
||||||
|
|
@ -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())
|
require.NoError(t, plugin.Init())
|
||||||
|
|
||||||
// Check the generated requests
|
// Check the generated requests
|
||||||
require.Len(t, plugin.requests, 1)
|
require.Len(t, plugin.requests, 1)
|
||||||
require.NotNil(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].coil)
|
||||||
require.Empty(t, plugin.requests[1].discrete)
|
require.Empty(t, plugin.requests[1].discrete)
|
||||||
require.Empty(t, plugin.requests[1].input)
|
require.Empty(t, plugin.requests[1].input)
|
||||||
|
|
@ -313,10 +343,25 @@ func TestMetricResult(t *testing.T) {
|
||||||
map[string]interface{}{"pi": float64(3.1415927410125732421875)},
|
map[string]interface{}{"pi": float64(3.1415927410125732421875)},
|
||||||
time.Unix(0, 0),
|
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()
|
actual := acc.GetTelegrafMetrics()
|
||||||
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime())
|
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMetricAddressOverflow(t *testing.T) {
|
func TestMetricAddressOverflow(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ type fieldDefinition struct {
|
||||||
DataType string `toml:"data_type"`
|
DataType string `toml:"data_type"`
|
||||||
Scale float64 `toml:"scale"`
|
Scale float64 `toml:"scale"`
|
||||||
Address []uint16 `toml:"address"`
|
Address []uint16 `toml:"address"`
|
||||||
|
Bit uint8 `toml:"bit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigurationOriginal struct {
|
type ConfigurationOriginal struct {
|
||||||
|
|
@ -172,7 +173,7 @@ func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition, type
|
||||||
return f, err
|
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 {
|
if err != nil {
|
||||||
return f, err
|
return f, err
|
||||||
}
|
}
|
||||||
|
|
@ -213,7 +214,7 @@ func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefini
|
||||||
if item.Scale == 0.0 {
|
if item.Scale == 0.0 {
|
||||||
return fmt.Errorf("invalid scale '%f' in %q - %q", item.Scale, registerType, item.Name)
|
return fmt.Errorf("invalid scale '%f' in %q - %q", item.Scale, registerType, item.Name)
|
||||||
}
|
}
|
||||||
case "STRING":
|
case "BIT", "STRING":
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid data type %q in %q - %q", item.DataType, registerType, item.Name)
|
return fmt.Errorf("invalid data type %q in %q - %q", item.DataType, registerType, item.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -226,42 +227,50 @@ func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefini
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check address
|
// Special address checking for special types
|
||||||
if item.DataType != "STRING" {
|
switch item.DataType {
|
||||||
if len(item.Address) != 1 && len(item.Address) != 2 && len(item.Address) != 4 {
|
case "STRING":
|
||||||
return fmt.Errorf("invalid address '%v' length '%v' in %q - %q", item.Address, len(item.Address), registerType, item.Name)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
if registerType == cInputRegisters || registerType == cHoldingRegisters {
|
// Check for the request size corresponding to the data-type
|
||||||
if 2*len(item.Address) != len(item.ByteOrder) {
|
var requiredAddresses int
|
||||||
return fmt.Errorf("invalid byte order %q and address '%v' in %q - %q", item.ByteOrder, item.Address, registerType, item.Name)
|
switch item.DataType {
|
||||||
}
|
case "INT8L", "INT8H", "UINT8L", "UINT8H", "UINT16", "INT16", "FLOAT16-IEEE":
|
||||||
|
requiredAddresses = 1
|
||||||
// Check for the request size corresponding to the data-type
|
case "UINT32", "INT32", "FLOAT32-IEEE":
|
||||||
var requiredAddresses int
|
requiredAddresses = 2
|
||||||
switch item.DataType {
|
case "UINT64", "INT64", "FLOAT64-IEEE":
|
||||||
case "INT8L", "INT8H", "UINT8L", "UINT8H", "UINT16", "INT16", "FLOAT16-IEEE":
|
requiredAddresses = 4
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -308,6 +317,8 @@ func (c *ConfigurationOriginal) normalizeInputDatatype(dataType string, words in
|
||||||
return "FLOAT64", nil
|
return "FLOAT64", nil
|
||||||
case "STRING":
|
case "STRING":
|
||||||
return "STRING", nil
|
return "STRING", nil
|
||||||
|
case "BIT":
|
||||||
|
return "BIT", nil
|
||||||
}
|
}
|
||||||
return normalizeInputDatatype(dataType)
|
return normalizeInputDatatype(dataType)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -215,12 +215,33 @@ func TestRegisterHoldingRegisters(t *testing.T) {
|
||||||
name string
|
name string
|
||||||
address []uint16
|
address []uint16
|
||||||
quantity uint16
|
quantity uint16
|
||||||
|
bit uint8
|
||||||
byteOrder string
|
byteOrder string
|
||||||
dataType string
|
dataType string
|
||||||
scale float64
|
scale float64
|
||||||
write []byte
|
write []byte
|
||||||
read interface{}
|
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",
|
name: "register0_ab_float32",
|
||||||
address: []uint16{0},
|
address: []uint16{0},
|
||||||
|
|
@ -888,6 +909,7 @@ func TestRegisterHoldingRegisters(t *testing.T) {
|
||||||
DataType: hrt.dataType,
|
DataType: hrt.dataType,
|
||||||
Scale: hrt.scale,
|
Scale: hrt.scale,
|
||||||
Address: hrt.address,
|
Address: hrt.address,
|
||||||
|
Bit: hrt.bit,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ type requestFieldDefinition struct {
|
||||||
OutputType string `toml:"output"`
|
OutputType string `toml:"output"`
|
||||||
Measurement string `toml:"measurement"`
|
Measurement string `toml:"measurement"`
|
||||||
Omit bool `toml:"omit"`
|
Omit bool `toml:"omit"`
|
||||||
|
Bit uint8 `toml:"bit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type requestDefinition struct {
|
type requestDefinition struct {
|
||||||
|
|
@ -135,6 +136,9 @@ func (c *ConfigurationPerRequest) Check() error {
|
||||||
if f.Length != 0 {
|
if f.Length != 0 {
|
||||||
return fmt.Errorf("length option cannot be used for type %q of field %q", f.InputType, f.Name)
|
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" {
|
if f.OutputType == "STRING" {
|
||||||
return fmt.Errorf("cannot output field %q as string", f.Name)
|
return fmt.Errorf("cannot output field %q as string", f.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -142,12 +146,22 @@ func (c *ConfigurationPerRequest) Check() error {
|
||||||
if f.Length < 1 {
|
if f.Length < 1 {
|
||||||
return fmt.Errorf("missing length for string field %q", f.Name)
|
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 {
|
if f.Scale != 0.0 {
|
||||||
return fmt.Errorf("scale option cannot be used for string field %q", f.Name)
|
return fmt.Errorf("scale option cannot be used for string field %q", f.Name)
|
||||||
}
|
}
|
||||||
if f.OutputType != "" && f.OutputType != "STRING" {
|
if f.OutputType != "" && f.OutputType != "STRING" {
|
||||||
return fmt.Errorf("invalid output type %q for string field %q", f.OutputType, f.Name)
|
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:
|
default:
|
||||||
return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name)
|
return fmt.Errorf("unknown register data-type %q for field %q", f.InputType, f.Name)
|
||||||
}
|
}
|
||||||
|
|
@ -361,7 +375,7 @@ func (c *ConfigurationPerRequest) newFieldFromDefinition(def requestFieldDefinit
|
||||||
return field{}, err
|
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 {
|
if err != nil {
|
||||||
return field{}, err
|
return field{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -399,7 +413,7 @@ func (c *ConfigurationPerRequest) determineOutputDatatype(input string) (string,
|
||||||
switch input {
|
switch input {
|
||||||
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
|
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
|
||||||
return "INT64", nil
|
return "INT64", nil
|
||||||
case "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
|
case "BIT", "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
|
||||||
return "UINT64", nil
|
return "UINT64", nil
|
||||||
case "FLOAT16", "FLOAT32", "FLOAT64":
|
case "FLOAT16", "FLOAT32", "FLOAT64":
|
||||||
return "FLOAT64", nil
|
return "FLOAT64", nil
|
||||||
|
|
@ -412,7 +426,7 @@ func (c *ConfigurationPerRequest) determineOutputDatatype(input string) (string,
|
||||||
func (c *ConfigurationPerRequest) determineFieldLength(input string, length uint16) (uint16, error) {
|
func (c *ConfigurationPerRequest) determineFieldLength(input string, length uint16) (uint16, error) {
|
||||||
// Handle our special types
|
// Handle our special types
|
||||||
switch input {
|
switch input {
|
||||||
case "INT8L", "INT8H", "UINT8L", "UINT8H":
|
case "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H":
|
||||||
return 1, nil
|
return 1, nil
|
||||||
case "INT16", "UINT16", "FLOAT16":
|
case "INT16", "UINT16", "FLOAT16":
|
||||||
return 1, nil
|
return 1, nil
|
||||||
|
|
|
||||||
|
|
@ -458,6 +458,7 @@ func TestRequestTypesHoldingABCD(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
address uint16
|
address uint16
|
||||||
|
bit uint8
|
||||||
length uint16
|
length uint16
|
||||||
byteOrder string
|
byteOrder string
|
||||||
dataTypeIn string
|
dataTypeIn string
|
||||||
|
|
@ -466,6 +467,22 @@ func TestRequestTypesHoldingABCD(t *testing.T) {
|
||||||
write []byte
|
write []byte
|
||||||
read interface{}
|
read interface{}
|
||||||
}{
|
}{
|
||||||
|
{
|
||||||
|
name: "register5_bit3",
|
||||||
|
address: 5,
|
||||||
|
dataTypeIn: "BIT",
|
||||||
|
bit: 3,
|
||||||
|
write: []byte{0x18, 0x0d},
|
||||||
|
read: uint8(1),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "register5_bit14",
|
||||||
|
address: 5,
|
||||||
|
dataTypeIn: "BIT",
|
||||||
|
bit: 14,
|
||||||
|
write: []byte{0x18, 0x0d},
|
||||||
|
read: uint8(0),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "register10_uint8L",
|
name: "register10_uint8L",
|
||||||
address: 10,
|
address: 10,
|
||||||
|
|
@ -1035,6 +1052,7 @@ func TestRequestTypesHoldingABCD(t *testing.T) {
|
||||||
Scale: hrt.scale,
|
Scale: hrt.scale,
|
||||||
Address: hrt.address,
|
Address: hrt.address,
|
||||||
Length: hrt.length,
|
Length: hrt.length,
|
||||||
|
Bit: hrt.bit,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -40,11 +40,13 @@
|
||||||
## address - address of the register to query. For coil and discrete inputs this is the bit address.
|
## address - address of the register to query. For coil and discrete inputs this is the bit address.
|
||||||
## name - field name
|
## name - field name
|
||||||
## type *1 - type of the modbus field, can be
|
## type *1 - type of the modbus field, can be
|
||||||
|
## BIT (single bit of a register)
|
||||||
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
|
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
|
||||||
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
|
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
|
||||||
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
|
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
|
||||||
## STRING (byte-sequence converted to string)
|
## STRING (byte-sequence converted to string)
|
||||||
## length *1 - (optional) number of registers, ONLY valid for STRING type
|
## 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
|
## 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
|
## 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).
|
## "scale" is provided and to the input "type" class otherwise (i.e. INT* -> INT64, etc).
|
||||||
|
|
|
||||||
|
|
@ -29,12 +29,14 @@
|
||||||
## |---BA, DCBA - Little Endian
|
## |---BA, DCBA - Little Endian
|
||||||
## |---BADC - Mid-Big Endian
|
## |---BADC - Mid-Big Endian
|
||||||
## |---CDAB - Mid-Little 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,
|
## INT16, UINT16, INT32, UINT32, INT64, UINT64,
|
||||||
## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation)
|
## FLOAT16-IEEE, FLOAT32-IEEE, FLOAT64-IEEE (IEEE 754 binary representation)
|
||||||
## FIXED, UFIXED (fixed-point representation on input)
|
## FIXED, UFIXED (fixed-point representation on input)
|
||||||
## FLOAT32 is a deprecated alias for UFIXED for historic reasons, should be avoided
|
## FLOAT32 is a deprecated alias for UFIXED for historic reasons, should be avoided
|
||||||
## STRING (byte-sequence converted to string)
|
## STRING (byte-sequence converted to string)
|
||||||
|
## bit - (optional) bit of the register, ONLY valid for BIT type
|
||||||
## scale - the final numeric variable representation
|
## scale - the final numeric variable representation
|
||||||
## address - variable address
|
## address - variable address
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,11 +49,13 @@
|
||||||
## address - address of the register to query. For coil and discrete inputs this is the bit address.
|
## address - address of the register to query. For coil and discrete inputs this is the bit address.
|
||||||
## name *1 - field name
|
## name *1 - field name
|
||||||
## type *1,2 - type of the modbus field, can be
|
## 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)
|
## INT8L, INT8H, UINT8L, UINT8H (low and high byte variants)
|
||||||
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
|
## INT16, UINT16, INT32, UINT32, INT64, UINT64 and
|
||||||
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
|
## FLOAT16, FLOAT32, FLOAT64 (IEEE 754 binary representation)
|
||||||
## STRING (byte-sequence converted to string)
|
## STRING (byte-sequence converted to string)
|
||||||
## length *1,2 - (optional) number of registers, ONLY valid for STRING type
|
## 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
|
## 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.
|
## 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.
|
## Defaults to FLOAT64 for numeric fields if "scale" is provided.
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,9 @@ func determineUntypedConverter(outType string) (fieldConverterFunc, error) {
|
||||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func determineConverter(inType, byteOrder, outType string, scale float64, strloc string) (fieldConverterFunc, error) {
|
func determineConverter(inType, byteOrder, outType string, scale float64, bit uint8, strloc string) (fieldConverterFunc, error) {
|
||||||
if inType == "STRING" {
|
switch inType {
|
||||||
|
case "STRING":
|
||||||
switch strloc {
|
switch strloc {
|
||||||
case "", "both":
|
case "", "both":
|
||||||
return determineConverterString(byteOrder)
|
return determineConverterString(byteOrder)
|
||||||
|
|
@ -28,6 +29,8 @@ func determineConverter(inType, byteOrder, outType string, scale float64, strloc
|
||||||
case "upper":
|
case "upper":
|
||||||
return determineConverterStringHigh(byteOrder)
|
return determineConverterStringHigh(byteOrder)
|
||||||
}
|
}
|
||||||
|
case "BIT":
|
||||||
|
return determineConverterBit(byteOrder, bit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if scale != 0.0 {
|
if scale != 0.0 {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package modbus
|
||||||
|
|
||||||
|
func determineConverterBit(byteOrder string, bit uint8) (fieldConverterFunc, error) {
|
||||||
|
tohost, err := endiannessConverter16(byteOrder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(b []byte) interface{} {
|
||||||
|
// Swap the bytes according to endianness
|
||||||
|
v := tohost(b)
|
||||||
|
return uint8(v >> bit & 0x01)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue