Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

modbus_gateway input plugin #8013

Closed
wants to merge 28 commits into from
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d4f41af
Create modbusgw input plugin
wz2b Aug 19, 2020
e7531b5
Create modbusgw input plugin
wz2b Aug 19, 2020
b278450
Merge branch 'master' of https://github.com/influxdata/telegraf into …
wz2b Aug 20, 2020
cb616e7
Remove toml "default" and add default to Init
wz2b Aug 20, 2020
504bb0a
rename module from modbusgw to modbus_gateway. Removed plugin name f…
wz2b Aug 20, 2020
7d4b5c3
Fix name of plugin (was modbusgw, now modbus_gateway) in README.md
wz2b Aug 20, 2020
0773598
Fixed some typos in the README.md and made the intent of default regi…
wz2b Aug 21, 2020
03f5b6d
fix missing close quote in comment
wz2b Aug 21, 2020
fe7df2d
Add OutputFormat parameter to field definitions to allow fields to be…
wz2b Aug 22, 2020
42012c5
Fixed error in unit test that made it not compile. Added unit tests …
wz2b Aug 23, 2020
419aff2
Merge branch 'master' of https://github.com/influxdata/telegraf into …
wz2b Aug 23, 2020
9e26adb
An entirely new way to convert values, using the built-in go binary s…
wz2b Aug 24, 2020
65635ac
Improved one of the for loops to not have 'i' hanging around past the…
wz2b Aug 24, 2020
1700001
Not sure why, but licenses were missing, probably do to some dependen…
wz2b Aug 25, 2020
87d8b24
Merge branch 'master' of https://github.com/influxdata/telegraf into …
wz2b Aug 25, 2020
8c36cdf
license declaration in wrong alphabetical order
wz2b Aug 25, 2020
5a45c6a
Updated CustomByteOrder to work using a pointer receiver rather than …
wz2b Aug 25, 2020
b3c79bb
oops, missed one change due to conversion update
wz2b Aug 25, 2020
5896856
fix tests
wz2b Aug 25, 2020
de37d8f
some dependency failed, ran go mod tidy
wz2b Aug 25, 2020
5aa4654
Add unit tests for converting (using the modbus byte order specifier)…
wz2b Aug 26, 2020
09711a0
Add floating point conversion tests; find some bugs, fix them up.
wz2b Aug 26, 2020
8d38560
Add a read flush before launching the next poll. This is to address …
wz2b Aug 26, 2020
09dd9c7
The defautl modbus implementation used by the original driver has a b…
wz2b Aug 26, 2020
ac4e062
use wz2b/modbus v0.1.1
wz2b Aug 26, 2020
5597346
Updated plugin documentation
wz2b Aug 28, 2020
ff5fee9
Add a little debug output
wz2b Aug 28, 2020
6614e88
Merge branch 'master' into modbusgw-input-plugin
ssoroka May 18, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/mesos"
_ "github.com/influxdata/telegraf/plugins/inputs/minecraft"
_ "github.com/influxdata/telegraf/plugins/inputs/modbus"
_ "github.com/influxdata/telegraf/plugins/inputs/modbus_gateway"
_ "github.com/influxdata/telegraf/plugins/inputs/mongodb"
_ "github.com/influxdata/telegraf/plugins/inputs/monit"
_ "github.com/influxdata/telegraf/plugins/inputs/mqtt_consumer"
Expand Down
188 changes: 188 additions & 0 deletions plugins/inputs/modbus_gateway/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
# Modbus "Gateway" Plugin

The Modbus Gateway plugin collects Input Registers and Holding
Registers via Modbus TCP. It is similar to the "modbus" driver (and uses the same
underlying protocol implementation) but has a different configuration format suited to
communicating with Modbus/TCP devices acting as _gateways_. A gateway is still a modbus server,
but instead of reaching one device a gateway has many real or virtual devices attached to it.
The devices beyond the gateway are often connected via modems, radios, or multi-drop RS-485
or RS-422 buses.

