Add retry when slave is busy to modbus input (#7271)
This commit is contained in:
parent
050ed9e61e
commit
1006c65587
|
@ -20,6 +20,14 @@ The Modbus plugin collects Discrete Inputs, Coils, Input Registers and Holding R
|
||||||
## Timeout for each request
|
## Timeout for each request
|
||||||
timeout = "1s"
|
timeout = "1s"
|
||||||
|
|
||||||
|
## Maximum number of retries and the time to wait between retries
|
||||||
|
## when a slave-device is busy.
|
||||||
|
## NOTE: Please make sure that the overall retry time (#retries * wait time)
|
||||||
|
## is always smaller than the query interval as otherwise you will get
|
||||||
|
## an "did not complete within its interval" warning.
|
||||||
|
#busy_retries = 0
|
||||||
|
#busy_retries_wait = "100ms"
|
||||||
|
|
||||||
# TCP - connect via Modbus/TCP
|
# TCP - connect via Modbus/TCP
|
||||||
controller = "tcp://localhost:502"
|
controller = "tcp://localhost:502"
|
||||||
|
|
||||||
|
@ -53,7 +61,7 @@ The Modbus plugin collects Discrete Inputs, Coils, Input Registers and Holding R
|
||||||
|
|
||||||
## Analog Variables, Input Registers and Holding Registers
|
## Analog Variables, Input Registers and Holding Registers
|
||||||
## measurement - the (optional) measurement name, defaults to "modbus"
|
## measurement - the (optional) measurement name, defaults to "modbus"
|
||||||
## name - the variable name
|
## name - the variable name
|
||||||
## byte_order - the ordering of bytes
|
## byte_order - the ordering of bytes
|
||||||
## |---AB, ABCD - Big Endian
|
## |---AB, ABCD - Big Endian
|
||||||
## |---BA, DCBA - Little Endian
|
## |---BA, DCBA - Little Endian
|
||||||
|
|
|
@ -3,6 +3,7 @@ package modbus
|
||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
@ -27,6 +28,8 @@ type Modbus struct {
|
||||||
StopBits int `toml:"stop_bits"`
|
StopBits int `toml:"stop_bits"`
|
||||||
SlaveID int `toml:"slave_id"`
|
SlaveID int `toml:"slave_id"`
|
||||||
Timeout internal.Duration `toml:"timeout"`
|
Timeout internal.Duration `toml:"timeout"`
|
||||||
|
Retries int `toml:"busy_retries"`
|
||||||
|
RetriesWaitTime internal.Duration `toml:"busy_retries_wait"`
|
||||||
DiscreteInputs []fieldContainer `toml:"discrete_inputs"`
|
DiscreteInputs []fieldContainer `toml:"discrete_inputs"`
|
||||||
Coils []fieldContainer `toml:"coils"`
|
Coils []fieldContainer `toml:"coils"`
|
||||||
HoldingRegisters []fieldContainer `toml:"holding_registers"`
|
HoldingRegisters []fieldContainer `toml:"holding_registers"`
|
||||||
|
@ -84,6 +87,14 @@ const sampleConfig = `
|
||||||
## Timeout for each request
|
## Timeout for each request
|
||||||
timeout = "1s"
|
timeout = "1s"
|
||||||
|
|
||||||
|
## Maximum number of retries and the time to wait between retries
|
||||||
|
## when a slave-device is busy.
|
||||||
|
## NOTE: Please make sure that the overall retry time (#retries * wait time)
|
||||||
|
## is always smaller than the query interval as otherwise you will get
|
||||||
|
## an "did not complete within its interval" warning.
|
||||||
|
#busy_retries = 0
|
||||||
|
#busy_retries_wait = "100ms"
|
||||||
|
|
||||||
# TCP - connect via Modbus/TCP
|
# TCP - connect via Modbus/TCP
|
||||||
controller = "tcp://localhost:502"
|
controller = "tcp://localhost:502"
|
||||||
|
|
||||||
|
@ -159,6 +170,10 @@ func (m *Modbus) Init() error {
|
||||||
return fmt.Errorf("device name is empty")
|
return fmt.Errorf("device name is empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if m.Retries < 0 {
|
||||||
|
return fmt.Errorf("retries cannot be negative")
|
||||||
|
}
|
||||||
|
|
||||||
err := m.InitRegister(m.DiscreteInputs, cDiscreteInputs)
|
err := m.InitRegister(m.DiscreteInputs, cDiscreteInputs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -642,11 +657,22 @@ func (m *Modbus) Gather(acc telegraf.Accumulator) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
timestamp := time.Now()
|
timestamp := time.Now()
|
||||||
err := m.getFields()
|
for retry := 0; retry <= m.Retries; retry += 1 {
|
||||||
if err != nil {
|
timestamp = time.Now()
|
||||||
disconnect(m)
|
err := m.getFields()
|
||||||
m.isConnected = false
|
if err != nil {
|
||||||
return err
|
mberr, ok := err.(*mb.ModbusError)
|
||||||
|
if ok && mberr.ExceptionCode == mb.ExceptionCodeServerDeviceBusy && retry < m.Retries {
|
||||||
|
log.Printf("I! [inputs.modbus] device busy! Retrying %d more time(s)...", m.Retries-retry)
|
||||||
|
time.Sleep(m.RetriesWaitTime.Duration)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
disconnect(m)
|
||||||
|
m.isConnected = false
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Reading was successful, leave the retry loop
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
grouper := metric.NewSeriesGrouper()
|
grouper := metric.NewSeriesGrouper()
|
||||||
|
|
|
@ -494,3 +494,131 @@ func TestHoldingRegisters(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRetrySuccessful(t *testing.T) {
|
||||||
|
retries := 0
|
||||||
|
maxretries := 2
|
||||||
|
value := 1
|
||||||
|
|
||||||
|
serv := mbserver.NewServer()
|
||||||
|
err := serv.ListenTCP("localhost:1502")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer serv.Close()
|
||||||
|
|
||||||
|
// Make read on coil-registers fail for some trials by making the device
|
||||||
|
// to appear busy
|
||||||
|
serv.RegisterFunctionHandler(1,
|
||||||
|
func(s *mbserver.Server, frame mbserver.Framer) ([]byte, *mbserver.Exception) {
|
||||||
|
data := make([]byte, 2)
|
||||||
|
data[0] = byte(1)
|
||||||
|
data[1] = byte(value)
|
||||||
|
|
||||||
|
except := &mbserver.SlaveDeviceBusy
|
||||||
|
if retries >= maxretries {
|
||||||
|
except = &mbserver.Success
|
||||||
|
}
|
||||||
|
retries += 1
|
||||||
|
|
||||||
|
return data, except
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("retry_success", func(t *testing.T) {
|
||||||
|
modbus := Modbus{
|
||||||
|
Name: "TestRetry",
|
||||||
|
Controller: "tcp://localhost:1502",
|
||||||
|
SlaveID: 1,
|
||||||
|
Retries: maxretries,
|
||||||
|
Coils: []fieldContainer{
|
||||||
|
{
|
||||||
|
Name: "retry_success",
|
||||||
|
Address: []uint16{0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = modbus.Init()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
err = modbus.Gather(&acc)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotEmpty(t, modbus.registers)
|
||||||
|
|
||||||
|
for _, coil := range modbus.registers {
|
||||||
|
assert.Equal(t, uint16(value), coil.Fields[0].value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRetryFail(t *testing.T) {
|
||||||
|
maxretries := 2
|
||||||
|
|
||||||
|
serv := mbserver.NewServer()
|
||||||
|
err := serv.ListenTCP("localhost:1502")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer serv.Close()
|
||||||
|
|
||||||
|
// Make the read on coils fail with busy
|
||||||
|
serv.RegisterFunctionHandler(1,
|
||||||
|
func(s *mbserver.Server, frame mbserver.Framer) ([]byte, *mbserver.Exception) {
|
||||||
|
data := make([]byte, 2)
|
||||||
|
data[0] = byte(1)
|
||||||
|
data[1] = byte(0)
|
||||||
|
|
||||||
|
return data, &mbserver.SlaveDeviceBusy
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("retry_fail", func(t *testing.T) {
|
||||||
|
modbus := Modbus{
|
||||||
|
Name: "TestRetryFail",
|
||||||
|
Controller: "tcp://localhost:1502",
|
||||||
|
SlaveID: 1,
|
||||||
|
Retries: maxretries,
|
||||||
|
Coils: []fieldContainer{
|
||||||
|
{
|
||||||
|
Name: "retry_fail",
|
||||||
|
Address: []uint16{0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = modbus.Init()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
err = modbus.Gather(&acc)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Make the read on coils fail with illegal function preventing retry
|
||||||
|
counter := 0
|
||||||
|
serv.RegisterFunctionHandler(1,
|
||||||
|
func(s *mbserver.Server, frame mbserver.Framer) ([]byte, *mbserver.Exception) {
|
||||||
|
counter += 1
|
||||||
|
data := make([]byte, 2)
|
||||||
|
data[0] = byte(1)
|
||||||
|
data[1] = byte(0)
|
||||||
|
|
||||||
|
return data, &mbserver.IllegalFunction
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("retry_fail", func(t *testing.T) {
|
||||||
|
modbus := Modbus{
|
||||||
|
Name: "TestRetryFail",
|
||||||
|
Controller: "tcp://localhost:1502",
|
||||||
|
SlaveID: 1,
|
||||||
|
Retries: maxretries,
|
||||||
|
Coils: []fieldContainer{
|
||||||
|
{
|
||||||
|
Name: "retry_fail",
|
||||||
|
Address: []uint16{0},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
err = modbus.Init()
|
||||||
|
assert.NoError(t, err)
|
||||||
|
var acc testutil.Accumulator
|
||||||
|
err = modbus.Gather(&acc)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Equal(t, counter, 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue