Modbus refactor (#9141)

This commit is contained in:
Sven Rebhan 2021-05-27 22:58:46 +02:00 committed by GitHub
parent 58479fdb05
commit 2e7b232073
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 1598 additions and 721 deletions

View File

@ -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-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-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/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/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/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) - 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/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/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/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/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/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) - github.com/harlow/kinesis-consumer [MIT License](https://github.com/harlow/kinesis-consumer/blob/master/MIT-LICENSE)

3
go.mod
View File

@ -53,7 +53,7 @@ require (
github.com/go-ping/ping v0.0.0-20210201095549-52eed920f98c github.com/go-ping/ping v0.0.0-20210201095549-52eed920f98c
github.com/go-redis/redis v6.15.9+incompatible github.com/go-redis/redis v6.15.9+incompatible
github.com/go-sql-driver/mysql v1.5.0 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/goburrow/serial v0.1.0 // indirect
github.com/gobwas/glob v0.2.3 github.com/gobwas/glob v0.2.3
github.com/gofrs/uuid v3.3.0+incompatible github.com/gofrs/uuid v3.3.0+incompatible
@ -66,6 +66,7 @@ require (
github.com/gopcua/opcua v0.1.13 github.com/gopcua/opcua v0.1.13
github.com/gorilla/mux v1.7.3 github.com/gorilla/mux v1.7.3
github.com/gosnmp/gosnmp v1.32.0 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/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect
github.com/harlow/kinesis-consumer v0.3.1-0.20181230152818-2f58b136fee0 github.com/harlow/kinesis-consumer v0.3.1-0.20181230152818-2f58b136fee0

4
go.sum
View File

@ -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 h1:gctewmZx5qFI0oHMzRnjETqIZ093d9NgZy9TQr3V0iA=
github.com/gosnmp/gosnmp v1.32.0/go.mod h1:EIp+qkEpXoVsyZxXKy0AmXQx0mCHMMcIhXXvNDMpgF0= 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/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-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/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= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=

View File

@ -127,6 +127,20 @@ with N decimal places'.
(FLOAT32 is deprecated and should not be used any more. UFIXED provides the same conversion (FLOAT32 is deprecated and should not be used any more. UFIXED provides the same conversion
from unsigned values). 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 ### Example Output
```sh ```sh

View File

@ -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)
}

View File

@ -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)
}

View File

