Modbus refactor (#9141)
This commit is contained in:
parent
58479fdb05
commit
2e7b232073
|
|
@ -71,8 +71,6 @@ following works:
|
|||
- github.com/go-redis/redis [BSD 2-Clause "Simplified" License](https://github.com/go-redis/redis/blob/master/LICENSE)
|
||||
- github.com/go-sql-driver/mysql [Mozilla Public License 2.0](https://github.com/go-sql-driver/mysql/blob/master/LICENSE)
|
||||
- github.com/go-stack/stack [MIT License](https://github.com/go-stack/stack/blob/master/LICENSE.md)
|
||||
- github.com/goburrow/modbus [BSD 3-Clause "New" or "Revised" License](https://github.com/goburrow/modbus/blob/master/LICENSE)
|
||||
- github.com/goburrow/serial [MIT License](https://github.com/goburrow/serial/LICENSE)
|
||||
- github.com/gobwas/glob [MIT License](https://github.com/gobwas/glob/blob/master/LICENSE)
|
||||
- github.com/gofrs/uuid [MIT License](https://github.com/gofrs/uuid/blob/master/LICENSE)
|
||||
- github.com/gogo/googleapis [Apache License 2.0](https://github.com/gogo/googleapis/blob/master/LICENSE)
|
||||
|
|
@ -92,6 +90,8 @@ following works:
|
|||
- github.com/gorilla/mux [BSD 3-Clause "New" or "Revised" License](https://github.com/gorilla/mux/blob/master/LICENSE)
|
||||
- github.com/gorilla/websocket [BSD 2-Clause "Simplified" License](https://github.com/gorilla/websocket/blob/master/LICENSE)
|
||||
- github.com/gosnmp/gosnmp [BSD 2-Clause "Simplified" License](https://github.com/gosnmp/gosnmp/blob/master/LICENSE)
|
||||
- github.com/grid-x/modbus [BSD 3-Clause "New" or "Revised" License](https://github.com/grid-x/modbus/blob/master/LICENSE)
|
||||
- github.com/grid-x/serial [MIT License](https://github.com/grid-x/serial/blob/master/LICENSE)
|
||||
- github.com/grpc-ecosystem/grpc-gateway [BSD 3-Clause "New" or "Revised" License](https://github.com/grpc-ecosystem/grpc-gateway/blob/master/LICENSE.txt)
|
||||
- github.com/hailocab/go-hostpool [MIT License](https://github.com/hailocab/go-hostpool/blob/master/LICENSE)
|
||||
- github.com/harlow/kinesis-consumer [MIT License](https://github.com/harlow/kinesis-consumer/blob/master/MIT-LICENSE)
|
||||
|
|
|
|||
3
go.mod
3
go.mod
|
|
@ -53,7 +53,7 @@ require (
|
|||
github.com/go-ping/ping v0.0.0-20210201095549-52eed920f98c
|
||||
github.com/go-redis/redis v6.15.9+incompatible
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
github.com/goburrow/modbus v0.1.0
|
||||
github.com/goburrow/modbus v0.1.0 // indirect
|
||||
github.com/goburrow/serial v0.1.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3
|
||||
github.com/gofrs/uuid v3.3.0+incompatible
|
||||
|
|
@ -66,6 +66,7 @@ require (
|
|||
github.com/gopcua/opcua v0.1.13
|
||||
github.com/gorilla/mux v1.7.3
|
||||
github.com/gosnmp/gosnmp v1.32.0
|
||||
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
|
||||
github.com/harlow/kinesis-consumer v0.3.1-0.20181230152818-2f58b136fee0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -561,6 +561,10 @@ github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/ad
|
|||
github.com/gosnmp/gosnmp v1.32.0 h1:gctewmZx5qFI0oHMzRnjETqIZ093d9NgZy9TQr3V0iA=
|
||||
github.com/gosnmp/gosnmp v1.32.0/go.mod h1:EIp+qkEpXoVsyZxXKy0AmXQx0mCHMMcIhXXvNDMpgF0=
|
||||
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
|
||||
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b h1:Y4xqzO0CDNoehCr3ncgie3IgFTO9AzV8PMMEWESFM5c=
|
||||
github.com/grid-x/modbus v0.0.0-20210224155242-c4a3d042e99b/go.mod h1:YaK0rKJenZ74vZFcSSLlAQqtG74PMI68eDjpDCDDmTw=
|
||||
github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08 h1:syBxnRYnSPUDdkdo5U4sy2roxBPQDjNiw4od7xlsABQ=
|
||||
github.com/grid-x/serial v0.0.0-20191104121038-e24bc9bf6f08/go.mod h1:kdOd86/VGFWRrtkNwf1MPk0u1gIjc4Y7R2j7nhwc7Rk=
|
||||
github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
|
||||
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
|
||||
|
|
|
|||
|
|
@ -127,6 +127,20 @@ with N decimal places'.
|
|||
(FLOAT32 is deprecated and should not be used any more. UFIXED provides the same conversion
|
||||
from unsigned values).
|
||||
|
||||
### Trouble shooting
|
||||
Modbus documentations are often a mess. People confuse memory-address (starts at one) and register address (starts at zero) or stay unclear about the used word-order. Furthermore, there are some non-standard implementations that also
|
||||
swap the bytes within the register word (16-bit).
|
||||
|
||||
If you get an error or don't get the expected values from your device, you can try the following steps (assuming a 32-bit value).
|
||||
|
||||
In case are using a serial device and get an `permission denied` error, please check the permissions of your serial device and change accordingly.
|
||||
|
||||
In case you get an `exception '2' (illegal data address)` error you might try to offset your `address` entries by minus one as it is very likely that there is a confusion between memory and register addresses.
|
||||
|
||||
In case you see strange values, the `byte_order` might be off. You can either probe all combinations (`ABCD`, `CDBA`, `BADC` or `DCBA`) or you set `byte_order="ABCD" data_type="UINT32"` and use the resulting value(s) in an online converter like [this](https://www.scadacore.com/tools/programming-calculators/online-hex-converter/). This makes especially sense if you don't want to mess with the device, deal with 64-bit values and/or don't know the `data_type` of your register (e.g. fix-point floating values vs. IEEE floating point).
|
||||
|
||||
If nothing helps, please post your configuration, error message and/or the output of `byte_order="ABCD" data_type="UINT32"` to one of the telegraf support channels (forum, slack or as issue).
|
||||
|
||||
### Example Output
|
||||
|
||||
```sh
|
||||
|
|
|
|||
|
|
@ -0,0 +1,61 @@
|
|||
package modbus
|
||||
|
||||
import "fmt"
|
||||
|
||||
const (
|
||||
maxQuantityDiscreteInput = uint16(2000)
|
||||
maxQuantityCoils = uint16(2000)
|
||||
maxQuantityInputRegisters = uint16(125)
|
||||
maxQuantityHoldingRegisters = uint16(125)
|
||||
)
|
||||
|
||||
type Configuration interface {
|
||||
Check() error
|
||||
Process() (map[byte]requestSet, error)
|
||||
}
|
||||
|
||||
func removeDuplicates(elements []uint16) []uint16 {
|
||||
encountered := map[uint16]bool{}
|
||||
result := []uint16{}
|
||||
|
||||
for _, addr := range elements {
|
||||
if !encountered[addr] {
|
||||
encountered[addr] = true
|
||||
result = append(result, addr)
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func normalizeInputDatatype(dataType string) (string, error) {
|
||||
switch dataType {
|
||||
case "INT16", "UINT16", "INT32", "UINT32", "INT64", "UINT64", "FLOAT32", "FLOAT64":
|
||||
return dataType, nil
|
||||
}
|
||||
return "unknown", fmt.Errorf("unknown type %q", dataType)
|
||||
}
|
||||
|
||||
func normalizeOutputDatatype(dataType string) (string, error) {
|
||||
switch dataType {
|
||||
case "", "native":
|
||||
return "native", nil
|
||||
case "INT64", "UINT64", "FLOAT64":
|
||||
return dataType, nil
|
||||
}
|
||||
return "unknown", fmt.Errorf("unknown type %q", dataType)
|
||||
}
|
||||
|
||||
func normalizeByteOrder(byteOrder string) (string, error) {
|
||||
switch byteOrder {
|
||||
case "ABCD", "MSW-BE", "MSW": // Big endian (Motorola)
|
||||
return "ABCD", nil
|
||||
case "BADC", "MSW-LE": // Big endian with bytes swapped
|
||||
return "BADC", nil
|
||||
case "CDAB", "LSW-BE": // Little endian with bytes swapped
|
||||
return "CDAB", nil
|
||||
case "DCBA", "LSW-LE", "LSW": // Little endian (Intel)
|
||||
return "DCBA", nil
|
||||
}
|
||||
return "unknown", fmt.Errorf("unknown byte-order %q", byteOrder)
|
||||
}
|
||||
|
|
@ -0,0 +1,246 @@
|
|||
package modbus
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type fieldDefinition struct {
|
||||
Measurement string `toml:"measurement"`
|
||||
Name string `toml:"name"`
|
||||
ByteOrder string `toml:"byte_order"`
|
||||
DataType string `toml:"data_type"`
|
||||
Scale float64 `toml:"scale"`
|
||||
Address []uint16 `toml:"address"`
|
||||
}
|
||||
|
||||
type ConfigurationOriginal struct {
|
||||
SlaveID byte `toml:"slave_id"`
|
||||
DiscreteInputs []fieldDefinition `toml:"discrete_inputs"`
|
||||
Coils []fieldDefinition `toml:"coils"`
|
||||
HoldingRegisters []fieldDefinition `toml:"holding_registers"`
|
||||
InputRegisters []fieldDefinition `toml:"input_registers"`
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) Process() (map[byte]requestSet, error) {
|
||||
coil, err := c.initRequests(c.Coils, cCoils, maxQuantityCoils)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
discrete, err := c.initRequests(c.DiscreteInputs, cDiscreteInputs, maxQuantityDiscreteInput)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
holding, err := c.initRequests(c.HoldingRegisters, cHoldingRegisters, maxQuantityHoldingRegisters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
input, err := c.initRequests(c.InputRegisters, cInputRegisters, maxQuantityInputRegisters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[byte]requestSet{
|
||||
c.SlaveID: {
|
||||
coil: coil,
|
||||
discrete: discrete,
|
||||
holding: holding,
|
||||
input: input,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) Check() error {
|
||||
if err := c.validateFieldDefinitions(c.DiscreteInputs, cDiscreteInputs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.validateFieldDefinitions(c.Coils, cCoils); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := c.validateFieldDefinitions(c.HoldingRegisters, cHoldingRegisters); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.validateFieldDefinitions(c.InputRegisters, cInputRegisters)
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) initRequests(fieldDefs []fieldDefinition, registerType string, maxQuantity uint16) ([]request, error) {
|
||||
fields, err := c.initFields(fieldDefs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newRequestsFromFields(fields, c.SlaveID, registerType, maxQuantity), nil
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) initFields(fieldDefs []fieldDefinition) ([]field, error) {
|
||||
// Construct the fields from the field definitions
|
||||
fields := make([]field, 0, len(fieldDefs))
|
||||
for _, def := range fieldDefs {
|
||||
f, err := c.newFieldFromDefinition(def)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("initializing field %q failed: %v", def.Name, err)
|
||||
}
|
||||
fields = append(fields, f)
|
||||
}
|
||||
|
||||
return fields, nil
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) newFieldFromDefinition(def fieldDefinition) (field, error) {
|
||||
// Check if the addresses are consecutive
|
||||
expected := def.Address[0]
|
||||
for _, current := range def.Address[1:] {
|
||||
expected++
|
||||
if current != expected {
|
||||
return field{}, fmt.Errorf("addresses of field %q are not consecutive", def.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the field
|
||||
f := field{
|
||||
measurement: def.Measurement,
|
||||
name: def.Name,
|
||||
scale: def.Scale,
|
||||
address: def.Address[0],
|
||||
length: uint16(len(def.Address)),
|
||||
}
|
||||
if def.DataType != "" {
|
||||
inType, err := c.normalizeInputDatatype(def.DataType, len(def.Address))
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
outType, err := c.normalizeOutputDatatype(def.DataType)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
byteOrder, err := c.normalizeByteOrder(def.ByteOrder)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
|
||||
f.converter, err = determineConverter(inType, byteOrder, outType, def.Scale)
|
||||
if err != nil {
|
||||
return f, err
|
||||
}
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) validateFieldDefinitions(fieldDefs []fieldDefinition, registerType string) error {
|
||||
nameEncountered := map[string]bool{}
|
||||
for _, item := range fieldDefs {
|
||||
//check empty name
|
||||
if item.Name == "" {
|
||||
return fmt.Errorf("empty name in '%s'", registerType)
|
||||
}
|
||||
|
||||
//search name duplicate
|
||||
canonicalName := item.Measurement + "." + item.Name
|
||||
if nameEncountered[canonicalName] {
|
||||
return fmt.Errorf("name '%s' is duplicated in measurement '%s' '%s' - '%s'", item.Name, item.Measurement, registerType, item.Name)
|
||||
}
|
||||
nameEncountered[canonicalName] = true
|
||||
|
||||
if registerType == cInputRegisters || registerType == cHoldingRegisters {
|
||||
// search byte order
|
||||
switch item.ByteOrder {
|
||||
case "AB", "BA", "ABCD", "CDAB", "BADC", "DCBA", "ABCDEFGH", "HGFEDCBA", "BADCFEHG", "GHEFCDAB":
|
||||
default:
|
||||
return fmt.Errorf("invalid byte order '%s' in '%s' - '%s'", item.ByteOrder, registerType, item.Name)
|
||||
}
|
||||
|
||||
// search data type
|
||||
switch item.DataType {
|
||||
case "UINT16", "INT16", "UINT32", "INT32", "UINT64", "INT64", "FLOAT32-IEEE", "FLOAT64-IEEE", "FLOAT32", "FIXED", "UFIXED":
|
||||
default:
|
||||
return fmt.Errorf("invalid data type '%s' in '%s' - '%s'", item.DataType, registerType, item.Name)
|
||||
}
|
||||
|
||||
// check scale
|
||||
if item.Scale == 0.0 {
|
||||
return fmt.Errorf("invalid scale '%f' in '%s' - '%s'", item.Scale, 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 '%s' - '%s'", 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 '%s' and address '%v' in '%s' - '%s'", item.ByteOrder, item.Address, registerType, item.Name)
|
||||
}
|
||||
|
||||
// search duplicated
|
||||
if len(item.Address) > len(removeDuplicates(item.Address)) {
|
||||
return fmt.Errorf("duplicate address '%v' in '%s' - '%s'", item.Address, registerType, item.Name)
|
||||
}
|
||||
} else if len(item.Address) != 1 {
|
||||
return fmt.Errorf("invalid address'%v' length'%v' in '%s' - '%s'", item.Address, len(item.Address), registerType, item.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) normalizeInputDatatype(dataType string, words int) (string, error) {
|
||||
// Handle our special types
|
||||
switch dataType {
|
||||
case "FIXED":
|
||||
switch words {
|
||||
case 1:
|
||||
return "INT16", nil
|
||||
case 2:
|
||||
return "INT32", nil
|
||||
case 4:
|
||||
return "INT64", nil
|
||||
default:
|
||||
return "unknown", fmt.Errorf("invalid length %d for type %q", words, dataType)
|
||||
}
|
||||
case "FLOAT32", "UFIXED":
|
||||
switch words {
|
||||
case 1:
|
||||
return "UINT16", nil
|
||||
case 2:
|
||||
return "UINT32", nil
|
||||
case 4:
|
||||
return "UINT64", nil
|
||||
default:
|
||||
return "unknown", fmt.Errorf("invalid length %d for type %q", words, dataType)
|
||||
}
|
||||
case "FLOAT32-IEEE":
|
||||
return "FLOAT32", nil
|
||||
case "FLOAT64-IEEE":
|
||||
return "FLOAT64", nil
|
||||
}
|
||||
return normalizeInputDatatype(dataType)
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) normalizeOutputDatatype(dataType string) (string, error) {
|
||||
// Handle our special types
|
||||
switch dataType {
|
||||
case "FIXED", "FLOAT32", "UFIXED":
|
||||
return "FLOAT64", nil
|
||||
}
|
||||
return normalizeOutputDatatype("native")
|
||||
}
|
||||
|
||||
func (c *ConfigurationOriginal) normalizeByteOrder(byteOrder string) (string, error) {
|
||||
// Handle our special types
|
||||
switch byteOrder {
|
||||
case "AB", "ABCDEFGH":
|
||||
return "ABCD", nil
|
||||
case "BADCFEHG":
|
||||
return "BADC", nil
|
||||
case "GHEFCDAB":
|
||||
return "CDAB", nil
|
||||
case "BA", "HGFEDCBA":
|
||||
return "DCBA", nil
|
||||
}
|
||||
return normalizeByteOrder(byteOrder)
|
||||
}
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
package modbus
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
mb "github.com/goburrow/modbus"
|
||||
mb "github.com/grid-x/modbus"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/config"
|
||||
"github.com/influxdata/telegraf/metric"
|
||||
|
|
@ -25,42 +24,37 @@ type Modbus struct {
|
|||
DataBits int `toml:"data_bits"`
|
||||
Parity string `toml:"parity"`
|
||||
StopBits int `toml:"stop_bits"`
|
||||
SlaveID int `toml:"slave_id"`
|
||||
Timeout config.Duration `toml:"timeout"`
|
||||
Retries int `toml:"busy_retries"`
|
||||
RetriesWaitTime config.Duration `toml:"busy_retries_wait"`
|
||||
DiscreteInputs []fieldContainer `toml:"discrete_inputs"`
|
||||
Coils []fieldContainer `toml:"coils"`
|
||||
HoldingRegisters []fieldContainer `toml:"holding_registers"`
|
||||
InputRegisters []fieldContainer `toml:"input_registers"`
|
||||
Log telegraf.Logger `toml:"-"`
|
||||
registers []register
|
||||
isConnected bool
|
||||
tcpHandler *mb.TCPClientHandler
|
||||
rtuHandler *mb.RTUClientHandler
|
||||
asciiHandler *mb.ASCIIClientHandler
|
||||
// Register configuration
|
||||
ConfigurationOriginal
|
||||
// Connection handling
|
||||
client mb.Client
|
||||
handler mb.ClientHandler
|
||||
isConnected bool
|
||||
// Request handling
|
||||
requests map[byte]requestSet
|
||||
}
|
||||
|
||||
type register struct {
|
||||
Type string
|
||||
RegistersRange []registerRange
|
||||
Fields []fieldContainer
|
||||
type fieldConverterFunc func(bytes []byte) interface{}
|
||||
|
||||
type requestSet struct {
|
||||
coil []request
|
||||
discrete []request
|
||||
holding []request
|
||||
input []request
|
||||
}
|
||||
|
||||
type fieldContainer struct {
|
||||
Measurement string `toml:"measurement"`
|
||||
Name string `toml:"name"`
|
||||
ByteOrder string `toml:"byte_order"`
|
||||
DataType string `toml:"data_type"`
|
||||
Scale float64 `toml:"scale"`
|
||||
Address []uint16 `toml:"address"`
|
||||
value interface{}
|
||||
}
|
||||
|
||||
type registerRange struct {
|
||||
type field struct {
|
||||
measurement string
|
||||
name string
|
||||
scale float64
|
||||
address uint16
|
||||
length uint16
|
||||
converter fieldConverterFunc
|
||||
value interface{}
|
||||
}
|
||||
|
||||
const (
|
||||
|
|
@ -173,500 +167,29 @@ func (m *Modbus) Init() error {
|
|||
return fmt.Errorf("retries cannot be negative")
|
||||
}
|
||||
|
||||
err := m.InitRegister(m.DiscreteInputs, cDiscreteInputs)
|
||||
if err != nil {
|
||||
return err
|
||||
// Check and process the configuration
|
||||
if err := m.ConfigurationOriginal.Check(); err != nil {
|
||||
return fmt.Errorf("original configuraton invalid: %v", err)
|
||||
}
|
||||
|
||||
err = m.InitRegister(m.Coils, cCoils)
|
||||
r, err := m.ConfigurationOriginal.Process()
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("cannot process original configuraton: %v", err)
|
||||
}
|
||||
m.requests = r
|
||||
|
||||
err = m.InitRegister(m.HoldingRegisters, cHoldingRegisters)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.InitRegister(m.InputRegisters, cInputRegisters)
|
||||
if err != nil {
|
||||
return err
|
||||
// Setup client
|
||||
if err := m.initClient(); err != nil {
|
||||
return fmt.Errorf("initializing client failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Modbus) InitRegister(fields []fieldContainer, name string) error {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := validateFieldContainers(fields, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
addrs := []uint16{}
|
||||
for _, field := range fields {
|
||||
addrs = append(addrs, field.Address...)
|
||||
}
|
||||
|
||||
addrs = removeDuplicates(addrs)
|
||||
sort.Slice(addrs, func(i, j int) bool { return addrs[i] < addrs[j] })
|
||||
|
||||
ii := 0
|
||||
maxQuantity := 1
|
||||
var registersRange []registerRange
|
||||
if name == cDiscreteInputs || name == cCoils {
|
||||
maxQuantity = 2000
|
||||
} else if name == cInputRegisters || name == cHoldingRegisters {
|
||||
maxQuantity = 125
|
||||
}
|
||||
|
||||
// Get range of consecutive integers
|
||||
// [1, 2, 3, 5, 6, 10, 11, 12, 14]
|
||||
// (1, 3) , (5, 2) , (10, 3), (14 , 1)
|
||||
for range addrs {
|
||||
if ii >= len(addrs) {
|
||||
break
|
||||
}
|
||||
quantity := 1
|
||||
start := addrs[ii]
|
||||
end := start
|
||||
|
||||
for ii < len(addrs)-1 && addrs[ii+1]-addrs[ii] == 1 && quantity < maxQuantity {
|
||||
end = addrs[ii+1]
|
||||
ii++
|
||||
quantity++
|
||||
}
|
||||
ii++
|
||||
|
||||
registersRange = append(registersRange, registerRange{start, end - start + 1})
|
||||
}
|
||||
|
||||
m.registers = append(m.registers, register{name, registersRange, fields})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to a MODBUS Slave device via Modbus/[TCP|RTU|ASCII]
|
||||
func connect(m *Modbus) error {
|
||||
u, err := url.Parse(m.Controller)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "tcp":
|
||||
var host, port string
|
||||
host, port, err = net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.tcpHandler = mb.NewTCPClientHandler(host + ":" + port)
|
||||
m.tcpHandler.Timeout = time.Duration(m.Timeout)
|
||||
m.tcpHandler.SlaveId = byte(m.SlaveID)
|
||||
m.client = mb.NewClient(m.tcpHandler)
|
||||
err := m.tcpHandler.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.isConnected = true
|
||||
return nil
|
||||
case "file":
|
||||
if m.TransmissionMode == "RTU" {
|
||||
m.rtuHandler = mb.NewRTUClientHandler(u.Path)
|
||||
m.rtuHandler.Timeout = time.Duration(m.Timeout)
|
||||
m.rtuHandler.SlaveId = byte(m.SlaveID)
|
||||
m.rtuHandler.BaudRate = m.BaudRate
|
||||
m.rtuHandler.DataBits = m.DataBits
|
||||
m.rtuHandler.Parity = m.Parity
|
||||
m.rtuHandler.StopBits = m.StopBits
|
||||
m.client = mb.NewClient(m.rtuHandler)
|
||||
err := m.rtuHandler.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.isConnected = true
|
||||
return nil
|
||||
} else if m.TransmissionMode == "ASCII" {
|
||||
m.asciiHandler = mb.NewASCIIClientHandler(u.Path)
|
||||
m.asciiHandler.Timeout = time.Duration(m.Timeout)
|
||||
m.asciiHandler.SlaveId = byte(m.SlaveID)
|
||||
m.asciiHandler.BaudRate = m.BaudRate
|
||||
m.asciiHandler.DataBits = m.DataBits
|
||||
m.asciiHandler.Parity = m.Parity
|
||||
m.asciiHandler.StopBits = m.StopBits
|
||||
m.client = mb.NewClient(m.asciiHandler)
|
||||
err := m.asciiHandler.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.isConnected = true
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("invalid protocol '%s' - '%s' ", u.Scheme, m.TransmissionMode)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid controller")
|
||||
}
|
||||
}
|
||||
|
||||
func disconnect(m *Modbus) error {
|
||||
u, err := url.Parse(m.Controller)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "tcp":
|
||||
m.tcpHandler.Close()
|
||||
return nil
|
||||
case "file":
|
||||
if m.TransmissionMode == "RTU" {
|
||||
m.rtuHandler.Close()
|
||||
return nil
|
||||
} else if m.TransmissionMode == "ASCII" {
|
||||
m.asciiHandler.Close()
|
||||
return nil
|
||||
} else {
|
||||
return fmt.Errorf("invalid protocol '%s' - '%s' ", u.Scheme, m.TransmissionMode)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid controller")
|
||||
}
|
||||
}
|
||||
|
||||
func validateFieldContainers(t []fieldContainer, n string) error {
|
||||
nameEncountered := map[string]bool{}
|
||||
for _, item := range t {
|
||||
//check empty name
|
||||
if item.Name == "" {
|
||||
return fmt.Errorf("empty name in '%s'", n)
|
||||
}
|
||||
|
||||
//search name duplicate
|
||||
canonicalName := item.Measurement + "." + item.Name
|
||||
if nameEncountered[canonicalName] {
|
||||
return fmt.Errorf("name '%s' is duplicated in measurement '%s' '%s' - '%s'", item.Name, item.Measurement, n, item.Name)
|
||||
}
|
||||
nameEncountered[canonicalName] = true
|
||||
|
||||
if n == cInputRegisters || n == cHoldingRegisters {
|
||||
// search byte order
|
||||
switch item.ByteOrder {
|
||||
case "AB", "BA", "ABCD", "CDAB", "BADC", "DCBA", "ABCDEFGH", "HGFEDCBA", "BADCFEHG", "GHEFCDAB":
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("invalid byte order '%s' in '%s' - '%s'", item.ByteOrder, n, item.Name)
|
||||
}
|
||||
|
||||
// search data type
|
||||
switch item.DataType {
|
||||
case "UINT16", "INT16", "UINT32", "INT32", "UINT64", "INT64", "FLOAT32-IEEE", "FLOAT64-IEEE", "FLOAT32", "FIXED", "UFIXED":
|
||||
break
|
||||
default:
|
||||
return fmt.Errorf("invalid data type '%s' in '%s' - '%s'", item.DataType, n, item.Name)
|
||||
}
|
||||
|
||||
// check scale
|
||||
if item.Scale == 0.0 {
|
||||
return fmt.Errorf("invalid scale '%f' in '%s' - '%s'", item.Scale, n, 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 '%s' - '%s'", item.Address, len(item.Address), n, item.Name)
|
||||
}
|
||||
|
||||
if n == cInputRegisters || n == cHoldingRegisters {
|
||||
if 2*len(item.Address) != len(item.ByteOrder) {
|
||||
return fmt.Errorf("invalid byte order '%s' and address '%v' in '%s' - '%s'", item.ByteOrder, item.Address, n, item.Name)
|
||||
}
|
||||
|
||||
// search duplicated
|
||||
if len(item.Address) > len(removeDuplicates(item.Address)) {
|
||||
return fmt.Errorf("duplicate address '%v' in '%s' - '%s'", item.Address, n, item.Name)
|
||||
}
|
||||
} else if len(item.Address) != 1 {
|
||||
return fmt.Errorf("invalid address'%v' length'%v' in '%s' - '%s'", item.Address, len(item.Address), n, item.Name)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func removeDuplicates(elements []uint16) []uint16 {
|
||||
encountered := map[uint16]bool{}
|
||||
result := []uint16{}
|
||||
|
||||
for v := range elements {
|
||||
if encountered[elements[v]] {
|
||||
} else {
|
||||
encountered[elements[v]] = true
|
||||
result = append(result, elements[v])
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func readRegisterValues(m *Modbus, rt string, rr registerRange) ([]byte, error) {
|
||||
if rt == cDiscreteInputs {
|
||||
return m.client.ReadDiscreteInputs(rr.address, rr.length)
|
||||
} else if rt == cCoils {
|
||||
return m.client.ReadCoils(rr.address, rr.length)
|
||||
} else if rt == cInputRegisters {
|
||||
return m.client.ReadInputRegisters(rr.address, rr.length)
|
||||
} else if rt == cHoldingRegisters {
|
||||
return m.client.ReadHoldingRegisters(rr.address, rr.length)
|
||||
} else {
|
||||
return []byte{}, fmt.Errorf("not Valid function")
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Modbus) getFields() error {
|
||||
for _, register := range m.registers {
|
||||
rawValues := make(map[uint16][]byte)
|
||||
bitRawValues := make(map[uint16]uint16)
|
||||
for _, rr := range register.RegistersRange {
|
||||
address := rr.address
|
||||
readValues, err := readRegisterValues(m, register.Type, rr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Raw Values
|
||||
if register.Type == cDiscreteInputs || register.Type == cCoils {
|
||||
for _, readValue := range readValues {
|
||||
for bitPosition := 0; bitPosition < 8; bitPosition++ {
|
||||
bitRawValues[address] = getBitValue(readValue, bitPosition)
|
||||
address = address + 1
|
||||
if address > rr.address+rr.length {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Raw Values
|
||||
if register.Type == cInputRegisters || register.Type == cHoldingRegisters {
|
||||
batchSize := 2
|
||||
for batchSize < len(readValues) {
|
||||
rawValues[address] = readValues[0:batchSize:batchSize]
|
||||
address = address + 1
|
||||
readValues = readValues[batchSize:]
|
||||
}
|
||||
|
||||
rawValues[address] = readValues[0:batchSize:batchSize]
|
||||
}
|
||||
}
|
||||
|
||||
if register.Type == cDiscreteInputs || register.Type == cCoils {
|
||||
for i := 0; i < len(register.Fields); i++ {
|
||||
register.Fields[i].value = bitRawValues[register.Fields[i].Address[0]]
|
||||
}
|
||||
}
|
||||
|
||||
if register.Type == cInputRegisters || register.Type == cHoldingRegisters {
|
||||
for i := 0; i < len(register.Fields); i++ {
|
||||
var valuesT []byte
|
||||
|
||||
for j := 0; j < len(register.Fields[i].Address); j++ {
|
||||
tempArray := rawValues[register.Fields[i].Address[j]]
|
||||
for x := 0; x < len(tempArray); x++ {
|
||||
valuesT = append(valuesT, tempArray[x])
|
||||
}
|
||||
}
|
||||
|
||||
register.Fields[i].value = convertDataType(register.Fields[i], valuesT)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBitValue(n byte, pos int) uint16 {
|
||||
return uint16(n >> uint(pos) & 0x01)
|
||||
}
|
||||
|
||||
func convertDataType(t fieldContainer, bytes []byte) interface{} {
|
||||
switch t.DataType {
|
||||
case "UINT16":
|
||||
e16 := convertEndianness16(t.ByteOrder, bytes)
|
||||
return scaleUint16(t.Scale, e16)
|
||||
case "INT16":
|
||||
e16 := convertEndianness16(t.ByteOrder, bytes)
|
||||
f16 := int16(e16)
|
||||
return scaleInt16(t.Scale, f16)
|
||||
case "UINT32":
|
||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
||||
return scaleUint32(t.Scale, e32)
|
||||
case "INT32":
|
||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
||||
f32 := int32(e32)
|
||||
return scaleInt32(t.Scale, f32)
|
||||
case "UINT64":
|
||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
||||
f64 := format64(t.DataType, e64).(uint64)
|
||||
return scaleUint64(t.Scale, f64)
|
||||
case "INT64":
|
||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
||||
f64 := format64(t.DataType, e64).(int64)
|
||||
return scaleInt64(t.Scale, f64)
|
||||
case "FLOAT32-IEEE":
|
||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
||||
f32 := math.Float32frombits(e32)
|
||||
return scaleFloat32(t.Scale, f32)
|
||||
case "FLOAT64-IEEE":
|
||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
||||
f64 := math.Float64frombits(e64)
|
||||
return scaleFloat64(t.Scale, f64)
|
||||
case "FIXED":
|
||||
if len(bytes) == 2 {
|
||||
e16 := convertEndianness16(t.ByteOrder, bytes)
|
||||
f16 := int16(e16)
|
||||
return scale16toFloat(t.Scale, f16)
|
||||
} else if len(bytes) == 4 {
|
||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
||||
f32 := int32(e32)
|
||||
return scale32toFloat(t.Scale, f32)
|
||||
} else {
|
||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
||||
f64 := int64(e64)
|
||||
return scale64toFloat(t.Scale, f64)
|
||||
}
|
||||
case "FLOAT32", "UFIXED":
|
||||
if len(bytes) == 2 {
|
||||
e16 := convertEndianness16(t.ByteOrder, bytes)
|
||||
return scale16UtoFloat(t.Scale, e16)
|
||||
} else if len(bytes) == 4 {
|
||||
e32 := convertEndianness32(t.ByteOrder, bytes)
|
||||
return scale32UtoFloat(t.Scale, e32)
|
||||
} else {
|
||||
e64 := convertEndianness64(t.ByteOrder, bytes)
|
||||
return scale64UtoFloat(t.Scale, e64)
|
||||
}
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func convertEndianness16(o string, b []byte) uint16 {
|
||||
switch o {
|
||||
case "AB":
|
||||
return binary.BigEndian.Uint16(b)
|
||||
case "BA":
|
||||
return binary.LittleEndian.Uint16(b)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func convertEndianness32(o string, b []byte) uint32 {
|
||||
switch o {
|
||||
case "ABCD":
|
||||
return binary.BigEndian.Uint32(b)
|
||||
case "DCBA":
|
||||
return binary.LittleEndian.Uint32(b)
|
||||
case "BADC":
|
||||
return uint32(binary.LittleEndian.Uint16(b[0:]))<<16 | uint32(binary.LittleEndian.Uint16(b[2:]))
|
||||
case "CDAB":
|
||||
return uint32(binary.BigEndian.Uint16(b[2:]))<<16 | uint32(binary.BigEndian.Uint16(b[0:]))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func convertEndianness64(o string, b []byte) uint64 {
|
||||
switch o {
|
||||
case "ABCDEFGH":
|
||||
return binary.BigEndian.Uint64(b)
|
||||
case "HGFEDCBA":
|
||||
return binary.LittleEndian.Uint64(b)
|
||||
case "BADCFEHG":
|
||||
return uint64(binary.LittleEndian.Uint16(b[0:]))<<48 | uint64(binary.LittleEndian.Uint16(b[2:]))<<32 | uint64(binary.LittleEndian.Uint16(b[4:]))<<16 | uint64(binary.LittleEndian.Uint16(b[6:]))
|
||||
case "GHEFCDAB":
|
||||
return uint64(binary.BigEndian.Uint16(b[6:]))<<48 | uint64(binary.BigEndian.Uint16(b[4:]))<<32 | uint64(binary.BigEndian.Uint16(b[2:]))<<16 | uint64(binary.BigEndian.Uint16(b[0:]))
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func format64(f string, r uint64) interface{} {
|
||||
switch f {
|
||||
case "UINT64":
|
||||
return r
|
||||
case "INT64":
|
||||
return int64(r)
|
||||
default:
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
func scale16toFloat(s float64, v int16) float64 {
|
||||
return float64(v) * s
|
||||
}
|
||||
|
||||
func scale32toFloat(s float64, v int32) float64 {
|
||||
return float64(float64(v) * float64(s))
|
||||
}
|
||||
|
||||
func scale64toFloat(s float64, v int64) float64 {
|
||||
return float64(float64(v) * float64(s))
|
||||
}
|
||||
|
||||
func scale16UtoFloat(s float64, v uint16) float64 {
|
||||
return float64(v) * s
|
||||
}
|
||||
|
||||
func scale32UtoFloat(s float64, v uint32) float64 {
|
||||
return float64(float64(v) * float64(s))
|
||||
}
|
||||
|
||||
func scale64UtoFloat(s float64, v uint64) float64 {
|
||||
return float64(float64(v) * float64(s))
|
||||
}
|
||||
|
||||
func scaleInt16(s float64, v int16) int16 {
|
||||
return int16(float64(v) * s)
|
||||
}
|
||||
|
||||
func scaleUint16(s float64, v uint16) uint16 {
|
||||
return uint16(float64(v) * s)
|
||||
}
|
||||
|
||||
func scaleUint32(s float64, v uint32) uint32 {
|
||||
return uint32(float64(v) * float64(s))
|
||||
}
|
||||
|
||||
func scaleInt32(s float64, v int32) int32 {
|
||||
return int32(float64(v) * float64(s))
|
||||
}
|
||||
|
||||
func scaleFloat32(s float64, v float32) float32 {
|
||||
return float32(float64(v) * s)
|
||||
}
|
||||
|
||||
func scaleFloat64(s float64, v float64) float64 {
|
||||
return v * s
|
||||
}
|
||||
|
||||
func scaleUint64(s float64, v uint64) uint64 {
|
||||
return uint64(float64(v) * float64(s))
|
||||
}
|
||||
|
||||
func scaleInt64(s float64, v int64) int64 {
|
||||
return int64(float64(v) * float64(s))
|
||||
}
|
||||
|
||||
// Gather implements the telegraf plugin interface method for data accumulation
|
||||
func (m *Modbus) Gather(acc telegraf.Accumulator) error {
|
||||
if !m.isConnected {
|
||||
err := connect(m)
|
||||
if err != nil {
|
||||
m.isConnected = false
|
||||
if err := m.connect(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -674,53 +197,236 @@ func (m *Modbus) Gather(acc telegraf.Accumulator) error {
|
|||
timestamp := time.Now()
|
||||
for retry := 0; retry <= m.Retries; retry++ {
|
||||
timestamp = time.Now()
|
||||
err := m.getFields()
|
||||
if err != nil {
|
||||
mberr, ok := err.(*mb.ModbusError)
|
||||
if ok && mberr.ExceptionCode == mb.ExceptionCodeServerDeviceBusy && retry < m.Retries {
|
||||
if err := m.gatherFields(); err != nil {
|
||||
if mberr, ok := err.(*mb.Error); ok && mberr.ExceptionCode == mb.ExceptionCodeServerDeviceBusy && retry < m.Retries {
|
||||
m.Log.Infof("Device busy! Retrying %d more time(s)...", m.Retries-retry)
|
||||
time.Sleep(time.Duration(m.RetriesWaitTime))
|
||||
continue
|
||||
}
|
||||
// Ignore return error to not shadow the initial error
|
||||
//nolint:errcheck,revive
|
||||
disconnect(m)
|
||||
m.isConnected = false
|
||||
// Show the disconnect error this way to not shadow the initial error
|
||||
if discerr := m.disconnect(); discerr != nil {
|
||||
m.Log.Errorf("Disconnecting failed: %v", discerr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
// Reading was successful, leave the retry loop
|
||||
break
|
||||
}
|
||||
|
||||
grouper := metric.NewSeriesGrouper()
|
||||
for _, reg := range m.registers {
|
||||
for slaveID, requests := range m.requests {
|
||||
tags := map[string]string{
|
||||
"name": m.Name,
|
||||
"type": reg.Type,
|
||||
"type": cCoils,
|
||||
"slave_id": strconv.Itoa(int(slaveID)),
|
||||
}
|
||||
m.collectFields(acc, timestamp, tags, requests.coil)
|
||||
|
||||
tags["type"] = cDiscreteInputs
|
||||
m.collectFields(acc, timestamp, tags, requests.discrete)
|
||||
|
||||
tags["type"] = cHoldingRegisters
|
||||
m.collectFields(acc, timestamp, tags, requests.holding)
|
||||
|
||||
tags["type"] = cInputRegisters
|
||||
m.collectFields(acc, timestamp, tags, requests.input)
|
||||
}
|
||||
|
||||
for _, field := range reg.Fields {
|
||||
// In case no measurement was specified we use "modbus" as default
|
||||
measurement := "modbus"
|
||||
if field.Measurement != "" {
|
||||
measurement = field.Measurement
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Group the data by series
|
||||
if err := grouper.Add(measurement, tags, timestamp, field.Name, field.value); err != nil {
|
||||
func (m *Modbus) initClient() error {
|
||||
u, err := url.Parse(m.Controller)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch u.Scheme {
|
||||
case "tcp":
|
||||
host, port, err := net.SplitHostPort(u.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
handler := mb.NewTCPClientHandler(host + ":" + port)
|
||||
handler.Timeout = time.Duration(m.Timeout)
|
||||
m.handler = handler
|
||||
case "file":
|
||||
switch m.TransmissionMode {
|
||||
case "RTU":
|
||||
handler := mb.NewRTUClientHandler(u.Path)
|
||||
handler.Timeout = time.Duration(m.Timeout)
|
||||
handler.BaudRate = m.BaudRate
|
||||
handler.DataBits = m.DataBits
|
||||
handler.Parity = m.Parity
|
||||
handler.StopBits = m.StopBits
|
||||
m.handler = handler
|
||||
case "ASCII":
|
||||
handler := mb.NewASCIIClientHandler(u.Path)
|
||||
handler.Timeout = time.Duration(m.Timeout)
|
||||
handler.BaudRate = m.BaudRate
|
||||
handler.DataBits = m.DataBits
|
||||
handler.Parity = m.Parity
|
||||
handler.StopBits = m.StopBits
|
||||
m.handler = handler
|
||||
default:
|
||||
return fmt.Errorf("invalid protocol '%s' - '%s' ", u.Scheme, m.TransmissionMode)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("invalid controller %q", m.Controller)
|
||||
}
|
||||
|
||||
// Add the metrics grouped by series to the accumulator
|
||||
for _, metric := range grouper.Metrics() {
|
||||
acc.AddMetric(metric)
|
||||
m.handler.SetSlave(m.SlaveID)
|
||||
m.client = mb.NewClient(m.handler)
|
||||
m.isConnected = false
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Connect to a MODBUS Slave device via Modbus/[TCP|RTU|ASCII]
|
||||
func (m *Modbus) connect() error {
|
||||
err := m.handler.Connect()
|
||||
m.isConnected = err == nil
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Modbus) disconnect() error {
|
||||
err := m.handler.Close()
|
||||
m.isConnected = false
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *Modbus) gatherFields() error {
|
||||
for _, requests := range m.requests {
|
||||
if err := m.gatherRequestsCoil(requests.coil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.gatherRequestsDiscrete(requests.discrete); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.gatherRequestsHolding(requests.holding); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := m.gatherRequestsInput(requests.input); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Modbus) gatherRequestsCoil(requests []request) error {
|
||||
for _, request := range requests {
|
||||
m.Log.Debugf("trying to read coil@%v[%v]...", request.address, request.length)
|
||||
bytes, err := m.client.ReadCoils(request.address, request.length)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Log.Debugf("got coil@%v[%v]: %v", request.address, request.length, bytes)
|
||||
|
||||
// Bit value handling
|
||||
for i, field := range request.fields {
|
||||
offset := field.address - request.address
|
||||
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)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Modbus) gatherRequestsDiscrete(requests []request) error {
|
||||
for _, request := range requests {
|
||||
m.Log.Debugf("trying to read discrete@%v[%v]...", request.address, request.length)
|
||||
bytes, err := m.client.ReadDiscreteInputs(request.address, request.length)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Log.Debugf("got discrete@%v[%v]: %v", request.address, request.length, bytes)
|
||||
|
||||
// Bit value handling
|
||||
for i, field := range request.fields {
|
||||
offset := field.address - request.address
|
||||
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)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Modbus) gatherRequestsHolding(requests []request) error {
|
||||
for _, request := range requests {
|
||||
m.Log.Debugf("trying to read holding@%v[%v]...", request.address, request.length)
|
||||
bytes, err := m.client.ReadHoldingRegisters(request.address, request.length)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Log.Debugf("got holding@%v[%v]: %v", request.address, request.length, bytes)
|
||||
|
||||
// Non-bit value handling
|
||||
for i, field := range request.fields {
|
||||
// Determine the offset of the field values in the read array
|
||||
offset := 2 * (field.address - request.address) // registers are 16bit = 2 byte
|
||||
length := 2 * field.length // field length is in registers a 16bit
|
||||
|
||||
// Convert the actual value
|
||||
request.fields[i].value = field.converter(bytes[offset : offset+length])
|
||||
m.Log.Debugf(" field %s with offset %d with len %d: %v --> %v", field.name, offset, length, bytes[offset:offset+length], request.fields[i].value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Modbus) gatherRequestsInput(requests []request) error {
|
||||
for _, request := range requests {
|
||||
m.Log.Debugf("trying to read input@%v[%v]...", request.address, request.length)
|
||||
bytes, err := m.client.ReadInputRegisters(request.address, request.length)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Log.Debugf("got input@%v[%v]: %v", request.address, request.length, bytes)
|
||||
|
||||
// Non-bit value handling
|
||||
for i, field := range request.fields {
|
||||
// Determine the offset of the field values in the read array
|
||||
offset := 2 * (field.address - request.address) // registers are 16bit = 2 byte
|
||||
length := 2 * field.length // field length is in registers a 16bit
|
||||
|
||||
// Convert the actual value
|
||||
request.fields[i].value = field.converter(bytes[offset : offset+length])
|
||||
m.Log.Debugf(" field %s with offset %d with len %d: %v --> %v", field.name, offset, length, bytes[offset:offset+length], request.fields[i].value)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Modbus) collectFields(acc telegraf.Accumulator, timestamp time.Time, tags map[string]string, requests []request) {
|
||||
grouper := metric.NewSeriesGrouper()
|
||||
for _, request := range requests {
|
||||
for _, field := range request.fields {
|
||||
// In case no measurement was specified we use "modbus" as default
|
||||
measurement := "modbus"
|
||||
if field.measurement != "" {
|
||||
measurement = field.measurement
|
||||
}
|
||||
|
||||
// Group the data by series
|
||||
if err := grouper.Add(measurement, tags, timestamp, field.name, field.value); err != nil {
|
||||
acc.AddError(fmt.Errorf("cannot add field %q for measurement %q: %v", field.name, measurement, err))
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the metrics grouped by series to the accumulator
|
||||
for _, x := range grouper.Metrics() {
|
||||
acc.AddMetric(x)
|
||||
}
|
||||
}
|
||||
|
||||
// Add this plugin to telegraf
|
||||
func init() {
|
||||
inputs.Add("modbus", func() telegraf.Input { return &Modbus{} })
|
||||
|
|
|
|||
|
|
@ -2,12 +2,15 @@ package modbus
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
m "github.com/goburrow/modbus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
mb "github.com/grid-x/modbus"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/tbrandon/mbserver"
|
||||
|
||||
"github.com/influxdata/telegraf"
|
||||
"github.com/influxdata/telegraf/testutil"
|
||||
)
|
||||
|
||||
|
|
@ -78,44 +81,52 @@ func TestCoils(t *testing.T) {
|
|||
}
|
||||
|
||||
serv := mbserver.NewServer()
|
||||
err := serv.ListenTCP("localhost:1502")
|
||||
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||
defer serv.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
handler := m.NewTCPClientHandler("localhost:1502")
|
||||
err = handler.Connect()
|
||||
assert.NoError(t, err)
|
||||
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||
require.NoError(t, handler.Connect())
|
||||
defer handler.Close()
|
||||
client := m.NewClient(handler)
|
||||
client := mb.NewClient(handler)
|
||||
|
||||
for _, ct := range coilTests {
|
||||
t.Run(ct.name, func(t *testing.T) {
|
||||
_, err = client.WriteMultipleCoils(ct.address, ct.quantity, ct.write)
|
||||
assert.NoError(t, err)
|
||||
_, err := client.WriteMultipleCoils(ct.address, ct.quantity, ct.write)
|
||||
require.NoError(t, err)
|
||||
|
||||
modbus := Modbus{
|
||||
Name: "TestCoils",
|
||||
Controller: "tcp://localhost:1502",
|
||||
SlaveID: 1,
|
||||
Coils: []fieldContainer{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.Coils = []fieldDefinition{
|
||||
{
|
||||
Name: ct.name,
|
||||
Address: []uint16{ct.address},
|
||||
},
|
||||
}
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"modbus",
|
||||
map[string]string{
|
||||
"type": cCoils,
|
||||
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||
"name": modbus.Name,
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
map[string]interface{}{ct.name: ct.read},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
err = modbus.Init()
|
||||
assert.NoError(t, err)
|
||||
var acc testutil.Accumulator
|
||||
err = modbus.Gather(&acc)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, modbus.registers)
|
||||
require.NoError(t, modbus.Init())
|
||||
require.NotEmpty(t, modbus.requests)
|
||||
require.NoError(t, modbus.Gather(&acc))
|
||||
acc.Wait(len(expected))
|
||||
|
||||
for _, coil := range modbus.registers {
|
||||
assert.Equal(t, ct.read, coil.Fields[0].value)
|
||||
}
|
||||
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -614,26 +625,26 @@ func TestHoldingRegisters(t *testing.T) {
|
|||
}
|
||||
|
||||
serv := mbserver.NewServer()
|
||||
err := serv.ListenTCP("localhost:1502")
|
||||
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||
defer serv.Close()
|
||||
assert.NoError(t, err)
|
||||
|
||||
handler := m.NewTCPClientHandler("localhost:1502")
|
||||
err = handler.Connect()
|
||||
assert.NoError(t, err)
|
||||
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||
require.NoError(t, handler.Connect())
|
||||
defer handler.Close()
|
||||
client := m.NewClient(handler)
|
||||
client := mb.NewClient(handler)
|
||||
|
||||
for _, hrt := range holdingRegisterTests {
|
||||
t.Run(hrt.name, func(t *testing.T) {
|
||||
_, err = client.WriteMultipleRegisters(hrt.address[0], hrt.quantity, hrt.write)
|
||||
assert.NoError(t, err)
|
||||
_, err := client.WriteMultipleRegisters(hrt.address[0], hrt.quantity, hrt.write)
|
||||
require.NoError(t, err)
|
||||
|
||||
modbus := Modbus{
|
||||
Name: "TestHoldingRegisters",
|
||||
Controller: "tcp://localhost:1502",
|
||||
SlaveID: 1,
|
||||
HoldingRegisters: []fieldContainer{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.HoldingRegisters = []fieldDefinition{
|
||||
{
|
||||
Name: hrt.name,
|
||||
ByteOrder: hrt.byteOrder,
|
||||
|
|
@ -641,88 +652,264 @@ func TestHoldingRegisters(t *testing.T) {
|
|||
Scale: hrt.scale,
|
||||
Address: hrt.address,
|
||||
},
|
||||
}
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"modbus",
|
||||
map[string]string{
|
||||
"type": cHoldingRegisters,
|
||||
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||
"name": modbus.Name,
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
map[string]interface{}{hrt.name: hrt.read},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
err = modbus.Init()
|
||||
assert.NoError(t, err)
|
||||
var acc testutil.Accumulator
|
||||
assert.NoError(t, modbus.Gather(&acc))
|
||||
assert.NotEmpty(t, modbus.registers)
|
||||
require.NoError(t, modbus.Init())
|
||||
require.NotEmpty(t, modbus.requests)
|
||||
require.NoError(t, modbus.Gather(&acc))
|
||||
acc.Wait(len(expected))
|
||||
|
||||
for _, coil := range modbus.registers {
|
||||
assert.Equal(t, hrt.read, coil.Fields[0].value)
|
||||
}
|
||||
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadMultipleCoilLimit(t *testing.T) {
|
||||
func TestReadMultipleCoilWithHole(t *testing.T) {
|
||||
serv := mbserver.NewServer()
|
||||
err := serv.ListenTCP("localhost:1502")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||
defer serv.Close()
|
||||
|
||||
handler := m.NewTCPClientHandler("localhost:1502")
|
||||
err = handler.Connect()
|
||||
assert.NoError(t, err)
|
||||
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||
require.NoError(t, handler.Connect())
|
||||
defer handler.Close()
|
||||
client := m.NewClient(handler)
|
||||
client := mb.NewClient(handler)
|
||||
|
||||
fcs := []fieldContainer{}
|
||||
fcs := []fieldDefinition{}
|
||||
expectedFields := make(map[string]interface{})
|
||||
writeValue := uint16(0)
|
||||
for i := 0; i <= 4000; i++ {
|
||||
fc := fieldContainer{}
|
||||
readValue := uint16(0)
|
||||
for i := 0; i < 14; i++ {
|
||||
fc := fieldDefinition{}
|
||||
fc.Name = fmt.Sprintf("coil-%v", i)
|
||||
fc.Address = []uint16{uint16(i)}
|
||||
fcs = append(fcs, fc)
|
||||
|
||||
t.Run(fc.Name, func(t *testing.T) {
|
||||
_, err = client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
_, err := client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFields[fc.Name] = readValue
|
||||
writeValue = 65280 - writeValue
|
||||
readValue = 1 - readValue
|
||||
}
|
||||
for i := 15; i < 18; i++ {
|
||||
fc := fieldDefinition{}
|
||||
fc.Name = fmt.Sprintf("coil-%v", i)
|
||||
fc.Address = []uint16{uint16(i)}
|
||||
fcs = append(fcs, fc)
|
||||
|
||||
_, err := client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFields[fc.Name] = readValue
|
||||
writeValue = 65280 - writeValue
|
||||
readValue = 1 - readValue
|
||||
}
|
||||
for i := 24; i < 33; i++ {
|
||||
fc := fieldDefinition{}
|
||||
fc.Name = fmt.Sprintf("coil-%v", i)
|
||||
fc.Address = []uint16{uint16(i)}
|
||||
fcs = append(fcs, fc)
|
||||
|
||||
_, err := client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFields[fc.Name] = readValue
|
||||
writeValue = 65280 - writeValue
|
||||
readValue = 1 - readValue
|
||||
}
|
||||
require.Len(t, expectedFields, len(fcs))
|
||||
|
||||
modbus := Modbus{
|
||||
Name: "TestReadMultipleCoilWithHole",
|
||||
Controller: "tcp://localhost:1502",
|
||||
Log: testutil.Logger{Name: "modbus:MultipleCoilWithHole"},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.Coils = fcs
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"modbus",
|
||||
map[string]string{
|
||||
"type": cCoils,
|
||||
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||
"name": modbus.Name,
|
||||
},
|
||||
expectedFields,
|
||||
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 TestReadMultipleCoilLimit(t *testing.T) {
|
||||
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)
|
||||
|
||||
fcs := []fieldDefinition{}
|
||||
expectedFields := make(map[string]interface{})
|
||||
writeValue := uint16(0)
|
||||
readValue := uint16(0)
|
||||
for i := 0; i < 4000; i++ {
|
||||
fc := fieldDefinition{}
|
||||
fc.Name = fmt.Sprintf("coil-%v", i)
|
||||
fc.Address = []uint16{uint16(i)}
|
||||
fcs = append(fcs, fc)
|
||||
|
||||
_, err := client.WriteSingleCoil(fc.Address[0], writeValue)
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFields[fc.Name] = readValue
|
||||
writeValue = 65280 - writeValue
|
||||
readValue = 1 - readValue
|
||||
}
|
||||
require.Len(t, expectedFields, len(fcs))
|
||||
|
||||
modbus := Modbus{
|
||||
Name: "TestReadCoils",
|
||||
Controller: "tcp://localhost:1502",
|
||||
SlaveID: 1,
|
||||
Coils: fcs,
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.Coils = fcs
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"modbus",
|
||||
map[string]string{
|
||||
"type": cCoils,
|
||||
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||
"name": modbus.Name,
|
||||
},
|
||||
expectedFields,
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
err = modbus.Init()
|
||||
assert.NoError(t, err)
|
||||
var acc testutil.Accumulator
|
||||
err = modbus.Gather(&acc)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, modbus.Init())
|
||||
require.NotEmpty(t, modbus.requests)
|
||||
require.NoError(t, modbus.Gather(&acc))
|
||||
acc.Wait(len(expected))
|
||||
|
||||
writeValue = 0
|
||||
for i := 0; i <= 4000; i++ {
|
||||
t.Run(modbus.registers[0].Fields[i].Name, func(t *testing.T) {
|
||||
assert.Equal(t, writeValue, modbus.registers[0].Fields[i].value)
|
||||
writeValue = 1 - writeValue
|
||||
})
|
||||
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||
}
|
||||
|
||||
func TestReadMultipleHoldingRegisterWithHole(t *testing.T) {
|
||||
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)
|
||||
|
||||
fcs := []fieldDefinition{}
|
||||
expectedFields := make(map[string]interface{})
|
||||
for i := 0; i < 10; i++ {
|
||||
fc := fieldDefinition{
|
||||
Name: fmt.Sprintf("HoldingRegister-%v", i),
|
||||
ByteOrder: "AB",
|
||||
DataType: "INT16",
|
||||
Scale: 1.0,
|
||||
Address: []uint16{uint16(i)},
|
||||
}
|
||||
fcs = append(fcs, fc)
|
||||
|
||||
_, err := client.WriteSingleRegister(fc.Address[0], uint16(i))
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFields[fc.Name] = int64(i)
|
||||
}
|
||||
for i := 20; i < 30; i++ {
|
||||
fc := fieldDefinition{
|
||||
Name: fmt.Sprintf("HoldingRegister-%v", i),
|
||||
ByteOrder: "AB",
|
||||
DataType: "INT16",
|
||||
Scale: 1.0,
|
||||
Address: []uint16{uint16(i)},
|
||||
}
|
||||
fcs = append(fcs, fc)
|
||||
|
||||
_, err := client.WriteSingleRegister(fc.Address[0], uint16(i))
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFields[fc.Name] = int64(i)
|
||||
}
|
||||
require.Len(t, expectedFields, len(fcs))
|
||||
|
||||
modbus := Modbus{
|
||||
Name: "TestHoldingRegister",
|
||||
Controller: "tcp://localhost:1502",
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.HoldingRegisters = fcs
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"modbus",
|
||||
map[string]string{
|
||||
"type": cHoldingRegisters,
|
||||
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||
"name": modbus.Name,
|
||||
},
|
||||
expectedFields,
|
||||
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 TestReadMultipleHoldingRegisterLimit(t *testing.T) {
|
||||
serv := mbserver.NewServer()
|
||||
err := serv.ListenTCP("localhost:1502")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||
defer serv.Close()
|
||||
|
||||
handler := m.NewTCPClientHandler("localhost:1502")
|
||||
err = handler.Connect()
|
||||
assert.NoError(t, err)
|
||||
handler := mb.NewTCPClientHandler("localhost:1502")
|
||||
require.NoError(t, handler.Connect())
|
||||
defer handler.Close()
|
||||
client := m.NewClient(handler)
|
||||
client := mb.NewClient(handler)
|
||||
|
||||
fcs := []fieldContainer{}
|
||||
fcs := []fieldDefinition{}
|
||||
expectedFields := make(map[string]interface{})
|
||||
for i := 0; i <= 400; i++ {
|
||||
fc := fieldContainer{}
|
||||
fc := fieldDefinition{}
|
||||
fc.Name = fmt.Sprintf("HoldingRegister-%v", i)
|
||||
fc.ByteOrder = "AB"
|
||||
fc.DataType = "INT16"
|
||||
|
|
@ -730,28 +917,40 @@ func TestReadMultipleHoldingRegisterLimit(t *testing.T) {
|
|||
fc.Address = []uint16{uint16(i)}
|
||||
fcs = append(fcs, fc)
|
||||
|
||||
t.Run(fc.Name, func(t *testing.T) {
|
||||
_, err = client.WriteSingleRegister(fc.Address[0], uint16(i))
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
_, err := client.WriteSingleRegister(fc.Address[0], uint16(i))
|
||||
require.NoError(t, err)
|
||||
|
||||
expectedFields[fc.Name] = int64(i)
|
||||
}
|
||||
|
||||
modbus := Modbus{
|
||||
Name: "TestHoldingRegister",
|
||||
Controller: "tcp://localhost:1502",
|
||||
SlaveID: 1,
|
||||
HoldingRegisters: fcs,
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.HoldingRegisters = fcs
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"modbus",
|
||||
map[string]string{
|
||||
"type": cHoldingRegisters,
|
||||
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||
"name": modbus.Name,
|
||||
},
|
||||
expectedFields,
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
err = modbus.Init()
|
||||
assert.NoError(t, err)
|
||||
var acc testutil.Accumulator
|
||||
err = modbus.Gather(&acc)
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, modbus.Init())
|
||||
require.NotEmpty(t, modbus.requests)
|
||||
require.NoError(t, modbus.Gather(&acc))
|
||||
acc.Wait(len(expected))
|
||||
|
||||
for i := 0; i <= 400; i++ {
|
||||
assert.Equal(t, int16(i), modbus.registers[0].Fields[i].value)
|
||||
}
|
||||
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||
}
|
||||
|
||||
func TestRetrySuccessful(t *testing.T) {
|
||||
|
|
@ -760,8 +959,7 @@ func TestRetrySuccessful(t *testing.T) {
|
|||
value := 1
|
||||
|
||||
serv := mbserver.NewServer()
|
||||
err := serv.ListenTCP("localhost:1502")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||
defer serv.Close()
|
||||
|
||||
// Make read on coil-registers fail for some trials by making the device
|
||||
|
|
@ -781,40 +979,47 @@ func TestRetrySuccessful(t *testing.T) {
|
|||
return data, except
|
||||
})
|
||||
|
||||
t.Run("retry_success", func(t *testing.T) {
|
||||
modbus := Modbus{
|
||||
Name: "TestRetry",
|
||||
Controller: "tcp://localhost:1502",
|
||||
SlaveID: 1,
|
||||
Retries: maxretries,
|
||||
Coils: []fieldContainer{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.Coils = []fieldDefinition{
|
||||
{
|
||||
Name: "retry_success",
|
||||
Address: []uint16{0},
|
||||
},
|
||||
}
|
||||
|
||||
expected := []telegraf.Metric{
|
||||
testutil.MustMetric(
|
||||
"modbus",
|
||||
map[string]string{
|
||||
"type": cCoils,
|
||||
"slave_id": strconv.Itoa(int(modbus.SlaveID)),
|
||||
"name": modbus.Name,
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
map[string]interface{}{"retry_success": uint16(value)},
|
||||
time.Unix(0, 0),
|
||||
),
|
||||
}
|
||||
|
||||
err = modbus.Init()
|
||||
assert.NoError(t, err)
|
||||
var acc testutil.Accumulator
|
||||
err = modbus.Gather(&acc)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, modbus.registers)
|
||||
require.NoError(t, modbus.Init())
|
||||
require.NotEmpty(t, modbus.requests)
|
||||
require.NoError(t, modbus.Gather(&acc))
|
||||
acc.Wait(len(expected))
|
||||
|
||||
for _, coil := range modbus.registers {
|
||||
assert.Equal(t, uint16(value), coil.Fields[0].value)
|
||||
}
|
||||
})
|
||||
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
|
||||
}
|
||||
|
||||
func TestRetryFail(t *testing.T) {
|
||||
func TestRetryFailExhausted(t *testing.T) {
|
||||
maxretries := 2
|
||||
|
||||
serv := mbserver.NewServer()
|
||||
err := serv.ListenTCP("localhost:1502")
|
||||
assert.NoError(t, err)
|
||||
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||
defer serv.Close()
|
||||
|
||||
// Make the read on coils fail with busy
|
||||
|
|
@ -827,27 +1032,35 @@ func TestRetryFail(t *testing.T) {
|
|||
return data, &mbserver.SlaveDeviceBusy
|
||||
})
|
||||
|
||||
t.Run("retry_fail", func(t *testing.T) {
|
||||
modbus := Modbus{
|
||||
Name: "TestRetryFail",
|
||||
Name: "TestRetryFailExhausted",
|
||||
Controller: "tcp://localhost:1502",
|
||||
SlaveID: 1,
|
||||
Retries: maxretries,
|
||||
Coils: []fieldContainer{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.Coils = []fieldDefinition{
|
||||
{
|
||||
Name: "retry_fail",
|
||||
Address: []uint16{0},
|
||||
},
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err = modbus.Init()
|
||||
assert.NoError(t, err)
|
||||
var acc testutil.Accumulator
|
||||
err = modbus.Gather(&acc)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
require.NoError(t, modbus.Init())
|
||||
require.NotEmpty(t, modbus.requests)
|
||||
|
||||
err := modbus.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "modbus: exception '6' (server device busy), function '129'", err.Error())
|
||||
}
|
||||
|
||||
func TestRetryFailIllegal(t *testing.T) {
|
||||
maxretries := 2
|
||||
|
||||
serv := mbserver.NewServer()
|
||||
require.NoError(t, serv.ListenTCP("localhost:1502"))
|
||||
defer serv.Close()
|
||||
|
||||
// Make the read on coils fail with illegal function preventing retry
|
||||
counter := 0
|
||||
|
|
@ -861,26 +1074,26 @@ func TestRetryFail(t *testing.T) {
|
|||
return data, &mbserver.IllegalFunction
|
||||
})
|
||||
|
||||
t.Run("retry_fail", func(t *testing.T) {
|
||||
modbus := Modbus{
|
||||
Name: "TestRetryFail",
|
||||
Name: "TestRetryFailExhausted",
|
||||
Controller: "tcp://localhost:1502",
|
||||
SlaveID: 1,
|
||||
Retries: maxretries,
|
||||
Coils: []fieldContainer{
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
modbus.SlaveID = 1
|
||||
modbus.Coils = []fieldDefinition{
|
||||
{
|
||||
Name: "retry_fail",
|
||||
Address: []uint16{0},
|
||||
},
|
||||
},
|
||||
Log: testutil.Logger{},
|
||||
}
|
||||
|
||||
err = modbus.Init()
|
||||
assert.NoError(t, err)
|
||||
var acc testutil.Accumulator
|
||||
err = modbus.Gather(&acc)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, counter, 1)
|
||||
})
|
||||
require.NoError(t, modbus.Init())
|
||||
require.NotEmpty(t, modbus.requests)
|
||||
|
||||
err := modbus.Gather(&acc)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, "modbus: exception '1' (illegal function), function '129'", err.Error())
|
||||
require.Equal(t, counter, 1)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,58 @@
|
|||
package modbus
|
||||
|
||||
import "sort"
|
||||
|
||||
type request struct {
|
||||
address uint16
|
||||
length uint16
|
||||
fields []field
|
||||
}
|
||||
|
||||
func newRequestsFromFields(fields []field, slaveID byte, registerType string, maxBatchSize uint16) []request {
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Sort the fields by address (ascending) and length
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
addrI := fields[i].address
|
||||
addrJ := fields[j].address
|
||||
return addrI < addrJ || (addrI == addrJ && fields[i].length > fields[j].length)
|
||||
})
|
||||
|
||||
// Construct the consecutive register chunks for the addresses and construct Modbus requests.
|
||||
// For field addresses like [1, 2, 3, 5, 6, 10, 11, 12, 14] we should construct the following
|
||||
// requests (1, 3) , (5, 2) , (10, 3), (14 , 1). Furthermore, we should respect field boundaries
|
||||
// and the given maximum chunk sizes.
|
||||
var requests []request
|
||||
|
||||
current := request{
|
||||
address: fields[0].address,
|
||||
length: fields[0].length,
|
||||
fields: []field{fields[0]},
|
||||
}
|
||||
|
||||
for _, f := range fields[1:] {
|
||||
// Check if we need to interrupt the current chunk and require a new one
|
||||
needInterrupt := f.address != current.address+current.length // not consecutive
|
||||
needInterrupt = needInterrupt || f.length+current.length > maxBatchSize // too large
|
||||
|
||||
if !needInterrupt {
|
||||
// Still save to add the field to the current request
|
||||
current.length += f.length
|
||||
current.fields = append(current.fields, f) // TODO: omit the field with a future flag
|
||||
continue
|
||||
}
|
||||
|
||||
// Finish the current request, add it to the list and construct a new one
|
||||
requests = append(requests, current)
|
||||
current = request{
|
||||
address: f.address,
|
||||
length: f.length,
|
||||
fields: []field{f},
|
||||
}
|
||||
}
|
||||
requests = append(requests, current)
|
||||
|
||||
return requests
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package modbus
|
||||
|
||||
import "fmt"
|
||||
|
||||
func determineConverter(inType, byteOrder, outType string, scale float64) (fieldConverterFunc, error) {
|
||||
if scale != 0.0 {
|
||||
return determineConverterScale(inType, byteOrder, outType, scale)
|
||||
}
|
||||
return determineConverterNoScale(inType, byteOrder, outType)
|
||||
}
|
||||
|
||||
func determineConverterScale(inType, byteOrder, outType string, scale float64) (fieldConverterFunc, error) {
|
||||
switch inType {
|
||||
case "INT16":
|
||||
return determineConverterI16Scale(outType, byteOrder, scale)
|
||||
case "UINT16":
|
||||
return determineConverterU16Scale(outType, byteOrder, scale)
|
||||
case "INT32":
|
||||
return determineConverterI32Scale(outType, byteOrder, scale)
|
||||
case "UINT32":
|
||||
return determineConverterU32Scale(outType, byteOrder, scale)
|
||||
case "INT64":
|
||||
return determineConverterI64Scale(outType, byteOrder, scale)
|
||||
case "UINT64":
|
||||
return determineConverterU64Scale(outType, byteOrder, scale)
|
||||
case "FLOAT32":
|
||||
return determineConverterF32Scale(outType, byteOrder, scale)
|
||||
case "FLOAT64":
|
||||
return determineConverterF64Scale(outType, byteOrder, scale)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid input data-type: %s", inType)
|
||||
}
|
||||
|
||||
func determineConverterNoScale(inType, byteOrder, outType string) (fieldConverterFunc, error) {
|
||||
switch inType {
|
||||
case "INT16":
|
||||
return determineConverterI16(outType, byteOrder)
|
||||
case "UINT16":
|
||||
return determineConverterU16(outType, byteOrder)
|
||||
case "INT32":
|
||||
return determineConverterI32(outType, byteOrder)
|
||||
case "UINT32":
|
||||
return determineConverterU32(outType, byteOrder)
|
||||
case "INT64":
|
||||
return determineConverterI64(outType, byteOrder)
|
||||
case "UINT64":
|
||||
return determineConverterU64(outType, byteOrder)
|
||||
case "FLOAT32":
|
||||
return determineConverterF32(outType, byteOrder)
|
||||
case "FLOAT64":
|
||||
return determineConverterF64(outType, byteOrder)
|
||||
}
|
||||
return nil, fmt.Errorf("invalid input data-type: %s", inType)
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
package modbus
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type convert16 func([]byte) uint16
|
||||
|
||||
func endianessConverter16(byteOrder string) (convert16, error) {
|
||||
switch byteOrder {
|
||||
case "ABCD": // Big endian (Motorola)
|
||||
return binary.BigEndian.Uint16, nil
|
||||
case "DCBA": // Little endian (Intel)
|
||||
return binary.LittleEndian.Uint16, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid byte-order: %s", byteOrder)
|
||||
}
|
||||
|
||||
// I16 - no scale
|
||||
func determineConverterI16(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter16(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
return int16(tohost(b))
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
return int64(int16(tohost(b)))
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
return uint64(int16(tohost(b)))
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
return float64(int16(tohost(b)))
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// U16 - no scale
|
||||
func determineConverterU16(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter16(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
return tohost(b)
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
return int64(tohost(b))
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
return uint64(tohost(b))
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
return float64(tohost(b))
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// I16 - scale
|
||||
func determineConverterI16Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter16(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
in := int16(tohost(b))
|
||||
return int16(float64(in) * scale)
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int16(tohost(b))
|
||||
return int64(float64(in) * scale)
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int16(tohost(b))
|
||||
return uint64(float64(in) * scale)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int16(tohost(b))
|
||||
return float64(in) * scale
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// U16 - scale
|
||||
func determineConverterU16Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter16(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return uint16(float64(in) * scale)
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return int64(float64(in) * scale)
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return uint64(float64(in) * scale)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return float64(in) * scale
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
|
@ -0,0 +1,200 @@
|
|||
package modbus
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type convert32 func([]byte) uint32
|
||||
|
||||
func binaryMSWLEU32(b []byte) uint32 {
|
||||
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint32(binary.LittleEndian.Uint16(b[0:]))<<16 | uint32(binary.LittleEndian.Uint16(b[2:]))
|
||||
}
|
||||
|
||||
func binaryLSWBEU32(b []byte) uint32 {
|
||||
_ = b[3] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint32(binary.BigEndian.Uint16(b[2:]))<<16 | uint32(binary.BigEndian.Uint16(b[0:]))
|
||||
}
|
||||
|
||||
func endianessConverter32(byteOrder string) (convert32, error) {
|
||||
switch byteOrder {
|
||||
case "ABCD": // Big endian (Motorola)
|
||||
return binary.BigEndian.Uint32, nil
|
||||
case "BADC": // Big endian with bytes swapped
|
||||
return binaryMSWLEU32, nil
|
||||
case "CDAB": // Little endian with bytes swapped
|
||||
return binaryLSWBEU32, nil
|
||||
case "DCBA": // Little endian (Intel)
|
||||
return binary.LittleEndian.Uint32, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid byte-order: %s", byteOrder)
|
||||
}
|
||||
|
||||
// I32 - no scale
|
||||
func determineConverterI32(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter32(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
return int32(tohost(b))
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
return int64(int32(tohost(b)))
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
return uint64(int32(tohost(b)))
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
return float64(int32(tohost(b)))
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// U32 - no scale
|
||||
func determineConverterU32(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter32(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
return tohost(b)
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
return int64(tohost(b))
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
return uint64(tohost(b))
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
return float64(tohost(b))
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// F32 - no scale
|
||||
func determineConverterF32(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter32(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
raw := tohost(b)
|
||||
return math.Float32frombits(raw)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
raw := tohost(b)
|
||||
in := math.Float32frombits(raw)
|
||||
return float64(in)
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// I32 - scale
|
||||
func determineConverterI32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter32(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
in := int32(tohost(b))
|
||||
return int32(float64(in) * scale)
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int32(tohost(b))
|
||||
return int64(float64(in) * scale)
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int32(tohost(b))
|
||||
return uint64(float64(in) * scale)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int32(tohost(b))
|
||||
return float64(in) * scale
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// U32 - scale
|
||||
func determineConverterU32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter32(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return uint32(float64(in) * scale)
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return int64(float64(in) * scale)
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return uint64(float64(in) * scale)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return float64(in) * scale
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// F32 - scale
|
||||
func determineConverterF32Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter32(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
raw := tohost(b)
|
||||
in := math.Float32frombits(raw)
|
||||
return float32(float64(in) * scale)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
raw := tohost(b)
|
||||
in := math.Float32frombits(raw)
|
||||
return float64(in) * scale
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package modbus
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
)
|
||||
|
||||
type convert64 func([]byte) uint64
|
||||
|
||||
func binaryMSWLEU64(b []byte) uint64 {
|
||||
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint64(binary.LittleEndian.Uint16(b[0:]))<<48 | uint64(binary.LittleEndian.Uint16(b[2:]))<<32 | uint64(binary.LittleEndian.Uint16(b[4:]))<<16 | uint64(binary.LittleEndian.Uint16(b[6:]))
|
||||
}
|
||||
|
||||
func binaryLSWBEU64(b []byte) uint64 {
|
||||
_ = b[7] // bounds check hint to compiler; see golang.org/issue/14808
|
||||
return uint64(binary.BigEndian.Uint16(b[6:]))<<48 | uint64(binary.BigEndian.Uint16(b[4:]))<<32 | uint64(binary.BigEndian.Uint16(b[2:]))<<16 | uint64(binary.BigEndian.Uint16(b[0:]))
|
||||
}
|
||||
|
||||
func endianessConverter64(byteOrder string) (convert64, error) {
|
||||
switch byteOrder {
|
||||
case "ABCD": // Big endian (Motorola)
|
||||
return binary.BigEndian.Uint64, nil
|
||||
case "BADC": // Big endian with bytes swapped
|
||||
return binaryMSWLEU64, nil
|
||||
case "CDAB": // Little endian with bytes swapped
|
||||
return binaryLSWBEU64, nil
|
||||
case "DCBA": // Little endian (Intel)
|
||||
return binary.LittleEndian.Uint64, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid byte-order: %s", byteOrder)
|
||||
}
|
||||
|
||||
// I64 - no scale
|
||||
func determineConverterI64(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter64(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native", "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
return int64(tohost(b))
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int64(tohost(b))
|
||||
return uint64(in)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int64(tohost(b))
|
||||
return float64(in)
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// U64 - no scale
|
||||
func determineConverterU64(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter64(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
return int64(tohost(b))
|
||||
}, nil
|
||||
case "native", "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
return tohost(b)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
return float64(tohost(b))
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// F64 - no scale
|
||||
func determineConverterF64(outType, byteOrder string) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter64(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native", "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
raw := tohost(b)
|
||||
return math.Float64frombits(raw)
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// I64 - scale
|
||||
func determineConverterI64Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter64(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
in := int64(tohost(b))
|
||||
return int64(float64(in) * scale)
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int64(tohost(b))
|
||||
return int64(float64(in) * scale)
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int64(tohost(b))
|
||||
return uint64(float64(in) * scale)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := int64(tohost(b))
|
||||
return float64(in) * scale
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// U64 - scale
|
||||
func determineConverterU64Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter64(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return uint64(float64(in) * scale)
|
||||
}, nil
|
||||
case "INT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return int64(float64(in) * scale)
|
||||
}, nil
|
||||
case "UINT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return uint64(float64(in) * scale)
|
||||
}, nil
|
||||
case "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
in := tohost(b)
|
||||
return float64(in) * scale
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
|
||||
// F64 - scale
|
||||
func determineConverterF64Scale(outType, byteOrder string, scale float64) (fieldConverterFunc, error) {
|
||||
tohost, err := endianessConverter64(byteOrder)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch outType {
|
||||
case "native", "FLOAT64":
|
||||
return func(b []byte) interface{} {
|
||||
raw := tohost(b)
|
||||
in := math.Float64frombits(raw)
|
||||
return in * scale
|
||||
}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("invalid output data-type: %s", outType)
|
||||
}
|
||||
Loading…
Reference in New Issue