Skip to content

Commit

Permalink
Implement workarounds (pause between reads and disconnect after gathe…
Browse files Browse the repository at this point in the history
…r) and allow to trace Modbus connectivity.
  • Loading branch information
Sven Rebhan committed Jun 14, 2021
1 parent da7f2c7 commit 5641cfb
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 12 deletions.
24 changes: 23 additions & 1 deletion plugins/inputs/modbus/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ Registers via Modbus TCP or Modbus RTU/ASCII.
# stop_bits = 1
# transmission_mode = "RTU"

## Trace the connection to the modbus device as debug messages
# trace_connection = false

## Enable workarounds required by some devices to work correctly
# workarounds = {
## Pause between read requests sent to the device. This might be necessary for (slow) serial devices.
# pause_between_requests = "0ms"
## Close the connection after every gather cycle. Usually the plugin closes the connection after a certain
## idle-timeout, however, if you query a device with limited simultaneous connectivity (e.g. serial devices)
## from multiple instances you might want to only stay connected during gather and disconnect afterwards.
# close_connection_after_gather = false
# }

## Measurements
##
Expand Down Expand Up @@ -128,6 +140,8 @@ with N decimal places'.
from unsigned values).

### Trouble shooting

#### Strange data
Modbus documentations are often a mess. People confuse memory-address (starts at one) and register address (starts at zero) or stay unclear about the used word-order. Furthermore, there are some non-standard implementations that also
swap the bytes within the register word (16-bit).

Expand All @@ -139,7 +153,15 @@ In case you get an `exception '2' (illegal data address)` error you might try to