The motivation behind this plugin is to address the situation where there are multiple devices
behind a gateway, but the gateway can't accept many TCP connections at once. This is
a typical case (unfortunately) because many gateways on the market are made using
small microcontrollers with small RAM and limited TCP/IP stacks. Even some
expensive devices, like the PowerLogic CM4200, only allow 4 simultaneous connections.
The [MBAP](https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf)
protocol includes a _unit identifier_ in each request specifically so that one TCP
connection can be shared talking to multiple devices. (Historical note: Modbus/UDP exists
as well, but is used less often).

### Naming Conventions

To address the Modbus Organization's
[new naming conventions](https://modbus.org/docs/Client-ServerPR-07-2020-final.docx.pdf)
we are adopting the following terminology:

- _Address_ refers to a 16-bit register address. Note that _input_ and _holding_ registers
occupy different address spaces (you can have an input register 1000 and a holding
register 1000, and they are different)
- _Gateway_ is a modbus "Server" thay _may_ have multiple devices attached to it
- _Unit Address_ is the 8-bit address of an individual physical or virtual device
attached to the gateway. It is difficult to think of a microcontroller with a
serial port and 2KB of RAM as a "server" so in agreement with the
[MBAP](https://modbus.org/docs/Modbus_Messaging_Implementation_Guide_V1_0b.pdf)
specification I'm calling it a "device" that has a _Unit Address_.


## When to use this plugin

- The Modbus/TCP device answers to multiple unit addresses and you
do not want the plugin to make separate, concurrent TCP connections to each
- There are multiple modbus devices attached to the gateway (typically serial),
each with it's own distinct unit address
- You want more control over how register fetches are grouped into
bulk modbus requests (even when the registers you want to fetch are not
immediately adjacent to each other)

## When to use the original modbus plugin

- The device uses a direct serial connection (RS-232, RS-485)
- The request is for modbus functions other than _READ INPUT_ and
_READ MULTIPLE HOLDING REGISTERS_. This plugin does not support discretes
(digital inputs and outputs) at this time (planned to be added later)
- The data types being retrieved are other than INT16, UINT16, INT32, UINT32
- The device uses ASCII mode (rare these days)

## Configuration

Each `[[input]]` communicates to one gateway and one or more devices. It is
perfectly reasonable to have only one device on the gateway - in fact, this
is the typical case when you have a single Ethernet modbus device.

In the configuration you create _requests_ then map the results of those requests
to _fields_. A request defines a single modbus _PDU_ (protocol data unit), or one message
sent to the gateway that (hopefully) solicits exactly one response message. The payload of
that response is a string of bytes. _Field_ definitions tell the plugin how to interpret
those bytes to turn the received values into measurements.

Sometimes a modbus device has a register layout like this:

`[1000][1001][1002][1003]`

but you don't want to store the middle registers at all. Why would you do that? Because it
can be less "expensive" to have fewer requests. If you were to request all four values,
but only assign 1000 and 1993 to measurements, you sent 8 unwanted bytes across the
wire. That's far cheaper than requesting 1000 and 1003 in totally separate requests.
To accomodate this type of request a field can be marked _omit=true_ meaning the
response is received but not stored as a field of the measurement.

## Sample Configuration
```toml
[[inputs.modbus_gateway]]
#
# Name of this input - should be unique
#
name="sma"

#
# Address and port of the modbus server or gateway
#
gateway="tcp://yourserver.com:502"

#
# Response timeout, in go duration fornat
# Usually can be set pretty low
#
timeout="5s"

#
# Request (poll) definitions
#
# Request parameters:
#
# unit - required. Unit address of device being polled. Per spec, the value is between
# 1 and 247, or 0 for broadcast. The values 0 or 255 are usually accepted to communicate
# directly to a Modbus/TCP device not acting as a gateway. Historically this has been a
# point of confusion. If this is what you want (to talk to the gateway itself), try 255 first.
# Using the broadcast address can cause unexpected device responses.
#
# address - the register address of the first register being requested. This address is zero-based.
# For example, the first holding register is address 0. Be aware that some documentation
#
# count - how many 16-bit registers to request
#
# type - defines the register type, which maps internally to the function code used to the
# PDU (request). Must be "holding" or "input", if unspecified defaults to "holding"
#
# measurement - the name of the measurement, for example when stored in influx
#
# fields - defines how the response PDU is mapped to fields of the measurement. Attributes
# of each field are:
#
# name - name of the field
#
# type - must be INT32, UINT32, INT16, or UINT16. More types will be added in the future.
#
# scale, offset - math performed on the raw modbus value before storing.
# stored field value = (modbus value * scale) + offset
#
# omit - if true, don't store this field at all. you must still set a 'type'. Use this to
# skip fields not of interest that are part of the response because they are within the
# requested register range.
#
# outfmt - the output format. Defaults to FLOAT64, but may be set to INT32, INT64,
# INT (alias for INT64), FLOAT32, FLOAT64, or FLOAT (alias for FLOAT64). Note
# that the scale and offset are applied using float64 math, but you may specify
# an integer output format for a variety of reasons including to save space and
# to avoid floating point imprecision (the value is -399 but it is written as -398.9999 ...)
# When converting to an integer format, the float value will be rounded to the nearest
# integer. In most cases, it's best to leave the output format at the default (float64)
# especially if you are outputting to InfluxDB and using flux, as having fields of
# mixed types might make you have to use typecasts in your queries.
#
requests = [
{ unit=3, address=30769, count=8, type="holding", measurement="pv1", fields = [
{name="Ipv", type="INT32", scale=0.001},
{name="Vpv", type="INT32", scale=0.01},
{name="Ppv", type="INT32", omit=true},
{name="Pac", type="INT32", scale=1.0},
] },
{ unit=4, address=30769, count=8, type="holding", measurement="pv2", fields = [
{name="Ipv", type="INT32", scale=0.001},
{name="Vpv", type="INT32", scale=0.01},
{name="Ppv", type="INT32", omit=true},
{name="Pac", type="INT32", scale=1.0},
] },
{ unit=5, address=30769, count=8, type="holding", measurement="pv3", fields = [
{name="Ipv", type="INT32", scale=0.001},
{name="Vpv", type="INT32", scale=0.01},
{name="Ppv", type="INT32", omit=true},
{name="Pac", type="INT32", scale=1.0},
] },
{ unit=6, address=30769, count=8, type="holding", measurement="pv4", fields = [
{name="Ipv", type="INT32", scale=0.001},
{name="Vpv", type="INT32", scale=0.01},
{name="Ppv", type="INT32", omit=true},
{name="Pac", type="INT32", scale=1.0},
] },
{ unit=7, address=30769, count=8, type="holding", measurement="pv5", fields = [
{name="Ipv", type="INT32", scale=0.001},
{name="Vpv", type="INT32", scale=0.01},
{name="Ppv", type="INT32", omit=true},
{name="Pac", type="INT32", scale=1.0},
] },
{ unit=8, address=30769, count=8, type="holding", measurement="pv6", fields = [
{name="Ipv", type="INT16", scale=0.001},
{name="Vpv", type="INT16", scale=0.01},
{name="Ppv", type="INT32", omit=true},
{name="Pac", type="INT32", scale=1.0},
] },
{ unit=9, address=30769, count=8, type="holding", measurement="pv7", fields = [
{name="Ipv", type="INT32", scale=0.001},
{name="Vpv", type="INT32", scale=0.01},
{name="Ppv", type="INT32", omit=true},
{name="Pac", type="INT32", scale=1.0},
] },
]
```
33 changes: 33 additions & 0 deletions plugins/inputs/modbus_gateway/connect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package modbus_gateway

import (
mb "github.com/goburrow/modbus"
"net"
"net/url"
)

func connect(m *ModbusGateway) error {
u, err := url.Parse(m.Gateway)
if err != nil {
return err
}
var host, port string
host, port, err = net.SplitHostPort(u.Host)
if err != nil {
return err
}
m.tcpHandler = mb.NewTCPClientHandler(host + ":" + port)
m.tcpHandler.Timeout = m.Timeout.Duration
m.client = mb.NewClient(m.tcpHandler)
err = m.tcpHandler.Connect()
if err != nil {
return err
}
m.isConnected = true
return err
}

func disconnect(m *ModbusGateway) error {
m.tcpHandler.Close()
return nil
}
81 changes: 81 additions & 0 deletions plugins/inputs/modbus_gateway/gather.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package modbus_gateway

import (
"bytes"
"encoding/binary"
"fmt"
"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/metric"
"time"
)

func (m *ModbusGateway) Gather(acc telegraf.Accumulator) error {
if !m.isConnected {
err := connect(m)
if err != nil {
m.isConnected = false
return err
}
}

grouper := metric.NewSeriesGrouper()

for _, req := range m.Requests {
now := time.Now()
m.tcpHandler.SlaveId = req.Unit
var resp []byte
var err error

if req.RequestType == "holding" {
resp, err = m.client.ReadHoldingRegisters(req.Address, req.Count)
} else if req.RequestType == "input" {
resp, err = m.client.ReadInputRegisters(req.Address, req.Count)
} else {
return fmt.Errorf("Don't know how to poll register type \"%s\"", req.RequestType)
}

if err == nil {

reader := bytes.NewReader(resp)

for _, f := range req.Fields {
switch f.InputType {
case "UINT16":
var value uint16
binary.Read(reader, binary.BigEndian, &value)
outputToGroup(grouper, &req, &f, int64(value), now)
break
case "INT16":
var value int16
binary.Read(reader, binary.BigEndian, &value)
outputToGroup(grouper, &req, &f, int64(value), now)
break
case "UINT32":
var value uint32
binary.Read(reader, binary.BigEndian, &value)
outputToGroup(grouper, &req, &f, int64(value), now)
break
case "INT32":
var value int32
binary.Read(reader, binary.BigEndian, &value)
outputToGroup(grouper, &req, &f, int64(value), now)

break

}

}

} else {

m.Log.Info("Modbus Error: ", err)
}

}

for _, metric := range grouper.Metrics() {
acc.AddMetric(metric)
}

return nil
}
58 changes: 58 additions & 0 deletions plugins/inputs/modbus_gateway/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package modbus_gateway

import (
"fmt"
"strings"
)

func (m *ModbusGateway) Init() error {
for i := range m.Requests {
request := &m.Requests[i]

/*
* If no register type was specified, default to "holding"
*/
if request.RequestType == "" {
request.RequestType = "holding"
} else {
/*
* User specified the register type - make sure they made a valid selection
*/
request.RequestType = strings.ToLower(request.RequestType)
if request.RequestType != "holding" && request.RequestType != "input" {
return fmt.Errorf("Request type must be \"holding\" or \"input\"")
}
}

/*
* Check field mappings
*/
for j := range m.Requests[i].Fields {
field := &m.Requests[i].Fields[j]

if field.Scale == 0.0 {
field.Scale = 1.0
}

field.InputType = strings.ToUpper(field.InputType)
if field.InputType == "" {
field.InputType = "UINT16"
}

field.OutputFormat = strings.ToUpper(field.OutputFormat)
if field.OutputFormat == "" {
field.OutputFormat = "FLOAT64"
} else {
switch field.OutputFormat {
case "INT", "UINT", "INT64", "UINT64", "FLOAT", "FLOAT32", "FLOAT64":
break
default:
return fmt.Errorf("Invalid output format")

}
}

wz2b marked this conversation as resolved.
Show resolved Hide resolved
}
}
return nil
}
Loading