1
0
Fork 0
telegraf/plugins/inputs/modbus/configuration_request_test.go

3340 lines
81 KiB
Go
Raw Permalink Normal View History

package modbus
import (
"strconv"
"strings"
"testing"
"time"
mb "github.com/grid-x/modbus"
"github.com/stretchr/testify/require"
"github.com/tbrandon/mbserver"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/testutil"
)
func TestRequest(t *testing.T) {
modbus := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: "coil-0",
Address: uint16(0),
},
{
Name: "coil-1",
Address: uint16(1),
Omit: true,
},
{
Name: "coil-2",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "UINT16",
Measurement: "modbus",
},
{
Name: "coil-3",
Address: uint16(3),
InputType: "INT64",
Scale: 1.2,
OutputType: "BOOL",
Measurement: "modbus",
},
},
},
{
SlaveID: 1,
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: "coil-4",
Address: uint16(6),
},
{
Name: "coil-5",
Address: uint16(7),
Omit: true,
},
{
Name: "coil-6",
Address: uint16(8),
InputType: "INT64",
Scale: 1.2,
OutputType: "UINT16",
Measurement: "modbus",
},
{
Name: "coil-7",
Address: uint16(9),
InputType: "INT64",
Scale: 1.2,
OutputType: "BOOL",
Measurement: "modbus",
},
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "discrete",
Fields: []requestFieldDefinition{
{
Name: "discrete-0",
Address: uint16(0),
},
{
Name: "discrete-1",
Address: uint16(1),
Omit: true,
},
{
Name: "discrete-2",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "UINT16",
Measurement: "modbus",
},
{
Name: "discrete-3",
Address: uint16(3),
InputType: "INT64",
Scale: 1.2,
OutputType: "BOOL",
Measurement: "modbus",
},
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
},
{
Name: "holding-1",
Address: uint16(1),
InputType: "UINT16",
Omit: true,
},
{
Name: "holding-2",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "FLOAT64",
Measurement: "modbus",
},
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "input",
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
InputType: "INT16",
},
{
Name: "input-1",
Address: uint16(1),
InputType: "UINT16",
Omit: true,
},
{
Name: "input-2",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "FLOAT64",
Measurement: "modbus",
},
},
},
}
require.NoError(t, modbus.Init())
require.NotEmpty(t, modbus.requests)
require.NotNil(t, modbus.requests[1])
require.Len(t, modbus.requests[1].coil, 2)
require.Len(t, modbus.requests[1].discrete, 1)
require.Len(t, modbus.requests[1].holding, 1)
require.Len(t, modbus.requests[1].input, 1)
}
func TestRequestWithTags(t *testing.T) {
modbus := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: "coil-0",
Address: uint16(0),
},
{
Name: "coil-1",
Address: uint16(1),
Omit: true,
},
{
Name: "coil-2",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "UINT16",
Measurement: "modbus",
},
},
Tags: map[string]string{
"first": "a",
"second": "bb",
"third": "ccc",
},
},
{
SlaveID: 1,
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: "coil-3",
Address: uint16(6),
},
{
Name: "coil-4",
Address: uint16(7),
Omit: true,
},
{
Name: "coil-5",
Address: uint16(8),
InputType: "INT64",
Scale: 1.2,
OutputType: "UINT16",
Measurement: "modbus",
},
},
Tags: map[string]string{
"first": "a",
"second": "bb",
"third": "ccc",
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "discrete",
Fields: []requestFieldDefinition{
{
Name: "discrete-0",
Address: uint16(0),
},
{
Name: "discrete-1",
Address: uint16(1),
Omit: true,
},
{
Name: "discrete-2",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "UINT16",
Measurement: "modbus",
},
},
Tags: map[string]string{
"first": "a",
"second": "bb",
"third": "ccc",
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
},
{
Name: "holding-1",
Address: uint16(1),
InputType: "UINT16",
Omit: true,
},
{
Name: "holding-2",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "FLOAT64",
Measurement: "modbus",
},
},
Tags: map[string]string{
"first": "a",
"second": "bb",
"third": "ccc",
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "input",
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
InputType: "INT16",
},
{
Name: "input-1",
Address: uint16(1),
InputType: "UINT16",
Omit: true,
},
{
Name: "input-2",
Address: uint16(2),
InputType: "INT64",
Scale: 1.2,
OutputType: "FLOAT64",
Measurement: "modbus",
},
},
Tags: map[string]string{
"first": "a",
"second": "bb",
"third": "ccc",
},
},
}
require.NoError(t, modbus.Init())
require.NotEmpty(t, modbus.requests)
require.NotNil(t, modbus.requests[1])
require.Len(t, modbus.requests[1].coil, 2)
require.Len(t, modbus.requests[1].discrete, 1)
require.Len(t, modbus.requests[1].holding, 1)
require.Len(t, modbus.requests[1].input, 1)
expectedTags := map[string]string{
"first": "a",
"second": "bb",
"third": "ccc",
}
require.Equal(t, expectedTags, modbus.requests[1].coil[0].fields[0].tags)
require.Equal(t, expectedTags, modbus.requests[1].coil[1].fields[0].tags)
require.Equal(t, expectedTags, modbus.requests[1].discrete[0].fields[0].tags)
require.Equal(t, expectedTags, modbus.requests[1].holding[0].fields[0].tags)
require.Equal(t, expectedTags, modbus.requests[1].input[0].fields[0].tags)
}
func TestRequestTypesCoil(t *testing.T) {
tests := []struct {
name string
address uint16
dataTypeOut string
write uint16
read interface{}
}{
{
name: "coil-1-off",
address: 1,
write: 0,
read: uint16(0),
},
{
name: "coil-2-on",
address: 2,
write: 0xFF00,
read: uint16(1),
},
{
name: "coil-3-false",
address: 3,
dataTypeOut: "BOOL",
write: 0,
read: false,
},
{
name: "coil-4-true",
address: 4,
dataTypeOut: "BOOL",
write: 0xFF00,
read: true,
},
}
serv := mbserver.NewServer()
require.NoError(t, serv.ListenTCP("localhost:1502"))
defer serv.Close()
handler := mb.NewTCPClientHandler("localhost:1502")
require.NoError(t, handler.Connect())
defer handler.Close()
client := mb.NewClient(handler)
for _, hrt := range tests {
t.Run(hrt.name, func(t *testing.T) {
_, err := client.WriteSingleCoil(hrt.address, hrt.write)
require.NoError(t, err)
modbus := Modbus{
Name: "TestRequestTypesCoil",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: hrt.name,
OutputType: hrt.dataTypeOut,
Address: hrt.address,
},
},
},
}
expected := []telegraf.Metric{
testutil.MustMetric(
"modbus",
map[string]string{
"type": cCoils,
"slave_id": "1",
"name": modbus.Name,
},
map[string]interface{}{hrt.name: hrt.read},
time.Unix(0, 0),
),
}
var acc testutil.Accumulator
require.NoError(t, modbus.Init())
require.NotEmpty(t, modbus.requests)
require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
})
}
}
func TestRequestTypesHoldingABCD(t *testing.T) {
byteOrder := "ABCD"
tests := []struct {
name string
address uint16
bit uint8
length uint16
byteOrder string
dataTypeIn string
dataTypeOut string
scale float64
write []byte
read interface{}
}{
{
name: "register5_bit3",
address: 5,
dataTypeIn: "BIT",
bit: 3,
write: []byte{0x18, 0x0d},
read: uint8(1),
},
{
name: "register5_bit14",
address: 5,
dataTypeIn: "BIT",
bit: 14,
write: []byte{0x18, 0x0d},
read: uint8(0),
},
{
name: "register10_uint8L",
address: 10,
dataTypeIn: "UINT8L",
write: []byte{0x18, 0x0d},
read: uint8(13),
},
{
name: "register10_uint8L-scale_.1",
address: 10,
dataTypeIn: "UINT8L",
scale: .1,
write: []byte{0x18, 0x0d},
read: float64(1.3),
},
{
name: "register10_uint8L_scale_10",
address: 10,
dataTypeIn: "UINT8L",
scale: 10,
write: []byte{0x18, 0x0d},
read: float64(130),
},
{
name: "register10_uint8L_uint64",
address: 10,
dataTypeIn: "UINT8L",
dataTypeOut: "UINT64",
write: []byte{0x18, 0x0d},
read: uint64(13),
},
{
name: "register10_uint8L_int64",
address: 10,
dataTypeIn: "UINT8L",
dataTypeOut: "INT64",
write: []byte{0x18, 0x0d},
read: int64(13),
},
{
name: "register10_uint8L_float64",
address: 10,
dataTypeIn: "UINT8L",
dataTypeOut: "FLOAT64",
write: []byte{0x18, 0x0d},
read: float64(13),
},
{
name: "register10_uint8L_float64_scale",
address: 10,
dataTypeIn: "UINT8L",
scale: 1.0,
write: []byte{0x18, 0x0d},
read: float64(13),
},
{
name: "register15_int8L",
address: 15,
dataTypeIn: "UINT8L",
write: []byte{0x18, 0x0d},
read: uint8(13),
},
{
name: "register15_int8L-scale_.1",
address: 15,
dataTypeIn: "INT8L",
scale: .1,
write: []byte{0x18, 0x0d},
read: float64(1.3),
},
{
name: "register15_int8L_scale_10",
address: 15,
dataTypeIn: "INT8L",
scale: 10,
write: []byte{0x18, 0x0d},
read: float64(130),
},
{
name: "register15_int8L_uint64",
address: 15,
dataTypeIn: "INT8L",
dataTypeOut: "UINT64",
write: []byte{0x18, 0x0d},
read: uint64(13),
},
{
name: "register15_int8L_int64",
address: 15,
dataTypeIn: "INT8L",
dataTypeOut: "INT64",
write: []byte{0x18, 0x0d},
read: int64(13),
},
{
name: "register15_int8L_float64",
address: 15,
dataTypeIn: "INT8L",
dataTypeOut: "FLOAT64",
write: []byte{0x18, 0x0d},
read: float64(13),
},
{
name: "register15_int8L_float64_scale",
address: 15,
dataTypeIn: "INT8L",
scale: 1.0,
write: []byte{0x18, 0x0d},
read: float64(13),
},
{
name: "register20_uint16",
address: 20,
dataTypeIn: "UINT16",
write: []byte{0x08, 0x98},
read: uint16(2200),
},
{
name: "register20_uint16-scale_.1",
address: 20,
dataTypeIn: "UINT16",
scale: .1,
write: []byte{0x08, 0x98},
read: float64(220),
},
{
name: "register20_uint16_scale_10",
address: 20,
dataTypeIn: "UINT16",
scale: 10,
write: []byte{0x08, 0x98},
read: float64(22000),
},
{
name: "register20_uint16_uint64",
address: 20,
dataTypeIn: "UINT16",
dataTypeOut: "UINT64",
write: []byte{0x08, 0x98},
read: uint64(2200),
},
{
name: "register20_uint16_int64",
address: 20,
dataTypeIn: "UINT16",
dataTypeOut: "INT64",
write: []byte{0x08, 0x98},
read: int64(2200),
},
{
name: "register20_uint16_float64",
address: 20,
dataTypeIn: "UINT16",
dataTypeOut: "FLOAT64",
write: []byte{0x08, 0x98},
read: float64(2200),
},
{
name: "register20_uint16_float64_scale",
address: 20,
dataTypeIn: "UINT16",
scale: 1.0,
write: []byte{0x08, 0x98},
read: float64(2200),
},
{
name: "register30_int16",
address: 30,
dataTypeIn: "INT16",
write: []byte{0xf8, 0x98},
read: int16(-1896),
},
{
name: "register30_int16-scale_.1",
address: 30,
dataTypeIn: "INT16",
scale: .1,
write: []byte{0xf8, 0x98},
read: float64(-189.60000000000002),
},
{
name: "register30_int16_scale_10",
address: 30,
dataTypeIn: "INT16",
scale: 10,
write: []byte{0xf8, 0x98},
read: float64(-18960),
},
{
name: "register30_int16_uint64",
address: 30,
dataTypeIn: "INT16",
dataTypeOut: "UINT64",
write: []byte{0xf8, 0x98},
read: uint64(18446744073709549720),
},
{
name: "register30_int16_int64",
address: 30,
dataTypeIn: "INT16",
dataTypeOut: "INT64",
write: []byte{0xf8, 0x98},
read: int64(-1896),
},
{
name: "register30_int16_float64",
address: 30,
dataTypeIn: "INT16",
dataTypeOut: "FLOAT64",
write: []byte{0xf8, 0x98},
read: float64(-1896),
},
{
name: "register30_int16_float64_scale",
address: 30,
dataTypeIn: "INT16",
scale: 1.0,
write: []byte{0xf8, 0x98},
read: float64(-1896),
},
{
name: "register40_uint32",
address: 40,
dataTypeIn: "UINT32",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: uint32(168496141),
},
{
name: "register40_uint32-scale_.1",
address: 40,
dataTypeIn: "UINT32",
scale: .1,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: float64(16849614.1),
},
{
name: "register40_uint32_scale_10",
address: 40,
dataTypeIn: "UINT32",
scale: 10,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: float64(1684961410),
},
{
name: "register40_uint32_uint64",
address: 40,
dataTypeIn: "UINT32",
dataTypeOut: "UINT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: uint64(168496141),
},
{
name: "register40_uint32_int64",
address: 40,
dataTypeIn: "UINT32",
dataTypeOut: "INT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: int64(168496141),
},
{
name: "register40_uint32_float64",
address: 40,
dataTypeIn: "UINT32",
dataTypeOut: "FLOAT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: float64(168496141),
},
{
name: "register40_uint32_float64_scale",
address: 40,
dataTypeIn: "UINT32",
scale: 1.0,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: float64(168496141),
},
{
name: "register50_int32",
address: 50,
dataTypeIn: "INT32",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: int32(-99939315),
},
{
name: "register50_int32-scale_.1",
address: 50,
dataTypeIn: "INT32",
scale: .1,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: float64(-9993931.5),
},
{
name: "register50_int32_scale_10",
address: 50,
dataTypeIn: "INT32",
scale: 10,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: float64(-999393150),
},
{
name: "register50_int32_uint64",
address: 50,
dataTypeIn: "INT32",
dataTypeOut: "UINT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: uint64(18446744073609612301),
},
{
name: "register50_int32_int64",
address: 50,
dataTypeIn: "INT32",
dataTypeOut: "INT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: int64(-99939315),
},
{
name: "register50_int32_float64",
address: 50,
dataTypeIn: "INT32",
dataTypeOut: "FLOAT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: float64(-99939315),
},
{
name: "register50_int32_float64_scale",
address: 50,
dataTypeIn: "INT32",
scale: 1.0,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: float64(-99939315),
},
{
name: "register60_uint64",
address: 60,
dataTypeIn: "UINT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: uint64(723685415333069058),
},
{
name: "register60_uint64-scale_.1",
address: 60,
dataTypeIn: "UINT64",
scale: .1,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(72368541533306905.8),
},
{
name: "register60_uint64_scale_10",
address: 60,
dataTypeIn: "UINT64",
scale: 10,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(7236854153330690000), // quantization error
},
{
name: "register60_uint64_int64",
address: 60,
dataTypeIn: "UINT64",
dataTypeOut: "INT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: int64(723685415333069058),
},
{
name: "register60_uint64_float64",
address: 60,
dataTypeIn: "UINT64",
dataTypeOut: "FLOAT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(723685415333069058),
},
{
name: "register60_uint64_float64_scale",
address: 60,
dataTypeIn: "UINT64",
scale: 1.0,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(723685415333069058),
},
{
name: "register70_int64",
address: 70,
dataTypeIn: "INT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: int64(-429236089273777918),
},
{
name: "register70_int64-scale_.1",
address: 70,
dataTypeIn: "INT64",
scale: .1,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(-42923608927377791.8),
},
{
name: "register70_int64_scale_10",
address: 70,
dataTypeIn: "INT64",
scale: 10,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(-4292360892737779180),
},
{
name: "register70_int64_uint64",
address: 70,
dataTypeIn: "INT64",
dataTypeOut: "UINT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: uint64(18017507984435773698),
},
{
name: "register70_int64_float64",
address: 70,
dataTypeIn: "INT64",
dataTypeOut: "FLOAT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(-429236089273777918),
},
{
name: "register70_int64_float64_scale",
address: 70,
dataTypeIn: "INT64",
scale: 1.0,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(-429236089273777918),
},
{
name: "register80_float32",
address: 80,
dataTypeIn: "FLOAT32",
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float32(3.1415927410125732421875),
},
{
name: "register80_float32-scale_.1",
address: 80,
dataTypeIn: "FLOAT32",
scale: .1,
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float64(0.31415927410125732421875),
},
{
name: "register80_float32_scale_10",
address: 80,
dataTypeIn: "FLOAT32",
scale: 10,
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float64(31.415927410125732421875),
},
{
name: "register80_float32_float64",
address: 80,
dataTypeIn: "FLOAT32",
dataTypeOut: "FLOAT64",
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float64(3.1415927410125732421875),
},
{
name: "register80_float32_float64_scale",
address: 80,
dataTypeIn: "FLOAT32",
scale: 1.0,
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float64(3.1415927410125732421875),
},
{
name: "register90_float64",
address: 90,
dataTypeIn: "FLOAT64",
write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
read: float64(3.14159265359000006156975359772),
},
{
name: "register90_float64-scale_.1",
address: 90,
dataTypeIn: "FLOAT64",
scale: .1,
write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
read: float64(0.314159265359000006156975359772),
},
{
name: "register90_float64_scale_10",
address: 90,
dataTypeIn: "FLOAT64",
scale: 10,
write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
read: float64(31.4159265359000006156975359772),
},
{
name: "register90_float64_float64_scale",
address: 90,
dataTypeIn: "FLOAT64",
scale: 1.0,
write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
read: float64(3.14159265359000006156975359772),
},
{
name: "register100_float16",
address: 100,
dataTypeIn: "FLOAT16",
write: []byte{0xb8, 0x14},
read: float64(-0.509765625),
},
{
name: "register100_float16-scale_.1",
address: 100,
dataTypeIn: "FLOAT16",
scale: .1,
write: []byte{0xb8, 0x14},
read: float64(-0.0509765625),
},
{
name: "register100_float16_scale_10",
address: 100,
dataTypeIn: "FLOAT16",
scale: 10,
write: []byte{0xb8, 0x14},
read: float64(-5.09765625),
},
{
name: "register100_float16_float64_scale",
address: 100,
dataTypeIn: "FLOAT16",
scale: 1.0,
write: []byte{0xb8, 0x14},
read: float64(-0.509765625),
},
{
name: "register110_string",
address: 110,
dataTypeIn: "STRING",
length: 7,
write: []byte{0x4d, 0x6f, 0x64, 0x62, 0x75, 0x73, 0x20, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x00},
read: "Modbus String",
},
}
serv := mbserver.NewServer()
require.NoError(t, serv.ListenTCP("localhost:1502"))
defer serv.Close()
handler := mb.NewTCPClientHandler("localhost:1502")
require.NoError(t, handler.Connect())
defer handler.Close()
client := mb.NewClient(handler)
for _, hrt := range tests {
t.Run(hrt.name, func(t *testing.T) {
quantity := uint16(len(hrt.write) / 2)
_, err := client.WriteMultipleRegisters(hrt.address, quantity, hrt.write)
require.NoError(t, err)
modbus := Modbus{
Name: "TestRequestTypesHoldingABCD",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: byteOrder,
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: hrt.name,
InputType: hrt.dataTypeIn,
OutputType: hrt.dataTypeOut,
Scale: hrt.scale,
Address: hrt.address,
Length: hrt.length,
Bit: hrt.bit,
},
},
},
}
expected := []telegraf.Metric{
testutil.MustMetric(
"modbus",
map[string]string{
"type": cHoldingRegisters,
"slave_id": "1",
"name": modbus.Name,
},
map[string]interface{}{hrt.name: hrt.read},
time.Unix(0, 0),
),
}
var acc testutil.Accumulator
require.NoError(t, modbus.Init())
require.NotEmpty(t, modbus.requests)
require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
})
}
}
func TestRequestTypesHoldingDCBA(t *testing.T) {
byteOrder := "DCBA"
tests := []struct {
name string
address uint16
length uint16
byteOrder string
dataTypeIn string
dataTypeOut string
scale float64
write []byte
read interface{}
}{
{
name: "register10_uint8L",
address: 10,
dataTypeIn: "UINT8L",
write: []byte{0x18, 0x0d},
read: uint8(13),
},
{
name: "register10_uint8L-scale_.1",
address: 10,
dataTypeIn: "UINT8L",
scale: .1,
write: []byte{0x18, 0x0d},
read: float64(1.3),
},
{
name: "register10_uint8L_scale_10",
address: 10,
dataTypeIn: "UINT8L",
scale: 10,
write: []byte{0x18, 0x0d},
read: float64(130),
},
{
name: "register10_uint8L_uint64",
address: 10,
dataTypeIn: "UINT8L",
dataTypeOut: "UINT64",
write: []byte{0x18, 0x0d},
read: uint64(13),
},
{
name: "register10_uint8L_int64",
address: 10,
dataTypeIn: "UINT8L",
dataTypeOut: "INT64",
write: []byte{0x18, 0x0d},
read: int64(13),
},
{
name: "register10_uint8L_float64",
address: 10,
dataTypeIn: "UINT8L",
dataTypeOut: "FLOAT64",
write: []byte{0x18, 0x0d},
read: float64(13),
},
{
name: "register10_uint8L_float64_scale",
address: 10,
dataTypeIn: "UINT8L",
scale: 1.0,
write: []byte{0x18, 0x0d},
read: float64(13),
},
{
name: "register15_int8L",
address: 15,
dataTypeIn: "UINT8L",
write: []byte{0x18, 0x0d},
read: uint8(13),
},
{
name: "register15_int8L-scale_.1",
address: 15,
dataTypeIn: "INT8L",
scale: .1,
write: []byte{0x18, 0x0d},
read: float64(1.3),
},
{
name: "register15_int8L_scale_10",
address: 15,
dataTypeIn: "INT8L",
scale: 10,
write: []byte{0x18, 0x0d},
read: float64(130),
},
{
name: "register15_int8L_uint64",
address: 15,
dataTypeIn: "INT8L",
dataTypeOut: "UINT64",
write: []byte{0x18, 0x0d},
read: uint64(13),
},
{
name: "register15_int8L_int64",
address: 15,
dataTypeIn: "INT8L",
dataTypeOut: "INT64",
write: []byte{0x18, 0x0d},
read: int64(13),
},
{
name: "register15_int8L_float64",
address: 15,
dataTypeIn: "INT8L",
dataTypeOut: "FLOAT64",
write: []byte{0x18, 0x0d},
read: float64(13),
},
{
name: "register15_int8L_float64_scale",
address: 15,
dataTypeIn: "INT8L",
scale: 1.0,
write: []byte{0x18, 0x0d},
read: float64(13),
},
{
name: "register20_uint16",
address: 20,
dataTypeIn: "UINT16",
write: []byte{0x08, 0x98},
read: uint16(2200),
},
{
name: "register20_uint16-scale_.1",
address: 20,
dataTypeIn: "UINT16",
scale: .1,
write: []byte{0x08, 0x98},
read: float64(220),
},
{
name: "register20_uint16_scale_10",
address: 20,
dataTypeIn: "UINT16",
scale: 10,
write: []byte{0x08, 0x98},
read: float64(22000),
},
{
name: "register20_uint16_uint64",
address: 20,
dataTypeIn: "UINT16",
dataTypeOut: "UINT64",
write: []byte{0x08, 0x98},
read: uint64(2200),
},
{
name: "register20_uint16_int64",
address: 20,
dataTypeIn: "UINT16",
dataTypeOut: "INT64",
write: []byte{0x08, 0x98},
read: int64(2200),
},
{
name: "register20_uint16_float64",
address: 20,
dataTypeIn: "UINT16",
dataTypeOut: "FLOAT64",
write: []byte{0x08, 0x98},
read: float64(2200),
},
{
name: "register20_uint16_float64_scale",
address: 20,
dataTypeIn: "UINT16",
scale: 1.0,
write: []byte{0x08, 0x98},
read: float64(2200),
},
{
name: "register30_int16",
address: 30,
dataTypeIn: "INT16",
write: []byte{0xf8, 0x98},
read: int16(-1896),
},
{
name: "register30_int16-scale_.1",
address: 30,
dataTypeIn: "INT16",
scale: .1,
write: []byte{0xf8, 0x98},
read: float64(-189.60000000000002),
},
{
name: "register30_int16_scale_10",
address: 30,
dataTypeIn: "INT16",
scale: 10,
write: []byte{0xf8, 0x98},
read: float64(-18960),
},
{
name: "register30_int16_uint64",
address: 30,
dataTypeIn: "INT16",
dataTypeOut: "UINT64",
write: []byte{0xf8, 0x98},
read: uint64(18446744073709549720),
},
{
name: "register30_int16_int64",
address: 30,
dataTypeIn: "INT16",
dataTypeOut: "INT64",
write: []byte{0xf8, 0x98},
read: int64(-1896),
},
{
name: "register30_int16_float64",
address: 30,
dataTypeIn: "INT16",
dataTypeOut: "FLOAT64",
write: []byte{0xf8, 0x98},
read: float64(-1896),
},
{
name: "register30_int16_float64_scale",
address: 30,
dataTypeIn: "INT16",
scale: 1.0,
write: []byte{0xf8, 0x98},
read: float64(-1896),
},
{
name: "register40_uint32",
address: 40,
dataTypeIn: "UINT32",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: uint32(168496141),
},
{
name: "register40_uint32-scale_.1",
address: 40,
dataTypeIn: "UINT32",
scale: .1,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: float64(16849614.1),
},
{
name: "register40_uint32_scale_10",
address: 40,
dataTypeIn: "UINT32",
scale: 10,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: float64(1684961410),
},
{
name: "register40_uint32_uint64",
address: 40,
dataTypeIn: "UINT32",
dataTypeOut: "UINT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: uint64(168496141),
},
{
name: "register40_uint32_int64",
address: 40,
dataTypeIn: "UINT32",
dataTypeOut: "INT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: int64(168496141),
},
{
name: "register40_uint32_float64",
address: 40,
dataTypeIn: "UINT32",
dataTypeOut: "FLOAT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: float64(168496141),
},
{
name: "register40_uint32_float64_scale",
address: 40,
dataTypeIn: "UINT32",
scale: 1.0,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d},
read: float64(168496141),
},
{
name: "register50_int32",
address: 50,
dataTypeIn: "INT32",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: int32(-99939315),
},
{
name: "register50_int32-scale_.1",
address: 50,
dataTypeIn: "INT32",
scale: .1,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: float64(-9993931.5),
},
{
name: "register50_int32_scale_10",
address: 50,
dataTypeIn: "INT32",
scale: 10,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: float64(-999393150),
},
{
name: "register50_int32_uint64",
address: 50,
dataTypeIn: "INT32",
dataTypeOut: "UINT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: uint64(18446744073609612301),
},
{
name: "register50_int32_int64",
address: 50,
dataTypeIn: "INT32",
dataTypeOut: "INT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: int64(-99939315),
},
{
name: "register50_int32_float64",
address: 50,
dataTypeIn: "INT32",
dataTypeOut: "FLOAT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: float64(-99939315),
},
{
name: "register50_int32_float64_scale",
address: 50,
dataTypeIn: "INT32",
scale: 1.0,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d},
read: float64(-99939315),
},
{
name: "register60_uint64",
address: 60,
dataTypeIn: "UINT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: uint64(723685415333069058),
},
{
name: "register60_uint64-scale_.1",
address: 60,
dataTypeIn: "UINT64",
scale: .1,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(72368541533306905.8),
},
{
name: "register60_uint64_scale_10",
address: 60,
dataTypeIn: "UINT64",
scale: 10,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(7236854153330690000), // quantization error
},
{
name: "register60_uint64_int64",
address: 60,
dataTypeIn: "UINT64",
dataTypeOut: "INT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: int64(723685415333069058),
},
{
name: "register60_uint64_float64",
address: 60,
dataTypeIn: "UINT64",
dataTypeOut: "FLOAT64",
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(723685415333069058),
},
{
name: "register60_uint64_float64_scale",
address: 60,
dataTypeIn: "UINT64",
scale: 1.0,
write: []byte{0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(723685415333069058),
},
{
name: "register70_int64",
address: 70,
dataTypeIn: "INT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: int64(-429236089273777918),
},
{
name: "register70_int64-scale_.1",
address: 70,
dataTypeIn: "INT64",
scale: .1,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(-42923608927377791.8),
},
{
name: "register70_int64_scale_10",
address: 70,
dataTypeIn: "INT64",
scale: 10,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(-4292360892737779180),
},
{
name: "register70_int64_uint64",
address: 70,
dataTypeIn: "INT64",
dataTypeOut: "UINT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: uint64(18017507984435773698),
},
{
name: "register70_int64_float64",
address: 70,
dataTypeIn: "INT64",
dataTypeOut: "FLOAT64",
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(-429236089273777918),
},
{
name: "register70_int64_float64_scale",
address: 70,
dataTypeIn: "INT64",
scale: 1.0,
write: []byte{0xfa, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x01, 0x02},
read: float64(-429236089273777918),
},
{
name: "register80_float32",
address: 80,
dataTypeIn: "FLOAT32",
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float32(3.1415927410125732421875),
},
{
name: "register80_float32-scale_.1",
address: 80,
dataTypeIn: "FLOAT32",
scale: .1,
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float64(0.31415927410125732421875),
},
{
name: "register80_float32_scale_10",
address: 80,
dataTypeIn: "FLOAT32",
scale: 10,
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float64(31.415927410125732421875),
},
{
name: "register80_float32_float64",
address: 80,
dataTypeIn: "FLOAT32",
dataTypeOut: "FLOAT64",
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float64(3.1415927410125732421875),
},
{
name: "register80_float32_float64_scale",
address: 80,
dataTypeIn: "FLOAT32",
scale: 1.0,
write: []byte{0x40, 0x49, 0x0f, 0xdb},
read: float64(3.1415927410125732421875),
},
{
name: "register90_float64",
address: 90,
dataTypeIn: "FLOAT64",
write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
read: float64(3.14159265359000006156975359772),
},
{
name: "register90_float64-scale_.1",
address: 90,
dataTypeIn: "FLOAT64",
scale: .1,
write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
read: float64(0.314159265359000006156975359772),
},
{
name: "register90_float64_scale_10",
address: 90,
dataTypeIn: "FLOAT64",
scale: 10,
write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
read: float64(31.4159265359000006156975359772),
},
{
name: "register90_float64_float64_scale",
address: 90,
dataTypeIn: "FLOAT64",
scale: 1.0,
write: []byte{0x40, 0x09, 0x21, 0xfb, 0x54, 0x44, 0x2e, 0xea},
read: float64(3.14159265359000006156975359772),
},
{
name: "register100_float16",
address: 100,
dataTypeIn: "FLOAT16",
write: []byte{0xb8, 0x14},
read: float64(-0.509765625),
},
{
name: "register100_float16-scale_.1",
address: 100,
dataTypeIn: "FLOAT16",
scale: .1,
write: []byte{0xb8, 0x14},
read: float64(-0.0509765625),
},
{
name: "register100_float16_scale_10",
address: 100,
dataTypeIn: "FLOAT16",
scale: 10,
write: []byte{0xb8, 0x14},
read: float64(-5.09765625),
},
{
name: "register100_float16_float64_scale",
address: 100,
dataTypeIn: "FLOAT16",
scale: 1.0,
write: []byte{0xb8, 0x14},
read: float64(-0.509765625),
},
{
name: "register110_string",
address: 110,
dataTypeIn: "STRING",
length: 7,
write: []byte{0x6f, 0x4d, 0x62, 0x64, 0x73, 0x75, 0x53, 0x20, 0x72, 0x74, 0x6e, 0x69, 0x00, 0x67},
read: "Modbus String",
},
}
serv := mbserver.NewServer()
require.NoError(t, serv.ListenTCP("localhost:1502"))
defer serv.Close()
handler := mb.NewTCPClientHandler("localhost:1502")
require.NoError(t, handler.Connect())
defer handler.Close()
client := mb.NewClient(handler)
for _, hrt := range tests {
t.Run(hrt.name, func(t *testing.T) {
quantity := uint16(len(hrt.write) / 2)
invert := make([]byte, 0, len(hrt.write))
if hrt.dataTypeIn != "STRING" {
for i := len(hrt.write) - 1; i >= 0; i-- {
invert = append(invert, hrt.write[i])
}
} else {
// Put in raw data for strings
invert = append(invert, hrt.write...)
}
_, err := client.WriteMultipleRegisters(hrt.address, quantity, invert)
require.NoError(t, err)
modbus := Modbus{
Name: "TestRequestTypesHoldingDCBA",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: byteOrder,
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: hrt.name,
InputType: hrt.dataTypeIn,
OutputType: hrt.dataTypeOut,
Scale: hrt.scale,
Address: hrt.address,
Length: hrt.length,
},
},
},
}
expected := []telegraf.Metric{
testutil.MustMetric(
"modbus",
map[string]string{
"type": cHoldingRegisters,
"slave_id": "1",
"name": modbus.Name,
},
map[string]interface{}{hrt.name: hrt.read},
time.Unix(0, 0),
),
}
var acc testutil.Accumulator
require.NoError(t, modbus.Init())
require.NotEmpty(t, modbus.requests)
require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
})
}
}
func TestRequestFail(t *testing.T) {
tests := []struct {
name string
requests []requestDefinition
errormsg string
}{
{
name: "empty field name (coil)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Address: uint16(15),
},
},
},
},
errormsg: "empty field name in request for slave 1",
},
{
name: "invalid byte-order (coil)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "AB",
RegisterType: "coil",
},
},
errormsg: "unknown byte-order \"AB\"",
},
{
name: "duplicate fields (coil)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: "coil-0",
Address: uint16(0),
},
{
Name: "coil-0",
Address: uint16(1),
},
},
},
},
errormsg: "field \"coil-0\" duplicated in measurement \"modbus\" (slave 1/\"coil\")",
},
{
name: "duplicate fields multiple requests (coil)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: "coil-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: "coil-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
},
errormsg: "field \"coil-0\" duplicated in measurement \"foo\" (slave 1/\"coil\")",
},
{
name: "invalid byte-order (discrete)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "AB",
RegisterType: "discrete",
},
},
errormsg: "unknown byte-order \"AB\"",
},
{
name: "duplicate fields (discrete)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "discrete",
Fields: []requestFieldDefinition{
{
Name: "discrete-0",
Address: uint16(0),
},
{
Name: "discrete-0",
Address: uint16(1),
},
},
},
},
errormsg: "field \"discrete-0\" duplicated in measurement \"modbus\" (slave 1/\"discrete\")",
},
{
name: "duplicate fields multiple requests (discrete)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "discrete",
Fields: []requestFieldDefinition{
{
Name: "discrete-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "discrete",
Fields: []requestFieldDefinition{
{
Name: "discrete-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
},
errormsg: "field \"discrete-0\" duplicated in measurement \"foo\" (slave 1/\"discrete\")",
},
{
name: "invalid byte-order (holding)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "AB",
RegisterType: "holding",
},
},
errormsg: "unknown byte-order \"AB\"",
},
{
name: "invalid field name (holding)",
requests: []requestDefinition{
{
SlaveID: 1,
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Address: uint16(0),
},
},
},
},
errormsg: "empty field name in request for slave 1",
},
{
name: "invalid field input type (holding)",
requests: []requestDefinition{
{
SlaveID: 1,
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
},
},
},
},
errormsg: "initializing field \"holding-0\" failed: invalid input datatype \"\" for determining field length",
},
{
name: "invalid field output type (holding)",
requests: []requestDefinition{
{
SlaveID: 1,
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "UINT16",
OutputType: "UINT8",
},
},
},
},
errormsg: `unknown output data-type "UINT8" for field "holding-0"`,
},
{
name: "duplicate fields (holding)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
},
{
Name: "holding-0",
Address: uint16(1),
},
},
},
},
errormsg: "field \"holding-0\" duplicated in measurement \"modbus\" (slave 1/\"holding\")",
},
{
name: "duplicate fields multiple requests (holding)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
},
errormsg: "field \"holding-0\" duplicated in measurement \"foo\" (slave 1/\"holding\")",
},
{
name: "invalid byte-order (input)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "AB",
RegisterType: "input",
},
},
errormsg: "unknown byte-order \"AB\"",
},
{
name: "invalid field name (input)",
requests: []requestDefinition{
{
SlaveID: 1,
RegisterType: "input",
Fields: []requestFieldDefinition{
{
Address: uint16(0),
},
},
},
},
errormsg: "empty field name in request for slave 1",
},
{
name: "invalid field input type (input)",
requests: []requestDefinition{
{
SlaveID: 1,
RegisterType: "input",
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
},
},
},
},
errormsg: "initializing field \"input-0\" failed: invalid input datatype \"\" for determining field length",
},
{
name: "invalid field output type (input)",
requests: []requestDefinition{
{
SlaveID: 1,
RegisterType: "input",
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
InputType: "UINT16",
OutputType: "UINT8",
},
},
},
},
errormsg: `unknown output data-type "UINT8" for field "input-0"`,
},
{
name: "duplicate fields (input)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "input",
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
},
{
Name: "input-0",
Address: uint16(1),
},
},
},
},
errormsg: "field \"input-0\" duplicated in measurement \"modbus\" (slave 1/\"input\")",
},
{
name: "duplicate fields multiple requests (input)",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "input",
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "input",
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
},
errormsg: "field \"input-0\" duplicated in measurement \"foo\" (slave 1/\"input\")",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
plugin.Requests = tt.requests
require.ErrorContains(t, plugin.Init(), tt.errormsg)
require.Empty(t, plugin.requests)
})
}
}
func TestRequestStartingWithOmits(t *testing.T) {
modbus := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
Omit: true,
},
{
Name: "holding-1",
Address: uint16(1),
InputType: "UINT16",
Omit: true,
},
{
Name: "holding-2",
Address: uint16(2),
InputType: "INT16",
},
},
},
}
require.NoError(t, modbus.Init())
require.NotEmpty(t, modbus.requests)
require.NotNil(t, modbus.requests[1])
require.Equal(t, uint16(0), modbus.requests[1].holding[0].address)
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)
_, err := client.WriteMultipleRegisters(uint16(0), 3, []byte{0x00, 0x01, 0x00, 0x02, 0x00, 0x03})
require.NoError(t, err)
expected := []telegraf.Metric{
testutil.MustMetric(
"modbus",
map[string]string{
"type": cHoldingRegisters,
"slave_id": strconv.Itoa(int(modbus.Requests[0].SlaveID)),
"name": modbus.Name,
},
map[string]interface{}{"holding-2": int16(3)},
time.Unix(0, 0),
),
}
var acc testutil.Accumulator
require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime())
}
func TestRequestWithOmittedFieldsOnly(t *testing.T) {
modbus := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
Omit: true,
},
{
Name: "holding-1",
Address: uint16(1),
InputType: "UINT16",
Omit: true,
},
{
Name: "holding-2",
Address: uint16(2),
InputType: "INT16",
Omit: true,
},
},
},
}
require.NoError(t, modbus.Init())
require.Empty(t, modbus.requests)
}
func TestRequestGroupWithOmittedFieldsOnly(t *testing.T) {
modbus := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
Omit: true,
},
{
Name: "holding-1",
Address: uint16(1),
InputType: "UINT16",
Omit: true,
},
{
Name: "holding-2",
Address: uint16(2),
InputType: "INT16",
Omit: true,
},
{
Name: "holding-8",
Address: uint16(8),
InputType: "INT16",
},
},
},
}
require.NoError(t, modbus.Init())
require.Len(t, modbus.requests, 1)
require.NotNil(t, modbus.requests[1])
require.Len(t, modbus.requests[1].holding, 1)
require.Equal(t, uint16(8), modbus.requests[1].holding[0].address)
require.Equal(t, uint16(1), modbus.requests[1].holding[0].length)
}
func TestRequestEmptyFields(t *testing.T) {
modbus := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
},
}
err := modbus.Init()
require.ErrorContains(t, err, `found request section without fields`)
}
func TestRequestMultipleSlavesOneFail(t *testing.T) {
modbus := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
Retries: 1,
ConfigurationType: "request",
Log: testutil.Logger{},
}
modbus.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
},
},
},
{
SlaveID: 2,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
},
},
},
{
SlaveID: 3,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-0",
Address: uint16(0),
InputType: "INT16",
},
},
},
}
require.NoError(t, modbus.Init())
serv := mbserver.NewServer()
require.NoError(t, serv.ListenTCP("localhost:1502"))
defer serv.Close()
serv.RegisterFunctionHandler(3,
func(_ *mbserver.Server, frame mbserver.Framer) ([]byte, *mbserver.Exception) {
tcpframe, ok := frame.(*mbserver.TCPFrame)
if !ok {
return nil, &mbserver.IllegalFunction
}
if tcpframe.Device == 2 {
// Simulate device 2 being unavailable
return nil, &mbserver.GatewayTargetDeviceFailedtoRespond
}
return []byte{0x02, 0x00, 0x42}, &mbserver.Success
},
)
expected := []telegraf.Metric{
testutil.MustMetric(
"modbus",
map[string]string{
"type": cHoldingRegisters,
"slave_id": "1",
"name": modbus.Name,
},
map[string]interface{}{"holding-0": int16(0x42)},
time.Unix(0, 0),
),
testutil.MustMetric(
"modbus",
map[string]string{
"type": cHoldingRegisters,
"slave_id": "3",
"name": modbus.Name,
},
map[string]interface{}{"holding-0": int16(0x42)},
time.Unix(0, 0),
),
}
var acc testutil.Accumulator
require.NoError(t, modbus.Gather(&acc))
acc.Wait(len(expected))
actual := acc.GetTelegrafMetrics()
testutil.RequireMetricsEqual(t, expected, actual, testutil.IgnoreTime(), testutil.SortMetrics())
require.Len(t, acc.Errors, 1)
require.ErrorContains(t, acc.FirstError(), `slave 2 on controller "tcp://localhost:1502": modbus: exception '11' (gateway target device failed to respond)`)
}
func TestRequestOptimizationShrink(t *testing.T) {
maxsize := maxQuantityHoldingRegisters
tests := []struct {
name string
inputs []rangeDefinition
expected []requestExpectation
}{
{
name: "no omit",
inputs: []rangeDefinition{
{0, 2 * maxQuantityHoldingRegisters, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 0, count: maxsize, length: 1}},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{{start: maxsize, count: maxsize, length: 1}},
req: request{address: maxsize, length: maxsize},
},
},
},
{
name: "borders",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, maxsize - 2, 1, 1, "INT16", true},
{maxsize - 1, 2, 1, 1, "INT16", false},
{maxsize + 1, maxsize - 2, 1, 1, "INT16", true},
{2*maxsize - 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
{start: maxsize - 1, count: 1, length: 1},
},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{
{start: maxsize, count: 1, length: 1},
{start: 2*maxsize - 1, count: 1, length: 1},
},
req: request{address: maxsize, length: maxsize},
},
},
},
{
name: "borders with gap",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, maxsize - 2, 1, 1, "INT16", true},
{maxsize - 1, 2, 1, 1, "INT16", false},
{maxsize + 1, 4, 1, 1, "INT16", true},
{2*maxsize - 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
{start: maxsize - 1, count: 1, length: 1},
},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{{start: maxsize, count: 1, length: 1}},
req: request{address: maxsize, length: 1},
},
{
fields: []rangeDefinition{{start: 2*maxsize - 1, count: 1, length: 1}},
req: request{address: 2*maxsize - 1, length: 1},
},
},
},
{
name: "large gaps",
inputs: []rangeDefinition{
{18, 3, 1, 1, "INT16", false},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
req: request{address: 18, length: 3},
},
{
fields: []rangeDefinition{{start: maxsize - 2, count: 5, length: 1}},
req: request{address: maxsize - 2, length: 5},
},
{
fields: []rangeDefinition{{start: maxsize + 42, count: 2, length: 1}},
req: request{address: maxsize + 42, length: 2},
},
},
},
{
name: "large gaps filled",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, 17, 1, 1, "INT16", true},
{18, 3, 1, 1, "INT16", false},
{21, maxsize - 23, 1, 1, "INT16", true},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 3, 39, 1, 1, "INT16", true},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
{start: 18, count: 3, length: 1},
{start: maxsize - 2, count: 2, length: 1},
},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{
{start: maxsize, count: 3, length: 1},
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize, length: 44},
},
},
},
{
name: "large gaps filled with offset",
inputs: []rangeDefinition{
{18, 3, 1, 1, "INT16", false},
{21, maxsize - 23, 1, 1, "INT16", true},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 3, 39, 1, 1, "INT16", true},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 18, count: 3, length: 1},
{start: maxsize - 2, count: 5, length: 1},
},
req: request{address: 18, length: 110},
},
{
fields: []rangeDefinition{{start: maxsize + 42, count: 2, length: 1}},
req: request{address: maxsize + 42, length: 2},
},
},
},
{
name: "worst case",
inputs: []rangeDefinition{
{0, maxsize, 2, 1, "INT16", false},
{1, maxsize, 2, 1, "INT16", true},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 0, count: maxsize/2 + 1, increment: 2, length: 1}},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{{start: maxsize + 1, count: maxsize / 2, increment: 2, length: 1}},
req: request{address: maxsize + 1, length: maxsize - 2},
},
},
},
{
name: "from PR #11106",
inputs: []rangeDefinition{
{0, 2, 1, 1, "INT16", true},
{2, 1, 1, 1, "INT16", false},
{3, 2*maxsize + 1, 1, 1, "INT16", true},
{2*maxsize + 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 2, count: 1, length: 1}},
req: request{address: 2, length: 1},
},
{
fields: []rangeDefinition{{start: 2*maxsize + 1, count: 1, length: 1}},
req: request{address: 2*maxsize + 1, length: 1},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Generate the input structure and the expectation
requestFields := generateRequestDefinitions(tt.inputs)
expected := generateExpectation(tt.expected)
// Setup the plugin
slaveID := byte(1)
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
plugin.Requests = []requestDefinition{
{
SlaveID: slaveID,
ByteOrder: "ABCD",
RegisterType: "holding",
Optimization: "shrink",
Fields: requestFields,
},
}
require.NoError(t, plugin.Init())
require.NotEmpty(t, plugin.requests)
require.Contains(t, plugin.requests, slaveID)
requireEqualRequests(t, expected, plugin.requests[slaveID].holding)
})
}
}
func TestRequestOptimizationRearrange(t *testing.T) {
maxsize := maxQuantityHoldingRegisters
tests := []struct {
name string
inputs []rangeDefinition
expected []requestExpectation
}{
{
name: "no omit",
inputs: []rangeDefinition{
{0, 2 * maxQuantityHoldingRegisters, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 0, count: maxsize, length: 1}},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{{start: maxsize, count: maxsize, length: 1}},
req: request{address: maxsize, length: maxsize},
},
},
},
{
name: "borders",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, maxsize - 2, 1, 1, "INT16", true},
{maxsize - 1, 2, 1, 1, "INT16", false},
{maxsize + 1, maxsize - 2, 1, 1, "INT16", true},
{2*maxsize - 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
{start: maxsize - 1, count: 1, length: 1},
},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{
{start: maxsize, count: 1, length: 1},
{start: 2*maxsize - 1, count: 1, length: 1},
},
req: request{address: maxsize, length: maxsize},
},
},
},
{
name: "borders with gap",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, maxsize - 2, 1, 1, "INT16", true},
{maxsize - 1, 2, 1, 1, "INT16", false},
{maxsize + 1, 4, 1, 1, "INT16", true},
{2*maxsize - 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 0, count: 1, length: 1}},
req: request{address: 0, length: 1},
},
{
fields: []rangeDefinition{
{start: maxsize - 1, count: 1, length: 1},
{start: maxsize, count: 1, length: 1},
},
req: request{address: maxsize - 1, length: 2},
},
{
fields: []rangeDefinition{{start: 2*maxsize - 1, count: 1, length: 1}},
req: request{address: 2*maxsize - 1, length: 1},
},
},
},
{
name: "large gaps",
inputs: []rangeDefinition{
{18, 3, 1, 1, "INT16", false},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
req: request{address: 18, length: 3},
},
{
fields: []rangeDefinition{{start: maxsize - 2, count: 5, length: 1}},
req: request{address: maxsize - 2, length: 5},
},
{
fields: []rangeDefinition{{start: maxsize + 42, count: 2, length: 1}},
req: request{address: maxsize + 42, length: 2},
},
},
},
{
name: "large gaps filled",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, 17, 1, 1, "INT16", true},
{18, 3, 1, 1, "INT16", false},
{21, maxsize - 23, 1, 1, "INT16", true},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 3, 39, 1, 1, "INT16", true},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
{start: 18, count: 3, length: 1},
},
req: request{address: 0, length: 21},
},
{
fields: []rangeDefinition{
{start: maxsize - 2, count: 5, length: 1},
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize - 2, length: 46},
},
},
},
{
name: "large gaps filled with offset",
inputs: []rangeDefinition{
{18, 3, 1, 1, "INT16", false},
{21, maxsize - 23, 1, 1, "INT16", true},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 3, 39, 1, 1, "INT16", true},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
req: request{address: 18, length: 3},
},
{
fields: []rangeDefinition{
{start: maxsize - 2, count: 5, length: 1},
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize - 2, length: 46},
},
},
},
{
name: "from PR #11106",
inputs: []rangeDefinition{
{0, 2, 1, 1, "INT16", true},
{2, 1, 1, 1, "INT16", false},
{3, 2*maxsize + 1, 1, 1, "INT16", true},
{2*maxsize + 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 2, count: 1, length: 1}},
req: request{address: 2, length: 1},
},
{
fields: []rangeDefinition{{start: 2*maxsize + 1, count: 1, length: 1}},
req: request{address: 2*maxsize + 1, length: 1},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Generate the input structure and the expectation
requestFields := generateRequestDefinitions(tt.inputs)
expected := generateExpectation(tt.expected)
// Setup the plugin
slaveID := byte(1)
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
plugin.Requests = []requestDefinition{
{
SlaveID: slaveID,
ByteOrder: "ABCD",
RegisterType: "holding",
Optimization: "rearrange",
Fields: requestFields,
},
}
require.NoError(t, plugin.Init())
require.NotEmpty(t, plugin.requests)
require.Contains(t, plugin.requests, slaveID)
requireEqualRequests(t, expected, plugin.requests[slaveID].holding)
})
}
}
func TestRequestOptimizationAggressive(t *testing.T) {
maxsize := maxQuantityHoldingRegisters
tests := []struct {
name string
inputs []rangeDefinition
expected []requestExpectation
}{
{
name: "no omit",
inputs: []rangeDefinition{
{0, 2 * maxQuantityHoldingRegisters, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 0, count: maxsize, length: 1}},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{{start: maxsize, count: maxsize, length: 1}},
req: request{address: maxsize, length: maxsize},
},
},
},
{
name: "borders",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, maxsize - 2, 1, 1, "INT16", true},
{maxsize - 1, 2, 1, 1, "INT16", false},
{maxsize + 1, maxsize - 2, 1, 1, "INT16", true},
{2*maxsize - 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
{start: maxsize - 1, count: 1, length: 1},
},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{
{start: maxsize, count: 1, length: 1},
{start: 2*maxsize - 1, count: 1, length: 1},
},
req: request{address: maxsize, length: maxsize},
},
},
},
{
name: "borders with gap",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, maxsize - 2, 1, 1, "INT16", true},
{maxsize - 1, 2, 1, 1, "INT16", false},
{maxsize + 1, 4, 1, 1, "INT16", true},
{2*maxsize - 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
{start: maxsize - 1, count: 1, length: 1},
},
req: request{address: 0, length: maxsize},
},
{
fields: []rangeDefinition{
{start: maxsize, count: 1, length: 1},
{start: 2*maxsize - 1, count: 1, length: 1},
},
req: request{address: maxsize, length: maxsize},
},
},
},
{
name: "large gaps",
inputs: []rangeDefinition{
{18, 3, 1, 1, "INT16", false},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
req: request{address: 18, length: 3},
},
{
fields: []rangeDefinition{
{start: maxsize - 2, count: 5, length: 1},
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize - 2, length: 46},
},
},
},
{
name: "large gaps filled",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, 17, 1, 1, "INT16", true},
{18, 3, 1, 1, "INT16", false},
{21, maxsize - 23, 1, 1, "INT16", true},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 3, 39, 1, 1, "INT16", true},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
{start: 18, count: 3, length: 1},
},
req: request{address: 0, length: 21},
},
{
fields: []rangeDefinition{
{start: maxsize - 2, count: 5, length: 1},
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize - 2, length: 46},
},
},
},
{
name: "large gaps filled with offset",
inputs: []rangeDefinition{
{18, 3, 1, 1, "INT16", false},
{21, maxsize - 23, 1, 1, "INT16", true},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 3, 39, 1, 1, "INT16", true},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
req: request{address: 18, length: 3},
},
{
fields: []rangeDefinition{
{start: maxsize - 2, count: 5, length: 1},
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize - 2, length: 46},
},
},
},
{
name: "from PR #11106",
inputs: []rangeDefinition{
{0, 2, 1, 1, "INT16", true},
{2, 1, 1, 1, "INT16", false},
{3, 2*maxsize + 1, 1, 1, "INT16", true},
{2*maxsize + 1, 1, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 2, count: 1, length: 1}},
req: request{address: 2, length: 1},
},
{
fields: []rangeDefinition{{start: 2*maxsize + 1, count: 1, length: 1}},
req: request{address: 2*maxsize + 1, length: 1},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Generate the input structure and the expectation
requestFields := generateRequestDefinitions(tt.inputs)
expected := generateExpectation(tt.expected)
// Setup the plugin
slaveID := byte(1)
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
plugin.Requests = []requestDefinition{
{
SlaveID: slaveID,
ByteOrder: "ABCD",
RegisterType: "holding",
Optimization: "aggressive",
Fields: requestFields,
},
}
require.NoError(t, plugin.Init())
require.NotEmpty(t, plugin.requests)
require.Contains(t, plugin.requests, slaveID)
requireEqualRequests(t, expected, plugin.requests[slaveID].holding)
})
}
}
func TestRequestOptimizationMaxExtraRegisterFail(t *testing.T) {
tests := []struct {
name string
requests []requestDefinition
errormsg string
}{{
name: "MaxExtraRegister too large",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "input",
Optimization: "max_insert",
MaxExtraRegisters: 5000,
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
},
errormsg: "optimization_max_register_fill has to be between 1 and 125",
},
{
name: "MaxExtraRegister too small",
requests: []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "input",
Optimization: "max_insert",
MaxExtraRegisters: 0,
Fields: []requestFieldDefinition{
{
Name: "input-0",
Address: uint16(0),
Measurement: "foo",
},
},
},
},
errormsg: "optimization_max_register_fill has to be between 1 and 125",
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
plugin.Requests = tt.requests
require.ErrorContains(t, plugin.Init(), tt.errormsg)
require.Empty(t, plugin.requests)
})
}
}
func TestRequestOptimizationMaxInsertSmall(t *testing.T) {
maxsize := maxQuantityHoldingRegisters
maxExtraRegisters := uint16(5)
tests := []struct {
name string
inputs []rangeDefinition
expected []requestExpectation
}{
{
name: "large gaps",
inputs: []rangeDefinition{
{18, 3, 1, 1, "INT16", false},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
req: request{address: 18, length: 3},
},
{
fields: []rangeDefinition{
{start: maxsize - 2, count: 5, length: 1},
},
req: request{address: maxsize - 2, length: 5},
},
{
fields: []rangeDefinition{
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize + 42, length: 2},
},
},
},
{
name: "large gaps filled",
inputs: []rangeDefinition{
{0, 1, 1, 1, "INT16", false},
{1, 17, 1, 1, "INT16", true},
{18, 3, 1, 1, "INT16", false},
{21, maxsize - 23, 1, 1, "INT16", true},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 3, 39, 1, 1, "INT16", true},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{
{start: 0, count: 1, length: 1},
},
req: request{address: 0, length: 1},
},
{
fields: []rangeDefinition{
{start: 18, count: 3, length: 1},
},
req: request{address: 18, length: 3},
},
{
fields: []rangeDefinition{
{start: maxsize - 2, count: 5, length: 1},
},
req: request{address: maxsize - 2, length: 5},
},
{
fields: []rangeDefinition{
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize + 42, length: 2},
},
},
},
{
name: "large gaps filled with offset",
inputs: []rangeDefinition{
{18, 3, 1, 1, "INT16", false},
{21, maxsize - 23, 1, 1, "INT16", true},
{maxsize - 2, 5, 1, 1, "INT16", false},
{maxsize + 3, 39, 1, 1, "INT16", true},
{maxsize + 42, 2, 1, 1, "INT16", false},
},
expected: []requestExpectation{
{
fields: []rangeDefinition{{start: 18, count: 3, length: 1}},
req: request{address: 18, length: 3},
},
{
fields: []rangeDefinition{
{start: maxsize - 2, count: 5, length: 1},
},
req: request{address: maxsize - 2, length: 5},
},
{
fields: []rangeDefinition{
{start: maxsize + 42, count: 2, length: 1},
},
req: request{address: maxsize + 42, length: 2},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Generate the input structure and the expectation
requestFields := generateRequestDefinitions(tt.inputs)
expected := generateExpectation(tt.expected)
// Setup the plugin
slaveID := byte(1)
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
}
plugin.Requests = []requestDefinition{
{
SlaveID: slaveID,
ByteOrder: "ABCD",
RegisterType: "holding",
Optimization: "max_insert",
MaxExtraRegisters: maxExtraRegisters,
Fields: requestFields,
},
}
require.NoError(t, plugin.Init())
require.NotEmpty(t, plugin.requests)
require.Contains(t, plugin.requests, slaveID)
requireEqualRequests(t, expected, plugin.requests[slaveID].holding)
})
}
}
func TestRequestWorkaroundsOneRequestPerField(t *testing.T) {
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
Workarounds: workarounds{OnRequestPerField: true},
}
plugin.Requests = []requestDefinition{
{
SlaveID: 1,
ByteOrder: "ABCD",
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "holding-1",
Address: uint16(1),
InputType: "INT16",
},
{
Name: "holding-2",
Address: uint16(2),
InputType: "INT16",
},
{
Name: "holding-3",
Address: uint16(3),
InputType: "INT16",
},
{
Name: "holding-4",
Address: uint16(4),
InputType: "INT16",
},
{
Name: "holding-5",
Address: uint16(5),
InputType: "INT16",
},
},
},
}
require.NoError(t, plugin.Init())
require.Len(t, plugin.requests[1].holding, len(plugin.Requests[0].Fields))
}
func TestRequestWorkaroundsReadCoilsStartingAtZeroRequest(t *testing.T) {
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: testutil.Logger{},
Workarounds: workarounds{ReadCoilsStartingAtZero: true},
}
plugin.SlaveID = 1
plugin.Requests = []requestDefinition{
{
SlaveID: 1,
RegisterType: "coil",
Fields: []requestFieldDefinition{
{
Name: "coil-8",
Address: uint16(8),
},
{
Name: "coil-new-group",
Address: maxQuantityCoils,
},
},
},
}
require.NoError(t, plugin.Init())
require.Len(t, plugin.requests[1].coil, 2)
// First group should now start at zero and have the cumulated length
require.Equal(t, uint16(0), plugin.requests[1].coil[0].address)
require.Equal(t, uint16(9), plugin.requests[1].coil[0].length)
// The second field should form a new group as the previous request
// is now too large (beyond max-coils-per-read) after zero enforcement.
require.Equal(t, maxQuantityCoils, plugin.requests[1].coil[1].address)
require.Equal(t, uint16(1), plugin.requests[1].coil[1].length)
}
func TestRequestOverlap(t *testing.T) {
logger := &testutil.CaptureLogger{}
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: logger,
Workarounds: workarounds{ReadCoilsStartingAtZero: true},
}
plugin.Requests = []requestDefinition{
{
SlaveID: 1,
RegisterType: "holding",
Optimization: "max_insert",
MaxExtraRegisters: 16,
Fields: []requestFieldDefinition{
{
Name: "field-1",
InputType: "UINT32",
Address: uint16(1),
},
{
Name: "field-2",
InputType: "UINT64",
Address: uint16(3),
},
{
Name: "field-3",
InputType: "UINT32",
Address: uint16(5),
},
{
Name: "field-4",
InputType: "UINT32",
Address: uint16(7),
},
},
},
}
require.NoError(t, plugin.Init())
require.Eventually(t, func() bool {
return len(logger.Warnings()) > 0
}, 3*time.Second, 100*time.Millisecond)
var found bool
for _, w := range logger.Warnings() {
if strings.Contains(w, "Request at 3 with length 4 overlaps with next request at 5") {
found = true
break
}
}
require.True(t, found, "Overlap warning not found!")
require.Len(t, plugin.requests, 1)
require.Len(t, plugin.requests[1].holding, 1)
}
func TestRequestAddressOverflow(t *testing.T) {
logger := &testutil.CaptureLogger{}
plugin := Modbus{
Name: "Test",
Controller: "tcp://localhost:1502",
ConfigurationType: "request",
Log: logger,
Workarounds: workarounds{ReadCoilsStartingAtZero: true},
}
plugin.Requests = []requestDefinition{
{
SlaveID: 1,
RegisterType: "holding",
Fields: []requestFieldDefinition{
{
Name: "field",
InputType: "UINT64",
Address: uint16(65534),
},
},
},
}
require.ErrorIs(t, plugin.Init(), errAddressOverflow)
}