3339 lines
81 KiB
Go
3339 lines
81 KiB
Go
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)
|
|
}
|