440 lines
13 KiB
Go
440 lines
13 KiB
Go
|
package modbus
|
||
|
|
||
|
import (
|
||
|
_ "embed"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"hash/maphash"
|
||
|
"math"
|
||
|
|
||
|
"github.com/influxdata/telegraf"
|
||
|
"github.com/influxdata/telegraf/config"
|
||
|
)
|
||
|
|
||
|
//go:embed sample_request.conf
|
||
|
var sampleConfigPartPerRequest string
|
||
|
|
||
|
type requestFieldDefinition struct {
|
||
|
Address uint16 `toml:"address"`
|
||
|
Name string `toml:"name"`
|
||
|
InputType string `toml:"type"`
|
||
|
Length uint16 `toml:"length"`
|
||
|
Scale float64 `toml:"scale"`
|
||
|
OutputType string `toml:"output"`
|
||
|
Measurement string `toml:"measurement"`
|
||
|
Omit bool `toml:"omit"`
|
||
|
Bit uint8 `toml:"bit"`
|
||
|
}
|
||
|
|
||
|
type requestDefinition struct {
|
||
|
SlaveID byte `toml:"slave_id"`
|
||
|
ByteOrder string `toml:"byte_order"`
|
||
|
RegisterType string `toml:"register"`
|
||
|
Measurement string `toml:"measurement"`
|
||
|
Optimization string `toml:"optimization"`
|
||
|
MaxExtraRegisters uint16 `toml:"optimization_max_register_fill"`
|
||
|
Fields []requestFieldDefinition `toml:"fields"`
|
||
|
Tags map[string]string `toml:"tags"`
|
||
|
}
|
||
|
|
||
|
type configurationPerRequest struct {
|
||
|
Requests []requestDefinition `toml:"request"`
|
||
|
|
||
|
workarounds workarounds
|
||
|
excludeRegisterType bool
|
||
|
logger telegraf.Logger
|
||
|
}
|
||
|
|
||
|
func (*configurationPerRequest) sampleConfigPart() string {
|
||
|
return sampleConfigPartPerRequest
|
||
|
}
|
||
|
|
||
|
func (c *configurationPerRequest) check() error {
|
||
|
switch c.workarounds.StringRegisterLocation {
|
||
|
case "", "both", "lower", "upper":
|
||
|
// Do nothing as those are valid
|
||
|
default:
|
||
|
return fmt.Errorf("invalid 'string_register_location' %q", c.workarounds.StringRegisterLocation)
|
||
|
}
|
||
|
|
||
|
seed := maphash.MakeSeed()
|
||
|
seenFields := make(map[uint64]bool)
|
||
|
|
||
|
for _, def := range c.Requests {
|
||
|
// Check byte order of the data
|
||
|
switch def.ByteOrder {
|
||
|
case "":
|
||
|
def.ByteOrder = "ABCD"
|
||
|
case "ABCD", "DCBA", "BADC", "CDAB", "MSW-BE", "MSW-LE", "LSW-LE", "LSW-BE":
|
||
|
default:
|
||
|
return fmt.Errorf("unknown byte-order %q", def.ByteOrder)
|
||
|
}
|
||
|
|
||
|
// Check register type
|
||
|
switch def.RegisterType {
|
||
|
case "":
|
||
|
def.RegisterType = "holding"
|
||
|
case "coil", "discrete", "holding", "input":
|
||
|
default:
|
||
|
return fmt.Errorf("unknown register-type %q", def.RegisterType)
|
||
|
}
|
||
|
// Check for valid optimization
|
||
|
switch def.Optimization {
|
||
|
case "", "none", "shrink", "rearrange":
|
||
|
case "aggressive":
|
||
|
config.PrintOptionValueDeprecationNotice(
|
||
|
"inputs.modbus",
|
||
|
"optimization",
|
||
|
"aggressive",
|
||
|
telegraf.DeprecationInfo{
|
||
|
Since: "1.28.2",
|
||
|
RemovalIn: "1.30.0",
|
||
|
Notice: `use "max_insert" instead`,
|
||
|
},
|
||
|
)
|
||
|
case "max_insert":
|
||
|
switch def.RegisterType {
|
||
|
case "coil":
|
||
|
if def.MaxExtraRegisters <= 0 || def.MaxExtraRegisters > maxQuantityCoils {
|
||
|
return fmt.Errorf("optimization_max_register_fill has to be between 1 and %d", maxQuantityCoils)
|
||
|
}
|
||
|
case "discrete":
|
||
|
if def.MaxExtraRegisters <= 0 || def.MaxExtraRegisters > maxQuantityDiscreteInput {
|
||
|
return fmt.Errorf("optimization_max_register_fill has to be between 1 and %d", maxQuantityDiscreteInput)
|
||
|
}
|
||
|
case "holding":
|
||
|
if def.MaxExtraRegisters <= 0 || def.MaxExtraRegisters > maxQuantityHoldingRegisters {
|
||
|
return fmt.Errorf("optimization_max_register_fill has to be between 1 and %d", maxQuantityHoldingRegisters)
|
||
|
}
|
||
|
case "input":
|
||
|
if def.MaxExtraRegisters <= 0 || def.MaxExtraRegisters > maxQuantityInputRegisters {
|
||
|
return fmt.Errorf("optimization_max_register_fill has to be between 1 and %d", maxQuantityInputRegisters)
|
||
|
}
|
||
|
}
|
||
|
default:
|
||
|
return fmt.Errorf("unknown optimization %q", def.Optimization)
|
||
|
}
|
||
|
// Set the default for measurement if required
|
||
|
if def.Measurement == "" {
|
||
|
def.Measurement = "modbus"
|
||
|
}
|
||
|
|
||
|
// Reject any configuration without fields as it
|
||
|
// makes no sense to not define anything but a request.
|
||
|
if len(def.Fields) == 0 {
|
||
|
return errors.New("found request section without fields")
|
||
|
}
|
||
|
|
||
|
// Check the fields
|
||
|
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 == "holding" || def.RegisterType == "input" {
|
||
|
switch f.InputType {
|
||
|
case "":
|
||
|
case "INT8L", "INT8H", "INT16", "INT32", "INT64",
|
||
|
"UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64",
|
||
|
"FLOAT16", "FLOAT32", "FLOAT64":
|
||
|
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)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Other properties don't need to be checked for omitted fields
|
||
|
if f.Omit {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Name is mandatory
|
||
|
if f.Name == "" {
|
||
|
return fmt.Errorf("empty field name in request for slave %d", def.SlaveID)
|
||
|
}
|
||
|
|
||
|
// Check output type
|
||
|
if def.RegisterType == "holding" || def.RegisterType == "input" {
|
||
|
switch f.OutputType {
|
||
|
case "", "INT64", "UINT64", "FLOAT64", "STRING":
|
||
|
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
|
||
|
if f.Measurement == "" {
|
||
|
f.Measurement = def.Measurement
|
||
|
}
|
||
|
def.Fields[fidx] = f
|
||
|
|
||
|
// Check for duplicate field definitions
|
||
|
id := c.fieldID(seed, def, f)
|
||
|
if seenFields[id] {
|
||
|
return fmt.Errorf("field %q duplicated in measurement %q (slave %d/%q)", f.Name, f.Measurement, def.SlaveID, def.RegisterType)
|
||
|
}
|
||
|
seenFields[id] = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (c *configurationPerRequest) process() (map[byte]requestSet, error) {
|
||
|
result := make(map[byte]requestSet, len(c.Requests))
|
||
|
for _, def := range c.Requests {
|
||
|
// Set default
|
||
|
if def.RegisterType == "" {
|
||
|
def.RegisterType = "holding"
|
||
|
}
|
||
|
|
||
|
// Construct the fields
|
||
|
isTyped := def.RegisterType == "holding" || def.RegisterType == "input"
|
||
|
fields, err := c.initFields(def.Fields, isTyped, def.ByteOrder)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
// Make sure we have a set to work with
|
||
|
set, found := result[def.SlaveID]
|
||
|
if !found {
|
||
|
set = requestSet{}
|
||
|
}
|
||
|
|
||
|
params := groupingParams{
|
||
|
maxExtraRegisters: def.MaxExtraRegisters,
|
||
|
optimization: def.Optimization,
|
||
|
tags: def.Tags,
|
||
|
log: c.logger,
|
||
|
}
|
||
|
switch def.RegisterType {
|
||
|
case "coil":
|
||
|
params.maxBatchSize = maxQuantityCoils
|
||
|
if c.workarounds.OnRequestPerField {
|
||
|
params.maxBatchSize = 1
|
||
|
}
|
||
|
params.enforceFromZero = c.workarounds.ReadCoilsStartingAtZero
|
||
|
requests := groupFieldsToRequests(fields, params)
|
||
|
set.coil = append(set.coil, requests...)
|
||
|
case "discrete":
|
||
|
params.maxBatchSize = maxQuantityDiscreteInput
|
||
|
if c.workarounds.OnRequestPerField {
|
||
|
params.maxBatchSize = 1
|
||
|
}
|
||
|
requests := groupFieldsToRequests(fields, params)
|
||
|
set.discrete = append(set.discrete, requests...)
|
||
|
case "holding":
|
||
|
params.maxBatchSize = maxQuantityHoldingRegisters
|
||
|
if c.workarounds.OnRequestPerField {
|
||
|
params.maxBatchSize = 1
|
||
|
}
|
||
|
requests := groupFieldsToRequests(fields, params)
|
||
|
set.holding = append(set.holding, requests...)
|
||
|
case "input":
|
||
|
params.maxBatchSize = maxQuantityInputRegisters
|
||
|
if c.workarounds.OnRequestPerField {
|
||
|
params.maxBatchSize = 1
|
||
|
}
|
||
|
requests := groupFieldsToRequests(fields, params)
|
||
|
set.input = append(set.input, requests...)
|
||
|
default:
|
||
|
return nil, fmt.Errorf("unknown register type %q", def.RegisterType)
|
||
|
}
|
||
|
if !set.empty() {
|
||
|
result[def.SlaveID] = set
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return result, nil
|
||
|
}
|
||
|
|
||
|
func (c *configurationPerRequest) initFields(fieldDefs []requestFieldDefinition, typed bool, byteOrder string) ([]field, error) {
|
||
|
// Construct the fields from the field definitions
|
||
|
fields := make([]field, 0, len(fieldDefs))
|
||
|
for _, def := range fieldDefs {
|
||
|
f, err := c.newFieldFromDefinition(def, typed, byteOrder)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("initializing field %q failed: %w", def.Name, err)
|
||
|
}
|
||
|
fields = append(fields, f)
|
||
|
}
|
||
|
|
||
|
return fields, nil
|
||
|
}
|
||
|
|
||
|
func (c *configurationPerRequest) newFieldFromDefinition(def requestFieldDefinition, typed bool, byteOrder string) (field, error) {
|
||
|
var err error
|
||
|
|
||
|
fieldLength := uint16(1)
|
||
|
if typed {
|
||
|
if fieldLength, err = determineFieldLength(def.InputType, def.Length); err != nil {
|
||
|
return field{}, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check for address overflow
|
||
|
if def.Address > math.MaxUint16-fieldLength {
|
||
|
return field{}, fmt.Errorf("%w for field %q", errAddressOverflow, def.Name)
|
||
|
}
|
||
|
|
||
|
// Initialize the field
|
||
|
f := field{
|
||
|
measurement: def.Measurement,
|
||
|
name: def.Name,
|
||
|
address: def.Address,
|
||
|
length: fieldLength,
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// Automagically determine the output type...
|
||
|
if def.OutputType == "" {
|
||
|
if def.Scale == 0.0 {
|
||
|
// For non-scaling cases we should choose the output corresponding to the input class
|
||
|
// i.e. INT64 for INT*, UINT64 for UINT* etc.
|
||
|
var err error
|
||
|
if def.OutputType, err = determineOutputDatatype(def.InputType); err != nil {
|
||
|
return field{}, err
|
||
|
}
|
||
|
} else {
|
||
|
// For scaling cases we always want FLOAT64 by default except for
|
||
|
// string fields
|
||
|
if def.InputType != "STRING" {
|
||
|
def.OutputType = "FLOAT64"
|
||
|
} else {
|
||
|
def.OutputType = "STRING"
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Setting default byte-order
|
||
|
if byteOrder == "" {
|
||
|
byteOrder = "ABCD"
|
||
|
}
|
||
|
|
||
|
// Normalize the data relevant for determining the converter
|
||
|
inType, err := normalizeInputDatatype(def.InputType)
|
||
|
if err != nil {
|
||
|
return field{}, err
|
||
|
}
|
||
|
outType, err := normalizeOutputDatatype(def.OutputType)
|
||
|
if err != nil {
|
||
|
return field{}, err
|
||
|
}
|
||
|
order, err := normalizeByteOrder(byteOrder)
|
||
|
if err != nil {
|
||
|
return field{}, err
|
||
|
}
|
||
|
|
||
|
f.converter, err = determineConverter(inType, order, outType, def.Scale, def.Bit, c.workarounds.StringRegisterLocation)
|
||
|
if err != nil {
|
||
|
return field{}, err
|
||
|
}
|
||
|
|
||
|
return f, nil
|
||
|
}
|
||
|
|
||
|
func (c *configurationPerRequest) fieldID(seed maphash.Seed, def requestDefinition, field requestFieldDefinition) uint64 {
|
||
|
var mh maphash.Hash
|
||
|
mh.SetSeed(seed)
|
||
|
|
||
|
mh.WriteByte(def.SlaveID)
|
||
|
mh.WriteByte(0)
|
||
|
if !c.excludeRegisterType {
|
||
|
mh.WriteString(def.RegisterType)
|
||
|
mh.WriteByte(0)
|
||
|
}
|
||
|
mh.WriteString(field.Measurement)
|
||
|
mh.WriteByte(0)
|
||
|
mh.WriteString(field.Name)
|
||
|
mh.WriteByte(0)
|
||
|
|
||
|
// tags
|
||
|
for k, v := range def.Tags {
|
||
|
mh.WriteString(k)
|
||
|
mh.WriteByte('=')
|
||
|
mh.WriteString(v)
|
||
|
mh.WriteByte(':')
|
||
|
}
|
||
|
mh.WriteByte(0)
|
||
|
|
||
|
return mh.Sum64()
|
||
|
}
|
||
|
|
||
|
func determineOutputDatatype(input string) (string, error) {
|
||
|
// Handle our special types
|
||
|
switch input {
|
||
|
case "INT8L", "INT8H", "INT16", "INT32", "INT64":
|
||
|
return "INT64", nil
|
||
|
case "BIT", "UINT8L", "UINT8H", "UINT16", "UINT32", "UINT64":
|
||
|
return "UINT64", nil
|
||
|
case "FLOAT16", "FLOAT32", "FLOAT64":
|
||
|
return "FLOAT64", nil
|
||
|
case "STRING":
|
||
|
return "STRING", nil
|
||
|
}
|
||
|
return "unknown", fmt.Errorf("invalid input datatype %q for determining output", input)
|
||
|
}
|
||
|
|
||
|
func determineFieldLength(input string, length uint16) (uint16, error) {
|
||
|
// Handle our special types
|
||
|
switch input {
|
||
|
case "BIT", "INT8L", "INT8H", "UINT8L", "UINT8H":
|
||
|
return 1, nil
|
||
|
case "INT16", "UINT16", "FLOAT16":
|
||
|
return 1, nil
|
||
|
case "INT32", "UINT32", "FLOAT32":
|
||
|
return 2, nil
|
||
|
case "INT64", "UINT64", "FLOAT64":
|
||
|
return 4, nil
|
||
|
case "STRING":
|
||
|
return length, nil
|
||
|
}
|
||
|
return 0, fmt.Errorf("invalid input datatype %q for determining field length", input)
|
||
|
}
|