diff --git a/plugins/inputs/modbus/README.md b/plugins/inputs/modbus/README.md index 6e3884e914620..dcec176dabf54 100644 --- a/plugins/inputs/modbus/README.md +++ b/plugins/inputs/modbus/README.md @@ -362,11 +362,6 @@ plugin ordering. See [CONFIGURATION.md][CONFIGURATION.md] for more details. ## stay connected during gather and disconnect afterwards. # close_connection_after_gather = false - ## Force the plugin to read each field in a separate request. - ## This might be necessary for devices not conforming to the spec, - ## see https://github.com/influxdata/telegraf/issues/12071. - # one_request_per_field = false - ## Enforce the starting address to be zero for the first request on ## coil registers. This is necessary for some devices see ## https://github.com/influxdata/telegraf/issues/8905 @@ -381,6 +376,21 @@ plugin ordering. See [CONFIGURATION.md][CONFIGURATION.md] for more details. ## upper -- use only upper byte of the register (XX00 XX00 XX00 XX00) ## By default both bytes of the register are used (XXXX XXXX). # string_register_location = "" + + ## Force the plugin to read each field in a separate request. + ## This might be necessary for devices not conforming to the spec, + ## see https://github.com/influxdata/telegraf/issues/12071. + # one_request_per_field = false + + ## Maximum number of coil or discrete-input registers to read per request + ## The specification defines this limit as 2000 but some devices only + ## support a smaller number per request + # max_bit_registers_per_request = 2000 + + ## Maximum number of input or holding registers to read per request + ## The specification defines this limit as 125 but some devices only + ## support a smaller number per request + # max_word_registers_per_request = 125 ``` ## Notes @@ -399,8 +409,8 @@ collection interval. Note that pauses add up if multiple requests are sent! The modbus plugin supports multiple configuration styles that can be set using the `configuration_type` setting. The different styles are described below. Please note that styles cannot be mixed. -Only the settings belonging to the configured `configuration_type` are used for constructing _modbus_ -requests and creation of metrics. +Only the settings belonging to the configured `configuration_type` are used for +constructing _modbus_ requests and creation of metrics. Directly jump to the styles: diff --git a/plugins/inputs/modbus/configuration_metric.go b/plugins/inputs/modbus/configuration_metric.go index 959690a1eb89f..80c1f6d707bf5 100644 --- a/plugins/inputs/modbus/configuration_metric.go +++ b/plugins/inputs/modbus/configuration_metric.go @@ -218,6 +218,8 @@ func (c *configurationPerMetric) process() (map[byte]requestSet, error) { params.maxBatchSize = maxQuantityCoils if c.workarounds.OnRequestPerField { params.maxBatchSize = 1 + } else if c.workarounds.MaxBitRegistersPerRequest > 0 { + params.maxBatchSize = c.workarounds.MaxBitRegistersPerRequest } params.enforceFromZero = c.workarounds.ReadCoilsStartingAtZero requests := groupFieldsToRequests(fields, params) @@ -226,6 +228,8 @@ func (c *configurationPerMetric) process() (map[byte]requestSet, error) { params.maxBatchSize = maxQuantityDiscreteInput if c.workarounds.OnRequestPerField { params.maxBatchSize = 1 + } else if c.workarounds.MaxBitRegistersPerRequest > 0 { + params.maxBatchSize = c.workarounds.MaxBitRegistersPerRequest } requests := groupFieldsToRequests(fields, params) set.discrete = append(set.discrete, requests...) @@ -233,6 +237,8 @@ func (c *configurationPerMetric) process() (map[byte]requestSet, error) { params.maxBatchSize = maxQuantityHoldingRegisters if c.workarounds.OnRequestPerField { params.maxBatchSize = 1 + } else if c.workarounds.MaxWordRegistersPerRequest > 0 { + params.maxBatchSize = c.workarounds.MaxWordRegistersPerRequest } requests := groupFieldsToRequests(fields, params) set.holding = append(set.holding, requests...) @@ -240,6 +246,8 @@ func (c *configurationPerMetric) process() (map[byte]requestSet, error) { params.maxBatchSize = maxQuantityInputRegisters if c.workarounds.OnRequestPerField { params.maxBatchSize = 1 + } else if c.workarounds.MaxWordRegistersPerRequest > 0 { + params.maxBatchSize = c.workarounds.MaxWordRegistersPerRequest } requests := groupFieldsToRequests(fields, params) set.input = append(set.input, requests...) diff --git a/plugins/inputs/modbus/configuration_metric_test.go b/plugins/inputs/modbus/configuration_metric_test.go index 39ff759d1eda9..db79074802338 100644 --- a/plugins/inputs/modbus/configuration_metric_test.go +++ b/plugins/inputs/modbus/configuration_metric_test.go @@ -1,6 +1,7 @@ package modbus import ( + "fmt" "testing" "time" @@ -391,3 +392,71 @@ func TestMetricAddressOverflow(t *testing.T) { } require.ErrorIs(t, plugin.Init(), errAddressOverflow) } + +func TestMetricMaxRegistersWorkaround(t *testing.T) { + fields := make([]metricFieldDefinition, 0, 40) + for i := range 10 { + fields = append(fields, + metricFieldDefinition{ + Name: fmt.Sprintf("field-coil-%d", i), + Address: uint16(i), + RegisterType: "coil", + }, + ) + } + for i := range 10 { + fields = append(fields, + metricFieldDefinition{ + Name: fmt.Sprintf("field-discrete-%d", i), + Address: uint16(i), + RegisterType: "discrete", + }, + ) + } + for i := range 10 { + fields = append(fields, + metricFieldDefinition{ + Name: fmt.Sprintf("field-holding-%d", i), + Address: uint16(i * 4), + InputType: "UINT64", + RegisterType: "holding", + }, + ) + } + for i := range 10 { + fields = append(fields, + metricFieldDefinition{ + Name: fmt.Sprintf("field-input-%d", i), + Address: uint16(i * 4), + InputType: "UINT64", + RegisterType: "input", + }, + ) + } + plugin := &Modbus{ + Name: "Test", + Controller: "tcp://localhost:1502", + ConfigurationType: "metric", + Log: &testutil.Logger{}, + Workarounds: workarounds{ + MaxBitRegistersPerRequest: 6, + MaxWordRegistersPerRequest: 8, + }, + } + plugin.Metrics = []metricDefinition{ + { + SlaveID: 1, + ByteOrder: "ABCD", + Measurement: "test", + Fields: fields, + }, + } + require.NoError(t, plugin.Init()) + + require.Len(t, plugin.requests, 1) + require.Contains(t, plugin.requests, byte(1)) + require.Len(t, plugin.requests[1].coil, 2, "coil") + require.Len(t, plugin.requests[1].discrete, 2, "discrete") + require.Len(t, plugin.requests[1].holding, 5, "holding") + require.Len(t, plugin.requests[1].input, 5, "input") +} diff --git a/plugins/inputs/modbus/configuration_register.go b/plugins/inputs/modbus/configuration_register.go index 898a2d0407895..0e34b9d0b5740 100644 --- a/plugins/inputs/modbus/configuration_register.go +++ b/plugins/inputs/modbus/configuration_register.go @@ -58,33 +58,44 @@ func (c *configurationOriginal) check() error { } func (c *configurationOriginal) process() (map[byte]requestSet, error) { - maxQuantity := uint16(1) - if !c.workarounds.OnRequestPerField { - maxQuantity = maxQuantityCoils + maxQuantity := maxQuantityCoils + if c.workarounds.OnRequestPerField { + maxQuantity = 1 + } else if c.workarounds.MaxBitRegistersPerRequest > 0 { + maxQuantity = c.workarounds.MaxBitRegistersPerRequest } coil, err := c.initRequests(c.Coils, maxQuantity, false) if err != nil { return nil, err } - if !c.workarounds.OnRequestPerField { - maxQuantity = maxQuantityDiscreteInput + maxQuantity = maxQuantityDiscreteInput + if c.workarounds.OnRequestPerField { + maxQuantity = 1 + } else if c.workarounds.MaxBitRegistersPerRequest > 0 { + maxQuantity = c.workarounds.MaxBitRegistersPerRequest } discrete, err := c.initRequests(c.DiscreteInputs, maxQuantity, false) if err != nil { return nil, err } - if !c.workarounds.OnRequestPerField { - maxQuantity = maxQuantityHoldingRegisters + maxQuantity = maxQuantityHoldingRegisters + if c.workarounds.OnRequestPerField { + maxQuantity = 1 + } else if c.workarounds.MaxWordRegistersPerRequest > 0 { + maxQuantity = c.workarounds.MaxWordRegistersPerRequest } holding, err := c.initRequests(c.HoldingRegisters, maxQuantity, true) if err != nil { return nil, err } - if !c.workarounds.OnRequestPerField { - maxQuantity = maxQuantityInputRegisters + maxQuantity = maxQuantityInputRegisters + if c.workarounds.OnRequestPerField { + maxQuantity = 1 + } else if c.workarounds.MaxWordRegistersPerRequest > 0 { + maxQuantity = c.workarounds.MaxWordRegistersPerRequest } input, err := c.initRequests(c.InputRegisters, maxQuantity, true) if err != nil { diff --git a/plugins/inputs/modbus/configuration_register_test.go b/plugins/inputs/modbus/configuration_register_test.go index 6aba625cb34ce..e2909baea01f9 100644 --- a/plugins/inputs/modbus/configuration_register_test.go +++ b/plugins/inputs/modbus/configuration_register_test.go @@ -1193,3 +1193,71 @@ func TestRegisterHighAddresses(t *testing.T) { require.NoError(t, modbus.Gather(&acc)) testutil.RequireMetricsEqual(t, expected, acc.GetTelegrafMetrics(), testutil.IgnoreTime()) } + +func TestRegisterMaxRegistersWorkaround(t *testing.T) { + plugin := &Modbus{ + Name: "Test", + Controller: "tcp://localhost:1502", + ConfigurationType: "register", + configurationOriginal: configurationOriginal{SlaveID: 1}, + Log: &testutil.Logger{}, + Workarounds: workarounds{ + MaxBitRegistersPerRequest: 6, + MaxWordRegistersPerRequest: 8, + }, + } + plugin.Coils = make([]fieldDefinition, 0, 10) + for i := range uint16(10) { + plugin.Coils = append(plugin.Coils, + fieldDefinition{ + Measurement: "test", + Name: fmt.Sprintf("field-coil-%d", i), + Address: []uint16{i}, + }, + ) + } + plugin.DiscreteInputs = make([]fieldDefinition, 0, 10) + for i := range uint16(10) { + plugin.DiscreteInputs = append(plugin.DiscreteInputs, + fieldDefinition{ + Measurement: "test", + Name: fmt.Sprintf("field-discrete-%d", i), + Address: []uint16{i}, + }, + ) + } + plugin.HoldingRegisters = make([]fieldDefinition, 0, 10) + for i := range uint16(10) { + plugin.HoldingRegisters = append(plugin.HoldingRegisters, + fieldDefinition{ + Measurement: "test", + Name: fmt.Sprintf("field-holding-%d", i), + Address: []uint16{4 * i, 4*i + 1, 4*i + 2, 4*i + 3}, + DataType: "UINT64", + ByteOrder: "ABCDEFGH", + Scale: 1.0, + }, + ) + } + plugin.InputRegisters = make([]fieldDefinition, 0, 10) + for i := range uint16(10) { + plugin.InputRegisters = append(plugin.InputRegisters, + fieldDefinition{ + Measurement: "test", + Name: fmt.Sprintf("field-input-%d", i), + Address: []uint16{4 * i, 4*i + 1, 4*i + 2, 4*i + 3}, + DataType: "UINT64", + ByteOrder: "ABCDEFGH", + Scale: 1.0, + }, + ) + } + require.NoError(t, plugin.Init()) + + require.Len(t, plugin.requests, 1) + require.Contains(t, plugin.requests, byte(1)) + require.Len(t, plugin.requests[1].coil, 2, "coil") + require.Len(t, plugin.requests[1].discrete, 2, "discrete") + require.Len(t, plugin.requests[1].holding, 5, "holding") + require.Len(t, plugin.requests[1].input, 5, "input") +} diff --git a/plugins/inputs/modbus/configuration_request.go b/plugins/inputs/modbus/configuration_request.go index 6d63dafc3eb1d..9907e559776ce 100644 --- a/plugins/inputs/modbus/configuration_request.go +++ b/plugins/inputs/modbus/configuration_request.go @@ -233,6 +233,8 @@ func (c *configurationPerRequest) process() (map[byte]requestSet, error) { params.maxBatchSize = maxQuantityCoils if c.workarounds.OnRequestPerField { params.maxBatchSize = 1 + } else if c.workarounds.MaxBitRegistersPerRequest > 0 { + params.maxBatchSize = c.workarounds.MaxBitRegistersPerRequest } params.enforceFromZero = c.workarounds.ReadCoilsStartingAtZero requests := groupFieldsToRequests(fields, params) @@ -241,6 +243,8 @@ func (c *configurationPerRequest) process() (map[byte]requestSet, error) { params.maxBatchSize = maxQuantityDiscreteInput if c.workarounds.OnRequestPerField { params.maxBatchSize = 1 + } else if c.workarounds.MaxBitRegistersPerRequest > 0 { + params.maxBatchSize = c.workarounds.MaxBitRegistersPerRequest } requests := groupFieldsToRequests(fields, params) set.discrete = append(set.discrete, requests...) @@ -248,6 +252,8 @@ func (c *configurationPerRequest) process() (map[byte]requestSet, error) { params.maxBatchSize = maxQuantityHoldingRegisters if c.workarounds.OnRequestPerField { params.maxBatchSize = 1 + } else if c.workarounds.MaxWordRegistersPerRequest > 0 { + params.maxBatchSize = c.workarounds.MaxWordRegistersPerRequest } requests := groupFieldsToRequests(fields, params) set.holding = append(set.holding, requests...) @@ -255,6 +261,8 @@ func (c *configurationPerRequest) process() (map[byte]requestSet, error) { params.maxBatchSize = maxQuantityInputRegisters if c.workarounds.OnRequestPerField { params.maxBatchSize = 1 + } else if c.workarounds.MaxWordRegistersPerRequest > 0 { + params.maxBatchSize = c.workarounds.MaxWordRegistersPerRequest } requests := groupFieldsToRequests(fields, params) set.input = append(set.input, requests...) diff --git a/plugins/inputs/modbus/configuration_request_test.go b/plugins/inputs/modbus/configuration_request_test.go index 0a6bb637d88e2..e65a4d1fc7d06 100644 --- a/plugins/inputs/modbus/configuration_request_test.go +++ b/plugins/inputs/modbus/configuration_request_test.go @@ -1,6 +1,7 @@ package modbus import ( + "fmt" "strconv" "strings" "testing" @@ -3139,3 +3140,86 @@ func TestRequestAddressOverflow(t *testing.T) { } require.ErrorIs(t, plugin.Init(), errAddressOverflow) } + +func TestRequestMaxRegistersWorkaround(t *testing.T) { + plugin := &Modbus{ + Name: "Test", + Controller: "tcp://localhost:1502", + ConfigurationType: "request", + Log: &testutil.Logger{}, + Workarounds: workarounds{ + MaxBitRegistersPerRequest: 6, + MaxWordRegistersPerRequest: 8, + }, + } + + fields := make([]requestFieldDefinition, 0, 10) + for i := range 10 { + fields = append(fields, + requestFieldDefinition{ + Name: fmt.Sprintf("field-coil-%d", i), + Address: uint16(i), + }, + ) + } + plugin.Requests = append(plugin.Requests, requestDefinition{ + SlaveID: 1, + RegisterType: "coil", + Fields: fields, + }) + + fields = make([]requestFieldDefinition, 0, 10) + for i := range 10 { + fields = append(fields, + requestFieldDefinition{ + Name: fmt.Sprintf("field-discrete-%d", i), + Address: uint16(i), + }, + ) + } + plugin.Requests = append(plugin.Requests, requestDefinition{ + SlaveID: 1, + RegisterType: "discrete", + Fields: fields, + }) + + fields = make([]requestFieldDefinition, 0, 10) + for i := range 10 { + fields = append(fields, + requestFieldDefinition{ + Name: fmt.Sprintf("field-holding-%d", i), + Address: uint16(4 * i), + InputType: "UINT64", + }, + ) + } + plugin.Requests = append(plugin.Requests, requestDefinition{ + SlaveID: 1, + RegisterType: "holding", + Fields: fields, + }) + + fields = make([]requestFieldDefinition, 0, 10) + for i := range 10 { + fields = append(fields, + requestFieldDefinition{ + Name: fmt.Sprintf("field-input-%d", i), + Address: uint16(4 * i), + InputType: "UINT64", + }, + ) + } + plugin.Requests = append(plugin.Requests, requestDefinition{ + SlaveID: 1, + RegisterType: "input", + Fields: fields, + }) + require.NoError(t, plugin.Init()) + + require.Len(t, plugin.requests, 1) + require.Contains(t, plugin.requests, byte(1)) + require.Len(t, plugin.requests[1].coil, 2, "coil") + require.Len(t, plugin.requests[1].discrete, 2, "discrete") + require.Len(t, plugin.requests[1].holding, 5, "holding") + require.Len(t, plugin.requests[1].input, 5, "input") +} diff --git a/plugins/inputs/modbus/modbus.go b/plugins/inputs/modbus/modbus.go index fe30aa87bc29d..8856cfbc9a5c1 100644 --- a/plugins/inputs/modbus/modbus.go +++ b/plugins/inputs/modbus/modbus.go @@ -67,12 +67,14 @@ type Modbus struct { } type workarounds struct { - AfterConnectPause config.Duration `toml:"pause_after_connect"` - PollPause config.Duration `toml:"pause_between_requests"` - CloseAfterGather bool `toml:"close_connection_after_gather"` - OnRequestPerField bool `toml:"one_request_per_field"` - ReadCoilsStartingAtZero bool `toml:"read_coils_starting_at_zero"` - StringRegisterLocation string `toml:"string_register_location"` + AfterConnectPause config.Duration `toml:"pause_after_connect"` + PollPause config.Duration `toml:"pause_between_requests"` + CloseAfterGather bool `toml:"close_connection_after_gather"` + ReadCoilsStartingAtZero bool `toml:"read_coils_starting_at_zero"` + StringRegisterLocation string `toml:"string_register_location"` + OnRequestPerField bool `toml:"one_request_per_field"` + MaxBitRegistersPerRequest uint16 `toml:"max_bit_registers_per_request"` + MaxWordRegistersPerRequest uint16 `toml:"max_word_registers_per_request"` } // According to github.com/grid-x/serial @@ -140,6 +142,13 @@ func (m *Modbus) Init() error { return fmt.Errorf("retries cannot be negative in device %q", m.Name) } + if m.Workarounds.MaxBitRegistersPerRequest > maxQuantityCoils { + return fmt.Errorf("maximum number of bit-registers cannot exceed %d", maxQuantityCoils) + } + if m.Workarounds.MaxWordRegistersPerRequest > maxQuantityHoldingRegisters { + return fmt.Errorf("maximum number of word-registers cannot exceed %d", maxQuantityHoldingRegisters) + } + // Determine the configuration style var cfg configuration switch m.ConfigurationType { diff --git a/plugins/inputs/modbus/sample_general_end.conf b/plugins/inputs/modbus/sample_general_end.conf index 1c2f7973c8b07..3423cb52e02d4 100644 --- a/plugins/inputs/modbus/sample_general_end.conf +++ b/plugins/inputs/modbus/sample_general_end.conf @@ -30,11 +30,6 @@ ## stay connected during gather and disconnect afterwards. # close_connection_after_gather = false - ## Force the plugin to read each field in a separate request. - ## This might be necessary for devices not conforming to the spec, - ## see https://github.com/influxdata/telegraf/issues/12071. - # one_request_per_field = false - ## Enforce the starting address to be zero for the first request on ## coil registers. This is necessary for some devices see ## https://github.com/influxdata/telegraf/issues/8905 @@ -49,3 +44,18 @@ ## upper -- use only upper byte of the register (XX00 XX00 XX00 XX00) ## By default both bytes of the register are used (XXXX XXXX). # string_register_location = "" + + ## Force the plugin to read each field in a separate request. + ## This might be necessary for devices not conforming to the spec, + ## see https://github.com/influxdata/telegraf/issues/12071. + # one_request_per_field = false + + ## Maximum number of coil or discrete-input registers to read per request + ## The specification defines this limit as 2000 but some devices only + ## support a smaller number per request + # max_bit_registers_per_request = 2000 + + ## Maximum number of input or holding registers to read per request + ## The specification defines this limit as 125 but some devices only + ## support a smaller number per request + # max_word_registers_per_request = 125