feat(inputs.modbus): Allow to convert coil and discrete registers to boolean (#12825)

This commit is contained in:
Sven Rebhan 2023-03-13 12:18:02 +01:00 committed by GitHub
parent 1eb70808d0
commit 2006086262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 274 additions and 57 deletions

View File

@ -76,6 +76,7 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## Digital Variables, Discrete Inputs and Coils ## Digital Variables, Discrete Inputs and Coils
## measurement - the (optional) measurement name, defaults to "modbus" ## measurement - the (optional) measurement name, defaults to "modbus"
## name - the variable name ## name - the variable name
## data_type - the (optional) output type, can be BOOL or UINT16 (default)
## address - variable address ## address - variable address
discrete_inputs = [ discrete_inputs = [
@ -178,23 +179,24 @@ See the [CONFIGURATION.md][CONFIGURATION.md] for more details.
## 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)
## scale *1,2 - (optional) factor to scale the variable with ## 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). ## "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 ## 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 ## omit - (optional) omit this field. Useful to leave out single values when querying many registers
## with a single request. Defaults to "false". ## with a single request. Defaults to "false".
## ##
## *1: Those fields are ignored if field is omitted ("omit"=true) ## *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.
## *2: Thise fields are ignored for both "coil" and "discrete"-input type of registers. For those register types ## *3: This field can only be "UINT16" or "BOOL" if specified for both "coil"
## the fields are output as zero or one in UINT64 format by default. ## 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 ## Coil / discrete input example
fields = [ fields = [
{ address=0, name="motor1_run"}, { address=0, name="motor1_run"},
{ address=1, name="jog", measurement="motor"}, { address=1, name="jog", measurement="motor"},
{ address=2, name="motor1_stop", omit=true}, { address=2, name="motor1_stop", omit=true},
{ address=3, name="motor1_overheating"}, { address=3, name="motor1_overheating", output="BOOL"},
] ]
[inputs.modbus.request.tags] [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 for all supported input types. The mapping from the input type to the output
type is fixed and cannot be configured. 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` ##### Integers: `INT8L`, `INT8H`, `UINT8L`, `UINT8H`
These types are used for 8-bit integer values. Select the one that matches your 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` ##### Integers: `INT16`, `UINT16`, `INT32`, `UINT32`, `INT64`, `UINT64`
These types are used for integer input values. Select the one that matches your 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` ##### 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 corresponds to the register datatype _class_, i.e. `INT*` will result in
`INT64`, `UINT*` in `UINT64` and `FLOAT*` in `FLOAT64`. `INT64`, `UINT*` in `UINT64` and `FLOAT*` in `FLOAT64`.
This setting is ignored if the field's `omit` is set to `true` or if the This setting is ignored if the field's `omit` is set to `true` and can be
`register` type is a bit-type (`coil` or `discrete`) and can be omitted in these omitted. In case the `register` type is a bit-type (`coil` or `discrete`) only
cases. For `coil` and `discrete` registers the field-value is output as zero or `UINT16` or `BOOL` are valid with the former being the default if omitted.
one in `UINT16` format. 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 #### per-field measurement setting

View File

@ -51,7 +51,7 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) {
if !c.workarounds.OnRequestPerField { if !c.workarounds.OnRequestPerField {
maxQuantity = maxQuantityCoils maxQuantity = maxQuantityCoils
} }
coil, err := c.initRequests(c.Coils, maxQuantity) coil, err := c.initRequests(c.Coils, maxQuantity, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -59,7 +59,7 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) {
if !c.workarounds.OnRequestPerField { if !c.workarounds.OnRequestPerField {
maxQuantity = maxQuantityDiscreteInput maxQuantity = maxQuantityDiscreteInput
} }
discrete, err := c.initRequests(c.DiscreteInputs, maxQuantity) discrete, err := c.initRequests(c.DiscreteInputs, maxQuantity, false)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -67,7 +67,7 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) {
if !c.workarounds.OnRequestPerField { if !c.workarounds.OnRequestPerField {
maxQuantity = maxQuantityHoldingRegisters maxQuantity = maxQuantityHoldingRegisters
} }
holding, err := c.initRequests(c.HoldingRegisters, maxQuantity) holding, err := c.initRequests(c.HoldingRegisters, maxQuantity, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -75,7 +75,7 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) {
if !c.workarounds.OnRequestPerField { if !c.workarounds.OnRequestPerField {
maxQuantity = maxQuantityInputRegisters maxQuantity = maxQuantityInputRegisters
} }
input, err := c.initRequests(c.InputRegisters, maxQuantity) input, err := c.initRequests(c.InputRegisters, maxQuantity, true)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -90,8 +90,8 @@ func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) {
}, nil }, nil
} }
func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, maxQuantity uint16) ([]request, error) { func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, maxQuantity uint16, typed bool) ([]request, error) {
fields, err := c.initFields(fieldDefs) fields, err := c.initFields(fieldDefs, typed)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -104,11 +104,11 @@ func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, maxQua
return groupFieldsToRequests(fields, params), nil 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 // Construct the fields from the field definitions
fields := make([]field, 0, len(fieldDefs)) fields := make([]field, 0, len(fieldDefs))
for _, def := range fieldDefs { for _, def := range fieldDefs {
f, err := c.newFieldFromDefinition(def) f, err := c.newFieldFromDefinition(def, typed)
if err != nil { if err != nil {
return nil, fmt.Errorf("initializing field %q failed: %w", def.Name, err) 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 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 // Check if the addresses are consecutive
expected := def.Address[0] expected := def.Address[0]
for _, current := range def.Address[1:] { for _, current := range def.Address[1:] {
@ -135,6 +135,17 @@ func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition) (fie
address: def.Address[0], address: def.Address[0],
length: uint16(len(def.Address)), 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 != "" { if def.DataType != "" {
inType, err := c.normalizeInputDatatype(def.DataType, len(def.Address)) inType, err := c.normalizeInputDatatype(def.DataType, len(def.Address))
if err != nil { if err != nil {
@ -194,6 +205,13 @@ 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)
} }
} 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 // check address

View File

@ -102,9 +102,12 @@ func (c *ConfigurationPerRequest) Check() error {
for fidx, f := range def.Fields { for fidx, f := range def.Fields {
// Check the input type for all fields except the bit-field ones. // 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. // 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 { 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: 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)
} }
@ -120,14 +123,20 @@ func (c *ConfigurationPerRequest) Check() error {
return fmt.Errorf("empty field name in request for slave %d", def.SlaveID) 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 { switch f.OutputType {
case "", "INT64", "UINT64", "FLOAT64": case "", "INT64", "UINT64", "FLOAT64":
default: default:
return fmt.Errorf("unknown output data-type %q for field %q", f.OutputType, f.Name) 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 // Handle the default for measurement
@ -257,6 +266,14 @@ func (c *ConfigurationPerRequest) newFieldFromDefinition(def requestFieldDefinit
omit: def.Omit, 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 // No more processing for un-typed (coil and discrete registers) or omitted fields
if !typed || def.Omit { if !typed || def.Omit {
return f, nil return f, nil

View File

@ -406,8 +406,9 @@ func (m *Modbus) gatherRequestsCoil(requests []request) error {
idx := offset / 8 idx := offset / 8
bit := offset % 8 bit := offset % 8
request.fields[i].value = uint16((bytes[idx] >> bit) & 0x01) v := (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) 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... // Some (serial) devices require a pause between requests...
@ -432,8 +433,9 @@ func (m *Modbus) gatherRequestsDiscrete(requests []request) error {
idx := offset / 8 idx := offset / 8
bit := offset % 8 bit := offset % 8
request.fields[i].value = uint16((bytes[idx] >> bit) & 0x01) v := (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) 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... // Some (serial) devices require a pause between requests...

View File

@ -21,6 +21,11 @@ import (
"github.com/influxdata/telegraf/testutil" "github.com/influxdata/telegraf/testutil"
) )
func TestMain(m *testing.M) {
telegraf.Debug = false
os.Exit(m.Run())
}
func TestControllers(t *testing.T) { func TestControllers(t *testing.T) {
var tests = []struct { var tests = []struct {
name string name string
@ -149,65 +154,96 @@ func TestCoils(t *testing.T) {
var coilTests = []struct { var coilTests = []struct {
name string name string
address uint16 address uint16
dtype string
quantity uint16 quantity uint16
write []byte write []byte
read uint16 read interface{}
}{ }{
{ {
name: "coil0_turn_off", name: "coil0_turn_off",
address: 0, address: 0,
quantity: 1, quantity: 1,
write: []byte{0x00}, write: []byte{0x00},
read: 0, read: uint16(0),
}, },
{ {
name: "coil0_turn_on", name: "coil0_turn_on",
address: 0, address: 0,
quantity: 1, quantity: 1,
write: []byte{0x01}, write: []byte{0x01},
read: 1, read: uint16(1),
}, },
{ {
name: "coil1_turn_on", name: "coil1_turn_on",
address: 1, address: 1,
quantity: 1, quantity: 1,
write: []byte{0x01}, write: []byte{0x01},
read: 1, read: uint16(1),
}, },
{ {
name: "coil2_turn_on", name: "coil2_turn_on",
address: 2, address: 2,
quantity: 1, quantity: 1,
write: []byte{0x01}, write: []byte{0x01},
read: 1, read: uint16(1),
}, },
{ {
name: "coil3_turn_on", name: "coil3_turn_on",
address: 3, address: 3,
quantity: 1, quantity: 1,
write: []byte{0x01}, write: []byte{0x01},
read: 1, read: uint16(1),
}, },
{ {
name: "coil1_turn_off", name: "coil1_turn_off",
address: 1, address: 1,
quantity: 1, quantity: 1,
write: []byte{0x00}, write: []byte{0x00},
read: 0, read: uint16(0),
}, },
{ {
name: "coil2_turn_off", name: "coil2_turn_off",
address: 2, address: 2,
quantity: 1, quantity: 1,
write: []byte{0x00}, write: []byte{0x00},
read: 0, read: uint16(0),
}, },
{ {
name: "coil3_turn_off", name: "coil3_turn_off",
address: 3, address: 3,
quantity: 1, quantity: 1,
write: []byte{0x00}, 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,
}, },
} }
@ -235,6 +271,7 @@ func TestCoils(t *testing.T) {
{ {
Name: ct.name, Name: ct.name,
Address: []uint16{ct.address}, 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) { func TestHoldingRegisters(t *testing.T) {
var holdingRegisterTests = []struct { var holdingRegisterTests = []struct {
name string name string
@ -2651,7 +2783,15 @@ func TestConfigurationPerRequest(t *testing.T) {
Address: uint16(2), Address: uint16(2),
InputType: "INT64", InputType: "INT64",
Scale: 1.2, Scale: 1.2,
OutputType: "FLOAT64", OutputType: "UINT16",
Measurement: "modbus",
},
{
Name: "coil-3",
Address: uint16(3),
InputType: "INT64",
Scale: 1.2,
OutputType: "BOOL",
Measurement: "modbus", Measurement: "modbus",
}, },
}, },
@ -2661,20 +2801,28 @@ func TestConfigurationPerRequest(t *testing.T) {
RegisterType: "coil", RegisterType: "coil",
Fields: []requestFieldDefinition{ Fields: []requestFieldDefinition{
{ {
Name: "coil-3", Name: "coil-4",
Address: uint16(6), Address: uint16(6),
}, },
{ {
Name: "coil-4", Name: "coil-5",
Address: uint16(7), Address: uint16(7),
Omit: true, Omit: true,
}, },
{ {
Name: "coil-5", Name: "coil-6",
Address: uint16(8), Address: uint16(8),
InputType: "INT64", InputType: "INT64",
Scale: 1.2, Scale: 1.2,
OutputType: "FLOAT64", OutputType: "UINT16",
Measurement: "modbus",
},
{
Name: "coil-7",
Address: uint16(9),
InputType: "INT64",
Scale: 1.2,
OutputType: "BOOL",
Measurement: "modbus", Measurement: "modbus",
}, },
}, },
@ -2698,7 +2846,15 @@ func TestConfigurationPerRequest(t *testing.T) {
Address: uint16(2), Address: uint16(2),
InputType: "INT64", InputType: "INT64",
Scale: 1.2, Scale: 1.2,
OutputType: "FLOAT64", OutputType: "UINT16",
Measurement: "modbus",
},
{
Name: "discrete-3",
Address: uint16(3),
InputType: "INT64",
Scale: 1.2,
OutputType: "BOOL",
Measurement: "modbus", Measurement: "modbus",
}, },
}, },
@ -2793,7 +2949,7 @@ func TestConfigurationPerRequestWithTags(t *testing.T) {
Address: uint16(2), Address: uint16(2),
InputType: "INT64", InputType: "INT64",
Scale: 1.2, Scale: 1.2,
OutputType: "FLOAT64", OutputType: "UINT16",
Measurement: "modbus", Measurement: "modbus",
}, },
}, },
@ -2821,7 +2977,7 @@ func TestConfigurationPerRequestWithTags(t *testing.T) {
Address: uint16(8), Address: uint16(8),
InputType: "INT64", InputType: "INT64",
Scale: 1.2, Scale: 1.2,
OutputType: "FLOAT64", OutputType: "UINT16",
Measurement: "modbus", Measurement: "modbus",
}, },
}, },
@ -2850,7 +3006,7 @@ func TestConfigurationPerRequestWithTags(t *testing.T) {
Address: uint16(2), Address: uint16(2),
InputType: "INT64", InputType: "INT64",
Scale: 1.2, Scale: 1.2,
OutputType: "FLOAT64", OutputType: "UINT16",
Measurement: "modbus", 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)", 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)", name: "duplicate fields (input)",

View File

@ -6,6 +6,7 @@
## Digital Variables, Discrete Inputs and Coils ## Digital Variables, Discrete Inputs and Coils
## measurement - the (optional) measurement name, defaults to "modbus" ## measurement - the (optional) measurement name, defaults to "modbus"
## name - the variable name ## name - the variable name
## data_type - the (optional) output type, can be BOOL or UINT16 (default)
## address - variable address ## address - variable address
discrete_inputs = [ discrete_inputs = [

View File

@ -57,23 +57,24 @@
## 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)
## scale *1,2 - (optional) factor to scale the variable with ## 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). ## "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 ## 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 ## omit - (optional) omit this field. Useful to leave out single values when querying many registers
## with a single request. Defaults to "false". ## with a single request. Defaults to "false".
## ##
## *1: Those fields are ignored if field is omitted ("omit"=true) ## *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.
## *2: Thise fields are ignored for both "coil" and "discrete"-input type of registers. For those register types ## *3: This field can only be "UINT16" or "BOOL" if specified for both "coil"
## the fields are output as zero or one in UINT64 format by default. ## 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 ## Coil / discrete input example
fields = [ fields = [
{ address=0, name="motor1_run"}, { address=0, name="motor1_run"},
{ address=1, name="jog", measurement="motor"}, { address=1, name="jog", measurement="motor"},
{ address=2, name="motor1_stop", omit=true}, { address=2, name="motor1_stop", omit=true},
{ address=3, name="motor1_overheating"}, { address=3, name="motor1_overheating", output="BOOL"},
] ]
[inputs.modbus.request.tags] [inputs.modbus.request.tags]

View File

@ -2,6 +2,20 @@ package modbus
import "fmt" 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) { func determineConverter(inType, byteOrder, outType string, scale float64) (fieldConverterFunc, error) {
if scale != 0.0 { if scale != 0.0 {
return determineConverterScale(inType, byteOrder, outType, scale) return determineConverterScale(inType, byteOrder, outType, scale)