@ -1,15 +1,14 @@
package modbus package modbus
import ( import (
"encoding/binary"
"fmt" "fmt"
"math"
"net" "net"
"net/url" "net/url"
"sort" "strconv"
"time" "time"
mb "github.com/goburrow/modbus" mb "github.com/grid-x/modbus"
"github.com/influxdata/telegraf" "github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/config" "github.com/influxdata/telegraf/config"
"github.com/influxdata/telegraf/metric" "github.com/influxdata/telegraf/metric"
@ -25,42 +24,37 @@ type Modbus struct {
DataBits int `toml:"data_bits"` DataBits int `toml:"data_bits"`
Parity string `toml:"parity"` Parity string `toml:"parity"`
StopBits int `toml:"stop_bits"` StopBits int `toml:"stop_bits"`
SlaveID int `toml:"slave_id"`
Timeout config.Duration `toml:"timeout"` Timeout config.Duration `toml:"timeout"`
Retries int `toml:"busy_retries"` Retries int `toml:"busy_retries"`
RetriesWaitTime config.Duration `toml:"busy_retries_wait"` 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:"-"` Log telegraf.Logger `toml:"-"`
registers []register // Register configuration
isConnected bool ConfigurationOriginal
tcpHandler *mb.TCPClientHandler // Connection handling
rtuHandler *mb.RTUClientHandler
asciiHandler *mb.ASCIIClientHandler
client mb.Client client mb.Client
handler mb.ClientHandler
isConnected bool
// Request handling
requests map[byte]requestSet
} }
type register struct { type fieldConverterFunc func(bytes []byte) interface{}
Type string
RegistersRange []registerRange type requestSet struct {
Fields []fieldContainer coil []request
discrete []request
holding []request
input []request
} }
type fieldContainer struct { type field struct {
Measurement string `toml:"measurement"` measurement string
Name string `toml:"name"` name string
ByteOrder string `toml:"byte_order"` scale float64
DataType string `toml:"data_type"`
Scale float64 `toml:"scale"`
Address []uint16 `toml:"address"`
value interface{}
}
type registerRange struct {
address uint16 address uint16
length uint16 length uint16
converter fieldConverterFunc
value interface{}
} }
const ( const (
@ -173,500 +167,29 @@ func (m *Modbus) Init() error {
return fmt.Errorf("retries cannot be negative") return fmt.Errorf("retries cannot be negative")
} }
err := m.InitRegister(m.DiscreteInputs, cDiscreteInputs) // Check and process the configuration
if err != nil { if err := m.ConfigurationOriginal.Check(); err != nil {
return err return fmt.Errorf("original configuraton invalid: %v", err)
} }
err = m.InitRegister(m.Coils, cCoils) r, err := m.ConfigurationOriginal.Process()
if err != nil { if err != nil {
return err return fmt.Errorf("cannot process original configuraton: %v", err)
} }
m.requests = r
err = m.InitRegister(m.HoldingRegisters, cHoldingRegisters) // Setup client
if err != nil { if err := m.initClient(); err != nil {
return err return fmt.Errorf("initializing client failed: %v", err)
}
err = m.InitRegister(m.InputRegisters, cInputRegisters)
if err != nil {
return err
} }
return nil 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 // Gather implements the telegraf plugin interface method for data accumulation
func (m *Modbus) Gather(acc telegraf.Accumulator) error { func (m *Modbus) Gather(acc telegraf.Accumulator) error {
if !m.isConnected { if !m.isConnected {
err := connect(m) if err := m.connect(); err != nil {
if err != nil {
m.isConnected = false
return err return err
} }
} }
@ -674,53 +197,236 @@ func (m *Modbus) Gather(acc telegraf.Accumulator) error {
timestamp := time.Now() timestamp := time.Now()
for retry := 0; retry <= m.Retries; retry++ { for retry := 0; retry <= m.Retries; retry++ {
timestamp = time.Now() timestamp = time.Now()
err := m.getFields() if err := m.gatherFields(); err != nil {
if err != nil { if mberr, ok := err.(*mb.Error); ok && mberr.ExceptionCode == mb.ExceptionCodeServerDeviceBusy && retry < m.Retries {
mberr, ok := err.(*mb.ModbusError)
if ok && mberr.ExceptionCode == mb.ExceptionCodeServerDeviceBusy && retry < m.Retries {
m.Log.Infof("Device busy! Retrying %d more time(s)...", m.Retries-retry) m.Log.Infof("Device busy! Retrying %d more time(s)...", m.Retries-retry)
time.Sleep(time.Duration(m.RetriesWaitTime)) time.Sleep(time.Duration(m.RetriesWaitTime))
continue continue
} }
// Ignore return error to not shadow the initial error // Show the disconnect error this way to not shadow the initial error
//nolint:errcheck,revive if discerr := m.disconnect(); discerr != nil {
disconnect(m) m.Log.Errorf("Disconnecting failed: %v", discerr)
m.isConnected = false }
return err return err
} }
// Reading was successful, leave the retry loop // Reading was successful, leave the retry loop
break break
} }
grouper := metric.NewSeriesGrouper() for slaveID, requests := range m.requests {
for _, reg := range m.registers {
tags := map[string]string{ tags := map[string]string{
"name": m.Name, "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 { return nil
// In case no measurement was specified we use "modbus" as default }
measurement := "modbus"
if field.Measurement != "" {
measurement = field.Measurement
}
// Group the data by series func (m *Modbus) initClient() error {
if err := grouper.Add(measurement, tags, timestamp, field.Name, field.value); err != nil { u, err := url.Parse(m.Controller)
if err != nil {
return err 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 m.handler.SetSlave(m.SlaveID)
for _, metric := range grouper.Metrics() { m.client = mb.NewClient(m.handler)
acc.AddMetric(metric) 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 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 // Add this plugin to telegraf
func init() { func init() {
inputs.Add("modbus", func() telegraf.Input { return &Modbus{} }) inputs.Add("modbus", func() telegraf.Input { return &Modbus{} })

View File

@ -2,12 +2,15 @@ package modbus
import ( import (
"fmt" "fmt"
"strconv"
"testing" "testing"
"time"
m "github.com/goburrow/modbus" mb "github.com/grid-x/modbus"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/require"
"github.com/tbrandon/mbserver" "github.com/tbrandon/mbserver"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/testutil" "github.com/influxdata/telegraf/testutil"
) )
@ -78,44 +81,52 @@ func TestCoils(t *testing.T) {
} }
serv := mbserver.NewServer() serv := mbserver.NewServer()
err := serv.ListenTCP("localhost:1502") require.NoError(t, serv.ListenTCP("localhost:1502"))
defer serv.Close() defer serv.Close()
assert.NoError(t, err)
handler := m.NewTCPClientHandler("localhost:1502") handler := mb.NewTCPClientHandler("localhost:1502")
err = handler.Connect() require.NoError(t, handler.Connect())
assert.NoError(t, err)
defer handler.Close() defer handler.Close()
client := m.NewClient(handler) client := mb.NewClient(handler)
for _, ct := range coilTests { for _, ct := range coilTests {
t.Run(ct.name, func(t *testing.T) { t.Run(ct.name, func(t *testing.T) {
_, err = client.WriteMultipleCoils(ct.address, ct.quantity, ct.write) _, err := client.WriteMultipleCoils(ct.address, ct.quantity, ct.write)
assert.NoError(t, err) require.NoError(t, err)
modbus := Modbus{ modbus := Modbus{
Name: "TestCoils", Name: "TestCoils",
Controller: "tcp://localhost:1502", Controller: "tcp://localhost:1502",
SlaveID: 1, Log: testutil.Logger{},
Coils: []fieldContainer{ }
modbus.SlaveID = 1
modbus.Coils = []fieldDefinition{
{ {
Name: ct.name, Name: ct.name,
Address: []uint16{ct.address}, 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 var acc testutil.Accumulator
err = modbus.Gather(&acc) require.NoError(t, modbus.Init())
assert.NoError(t, err) require.NotEmpty(t, modbus.requests)
assert.NotEmpty(t, modbus.registers) require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
for _, coil := range modbus.registers { testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
assert.Equal(t, ct.read, coil.Fields[0].value)
}
}) })
} }
} }
@ -614,26 +625,26 @@ func TestHoldingRegisters(t *testing.T) {
} }
serv := mbserver.NewServer() serv := mbserver.NewServer()
err := serv.ListenTCP("localhost:1502") require.NoError(t, serv.ListenTCP("localhost:1502"))
defer serv.Close() defer serv.Close()
assert.NoError(t, err)
handler := m.NewTCPClientHandler("localhost:1502") handler := mb.NewTCPClientHandler("localhost:1502")
err = handler.Connect() require.NoError(t, handler.Connect())
assert.NoError(t, err)
defer handler.Close() defer handler.Close()
client := m.NewClient(handler) client := mb.NewClient(handler)
for _, hrt := range holdingRegisterTests { for _, hrt := range holdingRegisterTests {
t.Run(hrt.name, func(t *testing.T) { t.Run(hrt.name, func(t *testing.T) {
_, err = client.WriteMultipleRegisters(hrt.address[0], hrt.quantity, hrt.write) _, err := client.WriteMultipleRegisters(hrt.address[0], hrt.quantity, hrt.write)
assert.NoError(t, err) require.NoError(t, err)
modbus := Modbus{ modbus := Modbus{
Name: "TestHoldingRegisters", Name: "TestHoldingRegisters",
Controller: "tcp://localhost:1502", Controller: "tcp://localhost:1502",
SlaveID: 1, Log: testutil.Logger{},
HoldingRegisters: []fieldContainer{ }
modbus.SlaveID = 1
modbus.HoldingRegisters = []fieldDefinition{
{ {
Name: hrt.name, Name: hrt.name,
ByteOrder: hrt.byteOrder, ByteOrder: hrt.byteOrder,
@ -641,88 +652,264 @@ func TestHoldingRegisters(t *testing.T) {
Scale: hrt.scale, Scale: hrt.scale,
Address: hrt.address, 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 var acc testutil.Accumulator
assert.NoError(t, modbus.Gather(&acc)) require.NoError(t, modbus.Init())
assert.NotEmpty(t, modbus.registers) require.NotEmpty(t, modbus.requests)
require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
for _, coil := range modbus.registers { testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
assert.Equal(t, hrt.read, coil.Fields[0].value)
}
}) })
} }
} }
func TestReadMultipleCoilLimit(t *testing.T) { func TestReadMultipleCoilWithHole(t *testing.T) {
serv := mbserver.NewServer() serv := mbserver.NewServer()
err := serv.ListenTCP("localhost:1502") require.NoError(t, serv.ListenTCP("localhost:1502"))
assert.NoError(t, err)
defer serv.Close() defer serv.Close()
handler := m.NewTCPClientHandler("localhost:1502") handler := mb.NewTCPClientHandler("localhost:1502")
err = handler.Connect() require.NoError(t, handler.Connect())
assert.NoError(t, err)
defer handler.Close() defer handler.Close()
client := m.NewClient(handler) client := mb.NewClient(handler)
fcs := []fieldContainer{} fcs := []fieldDefinition{}
expectedFields := make(map[string]interface{})
writeValue := uint16(0) writeValue := uint16(0)
for i := 0; i <= 4000; i++ { readValue := uint16(0)
fc := fieldContainer{} for i := 0; i < 14; i++ {
fc := fieldDefinition{}
fc.Name = fmt.Sprintf("coil-%v", i) fc.Name = fmt.Sprintf("coil-%v", i)
fc.Address = []uint16{uint16(i)} fc.Address = []uint16{uint16(i)}
fcs = append(fcs, fc) fcs = append(fcs, fc)
t.Run(fc.Name, func(t *testing.T) { _, err := client.WriteSingleCoil(fc.Address[0], writeValue)
_, err = client.WriteSingleCoil(fc.Address[0], writeValue) require.NoError(t, err)
assert.NoError(t, err)
})
expectedFields[fc.Name] = readValue
writeValue = 65280 - writeValue 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{ modbus := Modbus{
Name: "TestReadCoils", Name: "TestReadCoils",
Controller: "tcp://localhost:1502", Controller: "tcp://localhost:1502",
SlaveID: 1, Log: testutil.Logger{},
Coils: fcs, }
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 var acc testutil.Accumulator
err = modbus.Gather(&acc) require.NoError(t, modbus.Init())
assert.NoError(t, err) require.NotEmpty(t, modbus.requests)
require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
writeValue = 0 testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
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) func TestReadMultipleHoldingRegisterWithHole(t *testing.T) {
writeValue = 1 - writeValue 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) { func TestReadMultipleHoldingRegisterLimit(t *testing.T) {
serv := mbserver.NewServer() serv := mbserver.NewServer()
err := serv.ListenTCP("localhost:1502") require.NoError(t, serv.ListenTCP("localhost:1502"))
assert.NoError(t, err)
defer serv.Close() defer serv.Close()
handler := m.NewTCPClientHandler("localhost:1502") handler := mb.NewTCPClientHandler("localhost:1502")
err = handler.Connect() require.NoError(t, handler.Connect())
assert.NoError(t, err)
defer handler.Close() 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++ { for i := 0; i <= 400; i++ {
fc := fieldContainer{} fc := fieldDefinition{}
fc.Name = fmt.Sprintf("HoldingRegister-%v", i) fc.Name = fmt.Sprintf("HoldingRegister-%v", i)
fc.ByteOrder = "AB" fc.ByteOrder = "AB"
fc.DataType = "INT16" fc.DataType = "INT16"
@ -730,28 +917,40 @@ func TestReadMultipleHoldingRegisterLimit(t *testing.T) {
fc.Address = []uint16{uint16(i)} fc.Address = []uint16{uint16(i)}
fcs = append(fcs, fc) fcs = append(fcs, fc)
t.Run(fc.Name, func(t *testing.T) { _, err := client.WriteSingleRegister(fc.Address[0], uint16(i))
_, err = client.WriteSingleRegister(fc.Address[0], uint16(i)) require.NoError(t, err)
assert.NoError(t, err)
}) expectedFields[fc.Name] = int64(i)
} }
modbus := Modbus{ modbus := Modbus{
Name: "TestHoldingRegister", Name: "TestHoldingRegister",
Controller: "tcp://localhost:1502", Controller: "tcp://localhost:1502",
SlaveID: 1, Log: testutil.Logger{},
HoldingRegisters: fcs, }
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 var acc testutil.Accumulator
err = modbus.Gather(&acc) require.NoError(t, modbus.Init())
assert.NoError(t, err) require.NotEmpty(t, modbus.requests)
require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
for i := 0; i <= 400; i++ { testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
assert.Equal(t, int16(i), modbus.registers[0].Fields[i].value)
}
} }
func TestRetrySuccessful(t *testing.T) { func TestRetrySuccessful(t *testing.T) {
@ -760,8 +959,7 @@ func TestRetrySuccessful(t *testing.T) {
value := 1 value := 1
serv := mbserver.NewServer() serv := mbserver.NewServer()
err := serv.ListenTCP("localhost:1502") require.NoError(t, serv.ListenTCP("localhost:1502"))
assert.NoError(t, err)
defer serv.Close() defer serv.Close()
// Make read on coil-registers fail for some trials by making the device // 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 return data, except
}) })
t.Run("retry_success", func(t *testing.T) {
modbus := Modbus{ modbus := Modbus{
Name: "TestRetry", Name: "TestRetry",
Controller: "tcp://localhost:1502", Controller: "tcp://localhost:1502",
SlaveID: 1,
Retries: maxretries, Retries: maxretries,
Coils: []fieldContainer{ Log: testutil.Logger{},
}
modbus.SlaveID = 1
modbus.Coils = []fieldDefinition{
{ {
Name: "retry_success", Name: "retry_success",
Address: []uint16{0}, 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 var acc testutil.Accumulator
err = modbus.Gather(&acc) require.NoError(t, modbus.Init())
assert.NoError(t, err) require.NotEmpty(t, modbus.requests)
assert.NotEmpty(t, modbus.registers) require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
for _, coil := range modbus.registers { testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
assert.Equal(t, uint16(value), coil.Fields[0].value)
}
})
} }
func TestRetryFail(t *testing.T) { func TestRetryFailExhausted(t *testing.T) {
maxretries := 2 maxretries := 2
serv := mbserver.NewServer() serv := mbserver.NewServer()
err := serv.ListenTCP("localhost:1502") require.NoError(t, serv.ListenTCP("localhost:1502"))
assert.NoError(t, err)
defer serv.Close() defer serv.Close()
// Make the read on coils fail with busy // Make the read on coils fail with busy
@ -827,27 +1032,35 @@ func TestRetryFail(t *testing.T) {
return data, &mbserver.SlaveDeviceBusy return data, &mbserver.SlaveDeviceBusy
}) })
t.Run("retry_fail", func(t *testing.T) {
modbus := Modbus{ modbus := Modbus{
Name: "TestRetryFail", Name: "TestRetryFailExhausted",
Controller: "tcp://localhost:1502", Controller: "tcp://localhost:1502",
SlaveID: 1,
Retries: maxretries, Retries: maxretries,
Coils: []fieldContainer{ Log: testutil.Logger{},
}
modbus.SlaveID = 1
modbus.Coils = []fieldDefinition{
{ {
Name: "retry_fail", Name: "retry_fail",
Address: []uint16{0}, Address: []uint16{0},
}, },
},
Log: testutil.Logger{},
} }
err = modbus.Init()
assert.NoError(t, err)
var acc testutil.Accumulator var acc testutil.Accumulator
err = modbus.Gather(&acc) require.NoError(t, modbus.Init())
assert.Error(t, err) 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 // Make the read on coils fail with illegal function preventing retry
counter := 0 counter := 0
@ -861,26 +1074,26 @@ func TestRetryFail(t *testing.T) {
return data, &mbserver.IllegalFunction return data, &mbserver.IllegalFunction
}) })
t.Run("retry_fail", func(t *testing.T) {
modbus := Modbus{ modbus := Modbus{
Name: "TestRetryFail", Name: "TestRetryFailExhausted",
Controller: "tcp://localhost:1502", Controller: "tcp://localhost:1502",
SlaveID: 1,
Retries: maxretries, Retries: maxretries,
Coils: []fieldContainer{ Log: testutil.Logger{},
}
modbus.SlaveID = 1
modbus.Coils = []fieldDefinition{
{ {
Name: "retry_fail", Name: "retry_fail",
Address: []uint16{0}, Address: []uint16{0},
}, },
},
Log: testutil.Logger{},
} }
err = modbus.Init()
assert.NoError(t, err)
var acc testutil.Accumulator var acc testutil.Accumulator
err = modbus.Gather(&acc) require.NoError(t, modbus.Init())
assert.Error(t, err) require.NotEmpty(t, modbus.requests)
assert.Equal(t, counter, 1)
}) 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)
} }

View File

@ -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
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}