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 to convert coil and discrete registers to boolean #12825

Merged
merged 3 commits into from
Mar 13, 2023
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
30 changes: 19 additions & 11 deletions plugins/inputs/modbus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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`

Expand Down Expand Up @@ -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

Expand Down
36 changes: 27 additions & 9 deletions plugins/inputs/modbus/configuration_register.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,31 +51,31 @@ 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
}

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
}

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
}

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
}
Expand All @@ -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
}
Expand All @@ -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)
}
Expand All @@ -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:] {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
27 changes: 22 additions & 5 deletions plugins/inputs/modbus/configuration_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions plugins/inputs/modbus/modbus.go
Original file line number Diff line number Diff line change
Expand Up @@ -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...
Expand All @@ -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...
Expand Down
Loading