In case you see strange values, the `byte_order` might be off. You can either probe all combinations (`ABCD`, `CDBA`, `BADC` or `DCBA`) or you set `byte_order="ABCD" data_type="UINT32"` and use the resulting value(s) in an online converter like [this](https://www.scadacore.com/tools/programming-calculators/online-hex-converter/). This makes especially sense if you don't want to mess with the device, deal with 64-bit values and/or don't know the `data_type` of your register (e.g. fix-point floating values vs. IEEE floating point).

If nothing helps, please post your configuration, error message and/or the output of `byte_order="ABCD" data_type="UINT32"` to one of the telegraf support channels (forum, slack or as issue).
If your data still looks corrupted, please post your configuration, error message and/or the output of `byte_order="ABCD" data_type="UINT32"` to one of the telegraf support channels (forum, slack or as issue).

#### Workarounds
Some Modbus devices need special read characteristics when reading data and will fail otherwise. For example, there are certain serial devices that need a certain pause between register read requests. Others might only offer a limited number of simultaneously connected devices, like serial devices or some ModbusTCP devices. In case you need to access those devices in parallel you might want to disconnect immediately after the plugin finished reading.

To allow this plugin to also handle those "special" devices there is the `workarounds` configuration options. In case your documentation states certain read requirements or you get read timeouts or other read errors you might want to try one or more workaround options.
If you find that workarounds are required for your device, please let us know.

In case your device needs a workaround that is not yet implemented, please open an issue or submit a pull-request.

### Example Output

Expand Down
82 changes: 71 additions & 11 deletions plugins/inputs/modbus/modbus.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,26 @@ import (
"github.com/influxdata/telegraf/plugins/inputs"
)

type ModbusWorkarounds struct {
PollPause config.Duration `toml:"pause_between_requests"`
CloseAfterGather bool `toml:"close_connection_after_gather"`
}

// Modbus holds all data relevant to the plugin
type Modbus struct {
Name string `toml:"name"`
Controller string `toml:"controller"`
TransmissionMode string `toml:"transmission_mode"`
BaudRate int `toml:"baud_rate"`
DataBits int `toml:"data_bits"`
Parity string `toml:"parity"`
StopBits int `toml:"stop_bits"`
Timeout config.Duration `toml:"timeout"`
Retries int `toml:"busy_retries"`
RetriesWaitTime config.Duration `toml:"busy_retries_wait"`
Log telegraf.Logger `toml:"-"`
Name string `toml:"name"`
Controller string `toml:"controller"`
TransmissionMode string `toml:"transmission_mode"`
BaudRate int `toml:"baud_rate"`
DataBits int `toml:"data_bits"`
Parity string `toml:"parity"`
StopBits int `toml:"stop_bits"`
Timeout config.Duration `toml:"timeout"`
Retries int `toml:"busy_retries"`
RetriesWaitTime config.Duration `toml:"busy_retries_wait"`
DebugConnection bool `toml:"debug_connection"`
Workarounds ModbusWorkarounds `toml:"workarounds"`
Log telegraf.Logger `toml:"-"`
// Register configuration
ConfigurationOriginal
// Connection handling
Expand Down Expand Up @@ -97,6 +104,24 @@ const sampleConfig = `
# stop_bits = 1
# transmission_mode = "RTU"
## Close the connection to the server after each gather cycle
## This option is helpful for devices only accepting a limited number of
## simultaneous connections (some ModbusTCP devices).
# close_connection = false
## Trace the connection to the modbus device as debug messages
# debug_connection = false
## Enable workarounds required by some devices to work correctly
# workarounds = {
## Pause between read requests sent to the device. This might be necessary for (slow) serial devices.
# pause_between_requests = "0ms"
## Close the connection after every gather cycle. Usually the plugin closes the connection after a certain
## idle-timeout, however, if you query a device with limited simultaneous connectivity (e.g. serial devices)
## from multiple instances you might want to only stay connected during gather and disconnect afterwards.
# close_connection_after_gather = false
# }
## Measurements
##
Expand Down Expand Up @@ -231,6 +256,11 @@ func (m *Modbus) Gather(acc telegraf.Accumulator) error {
m.collectFields(acc, timestamp, tags, requests.input)
}

// Disconnect after read if configured
if m.Workarounds.CloseAfterGather {
return m.disconnect()
}

return nil
}

Expand All @@ -248,6 +278,9 @@ func (m *Modbus) initClient() error {
}
handler := mb.NewTCPClientHandler(host + ":" + port)
handler.Timeout = time.Duration(m.Timeout)
if m.DebugConnection {
handler.Logger = m
}
m.handler = handler
case "file":
switch m.TransmissionMode {
Expand All @@ -258,6 +291,9 @@ func (m *Modbus) initClient() error {
handler.DataBits = m.DataBits
handler.Parity = m.Parity
handler.StopBits = m.StopBits
if m.DebugConnection {
handler.Logger = m
}
m.handler = handler
case "ASCII":
handler := mb.NewASCIIClientHandler(u.Path)
Expand All @@ -266,6 +302,9 @@ func (m *Modbus) initClient() error {
handler.DataBits = m.DataBits
handler.Parity = m.Parity
handler.StopBits = m.StopBits
if m.DebugConnection {
handler.Logger = m
}
m.handler = handler
default:
return fmt.Errorf("invalid protocol '%s' - '%s' ", u.Scheme, m.TransmissionMode)
Expand Down Expand Up @@ -320,6 +359,7 @@ func (m *Modbus) gatherRequestsCoil(requests []request) error {
if err != nil {
return err
}
nextRequest := time.Now().Add(time.Duration(m.Workarounds.PollPause))
m.Log.Debugf("got coil@%v[%v]: %v", request.address, request.length, bytes)

// Bit value handling
Expand All @@ -331,6 +371,9 @@ func (m *Modbus) gatherRequestsCoil(requests []request) error {
request.fields[i].value = uint16((bytes[idx] >> bit) & 0x01)
m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, (bytes[idx]>>bit)&0x01, request.fields[i].value)
}

// Some (serial) devices require a pause between requests...
time.Sleep(time.Until(nextRequest))
}
return nil
}
Expand All @@ -342,6 +385,7 @@ func (m *Modbus) gatherRequestsDiscrete(requests []request) error {
if err != nil {
return err
}
nextRequest := time.Now().Add(time.Duration(m.Workarounds.PollPause))
m.Log.Debugf("got discrete@%v[%v]: %v", request.address, request.length, bytes)

// Bit value handling
Expand All @@ -353,6 +397,9 @@ func (m *Modbus) gatherRequestsDiscrete(requests []request) error {
request.fields[i].value = uint16((bytes[idx] >> bit) & 0x01)
m.Log.Debugf(" field %s with bit %d @ byte %d: %v --> %v", field.name, bit, idx, (bytes[idx]>>bit)&0x01, request.fields[i].value)
}

// Some (serial) devices require a pause between requests...
time.Sleep(time.Until(nextRequest))
}
return nil
}
Expand All @@ -364,6 +411,7 @@ func (m *Modbus) gatherRequestsHolding(requests []request) error {
if err != nil {
return err
}
nextRequest := time.Now().Add(time.Duration(m.Workarounds.PollPause))
m.Log.Debugf("got holding@%v[%v]: %v", request.address, request.length, bytes)

// Non-bit value handling
Expand All @@ -376,6 +424,9 @@ func (m *Modbus) gatherRequestsHolding(requests []request) error {
request.fields[i].value = field.converter(bytes[offset : offset+length])
m.Log.Debugf(" field %s with offset %d with len %d: %v --> %v", field.name, offset, length, bytes[offset:offset+length], request.fields[i].value)
}

// Some (serial) devices require a pause between requests...
time.Sleep(time.Until(nextRequest))
}
return nil
}
Expand All @@ -387,6 +438,7 @@ func (m *Modbus) gatherRequestsInput(requests []request) error {
if err != nil {
return err
}
nextRequest := time.Now().Add(time.Duration(m.Workarounds.PollPause))
m.Log.Debugf("got input@%v[%v]: %v", request.address, request.length, bytes)

// Non-bit value handling
Expand All @@ -399,6 +451,9 @@ func (m *Modbus) gatherRequestsInput(requests []request) error {
request.fields[i].value = field.converter(bytes[offset : offset+length])
m.Log.Debugf(" field %s with offset %d with len %d: %v --> %v", field.name, offset, length, bytes[offset:offset+length], request.fields[i].value)
}

// Some (serial) devices require a pause between requests...
time.Sleep(time.Until(nextRequest))
}
return nil
}
Expand Down Expand Up @@ -427,6 +482,11 @@ func (m *Modbus) collectFields(acc telegraf.Accumulator, timestamp time.Time, ta
}
}

// Implement the logger interface of the modbus client
func (m *Modbus) Printf(format string, v ...interface{}) {
m.Log.Debugf(format, v...)
}

// Add this plugin to telegraf
func init() {
inputs.Add("modbus", func() telegraf.Input { return &Modbus{} })
Expand Down

0 comments on commit 5641cfb

Please sign in to comment.