hx.modbus doc
Overview
The ModbusExt implements connector support for Modbus protocol.
A connector describes its device one of two ways: a register map CSV, or the xeto spec which models the device. See Register Map and Addr Specs.
Supported Protocols
ModbusExt supports two protocols for accessing Modbus slave devices:
TCP/IP
To setup a connector to access a Modbus slave over TCP/IP, we use the
modbus-tcp URI scheme:
dis: "Modbus TCP/IP Connector"
conn
modbusConn
uri: `modbus-tcp://host/`
modbusSlave: 1
modbusRegMapUri: `data/modbus/xxx.csv`
RTU over TCP/IP
To access the slave using serial RTU over a TCP connection, use the
modbus-rtutcp scheme:
dis: "Modbus RTU over TCP/IP Connector"
conn
modbusConn
uri: `modbus-rtutcp://host/`
modbusSlave: 1
modbusRegMapUri: `data/modbus/xxx.csv`
Register Map
A connector may define a register map using the modbusRegMapUri
tag. The register map specifies which points get read/written out of a Modbus
slave. The register map URI must be a path relative to the project's home
directory or can be a fan:// URI to a CSV file bundled in a pod.
A connector whose points address their registers from a spec needs no register map - see Addr Specs.
Register maps are simple CSV files:
name, addr, data, rw, scale, dis, unit, tags
ping, 40001, u2, r, , Ping, ,
do0, 00017, bit, rw, , DO-0, ,
do1, 00017, bit, rw, , DO-1, ,
ai0, 40001, u2, r, , AI-0, ,
ai1, 40002, u2, r, *10, AI-1, kW, power
Addressing
Register address are defined with addr column and specified using
Modbus convention, where the leading digit selects the register type and
the rest is the 1-based register number. Both the 5-digit form and the
6-digit extended form are accepted:
0xxxx Coil 00001-09999 0xxxxx 000001-065536
1xxxx Discrete Input 10001-19999 1xxxxx 100001-165536
3xxxx Input Register 30001-39999 3xxxxx 300001-365536
4xxxx Holding Register 40001-49999 4xxxxx 400001-465536
The two are equivalent where they overlap: 40001 and 400001 are both
holding register 1. Registers above 9999 need the extended form, as do
addresses authored in a spec - see Addr Specs.
Ping
A valid ping register is required in order to test connectivity to slave.
A connector configured by device spec names a point instead - see
Ping Point.
Data Type
The data column specifies how register data is modeled.
bit Bool
u1 Unsigned 8-bit Int
u2 Unsigned 16-bit Int
u4 Unsigned 32-bit Int
s1 Signed 8-bit Int
s2 Signed 16-bit Int
s4 Signed 32-bit Int
s8 Signed 64-bit Int
f4 32-bit Float
f8 64-bit Float
Bit Mask
The bit data type supports a position notation for cases where bits are
packed into input or holding registers. The position is 0 to 15:
name, addr, data, rw
do0, 40101, bit:0, rw
do1, 40101, bit:1, rw
do2, 40101, bit:2, rw
Word and Byte Order
If register data is not stored in network byte order, you can specify the order using a suffix:
u4le Unsigned 32-bit Int Little endian byte and word order
u4leb Unsigned 32-bit Int Little endian byte order only
u4lew Unsigned 32-bit Int Little endian word order only
Word order only applies to types which span more than one register, so on a
16-bit type u2lew is the same as u2, and u2le the same as u2leb.
Read/Write
Read and write permissions are configured using rw column:
rw Register may be read and written
r Register is read-only
w Register is write-only
Scale Factor
An optional scale factor can be applied to registers using the scale column.
The scale format is ([op] [num])+ where the factor is a numeric constant.
add: +1.5
minus: -0.25
mult: *10
div: /1000
You can also chain multiple scaling factors together:
chained: +32768 /10
Every term needs its operator: a bare 0.1 is not a scale and fails the row
rather than loading the register unscaled.
Dis, Unit, Tags
The dis, unit, and tags column allow optional pre-configuration for the
point during the learn process:
name, addr, data, rw, dis, unit, tags
ai1, 40002, u2, r, AI-1, kW, power foo bar
The unit must be a known unit symbol or name; an unrecognized one fails the
row rather than loading the register unitless.
Current Points
Modbus proxy points are configured with the modbusCur tag, which names a register in the connector's register map:
point
modbusConnRef: @conn
modbusCur: ai0
A point may instead address its register directly from a spec - see Addr Specs.
Writable Points
Modbus proxy points are configured to write to remote system points via the modbusWrite tag:
point
writable
modbusConnRef: @conn
modbusWrite: ao5
A point may instead address its register directly from a spec - see Addr Specs.
Addr Specs
A point may address its registers from an equip template instead of a register
map. Author a ModbusAddr on the point using the
modbusCurAddr and modbusWriteAddr globals:
Vav : Equip {
points: {
zoneTemp: ZoneAirTempSensor {
modbusCurAddr: { addr: "400101", encoding: "s2", scale: "*0.1" }
modbusWriteAddr: { addr: "400201", encoding: "s2" }
}
}
}
Binding the template to a connector sets modbusCur and modbusWrite to the qname of the point spec. The tag selects which addr global is read off it, and the connector takes the encoding, scale and byte order from there:
point
modbusConnRef: @conn
modbusCur: "acme.vav::Vav.points.zoneTemp"
modbusWrite: "acme.vav::Vav.points.zoneTemp"
Any value containing "::" is resolved as a qname; anything else is a register map name, so the two styles can be mixed on one connector.
Field Mapping
The spec fields map onto the register map columns as follows:
| ModbusAddr | Register map | Notes |
|---|---|---|
addr |
addr |
Must be 6-digit extended Modicon |
encoding |
data |
Combined with bitIndex and byteOrder |
bitIndex |
data |
Only applies to bit encoding |
byteOrder |
data |
Suffix, be adds none |
scale |
scale |
Requires a leading operator such as *0.1 |
dis |
dis |
There is no unit field on the addr; the point's own unit tag is applied to
the value.
Device Spec
A connector whose points address their registers this way names the spec which models the device with modbusDeviceSpec, and needs no modbusRegMapUri:
modbusConn
uri: `modbus-tcp://host/`
modbusSlave: 1
modbusDeviceSpec: "acme.vav::Vav"
modbusPingAddr: "zoneTemp"
Learn
With a device spec configured, connLearn() walks that spec's points rather
than a register map. The root level is a folder per register type in use -
coils, discrete inputs, input registers, holding registers - and expanding one
lists its points in register order. Each learned point carries its dis,
kind and unit from the point spec, and addresses its registers by qname.
A connector with a register map is learned the same way, grouping its registers into the same folders.
Ping Point
There is no ping register without a register map, so a connector configured
by device spec reads one of that spec's points instead.
modbusPingAddr names it, such as zoneTemp, and its modbusCurAddr
is the register read.
History
History synchronization is not supported by Modbus. You will need to use collection to store history.
Block Reads
Modbus connectors will attempt to optimize consecutive registers into a single
block read when possible. This behavior can be tuned by adding the
modbusBlockGap and modbusBlockMax tags to your modbusConn rec, or by configuring
them on the connector's associated conn tuning rec.
modbusBlockGap
By default, blocks require all points to be in truly consecutive registers:
// block 1
40001 u2
40002 u2
40003 u2
// block 2
40005 u2
If you have a small gap in an otherwise sequential run of registers, you can tune to maintain a single read by adding the modbusBlockGap tag to your conn rec. This tag specifies the maximum number of registers that can be skipped for a single block:
modbusConn
modbusBlockGap: 1
// block 1
40001 u2
40002 u2
40003 u2
40005 u2 // 40004 will be read but discarded
modbusBlockMax
By default, blocks are limited to 100 total registers (which includes skipped
registers from modbusBlockGap). To force a connector to split blocks into
smaller or larger chunks, add the modbusBlockMax tag to your conn rec:
modbusConn
modbusBlockMax: 2
// block 1
40001 u2
40002 u2
// block 2
40003 u2
40004 u2
Be aware a block read failure will result in the entire block's points being marked as fault.
Frame Delay
Every conn that shares a uri shares a single connection to that endpoint, and
their transactions are serialized onto it back-to-back with no gap. That is
usually what you want, but it breaks a multi-drop TCP-to-RTU gateway: the
gateway needs turnaround time to switch the RS-485 bus direction, and the
slaves require roughly 3.5 character-times of silence before the next frame.
The failure looks like this. The first request in a poll cycle is answered
normally, and the second request - whichever unit it happens to target - is
never answered at all. Because the frame is discarded on the serial side rather
than rejected by a slave, no Modbus exception code comes back; the read simply
times out. Enabling a single slave on the gateway masks the problem, since
ConnTuning.pollTime then supplies the gap by accident.
Use modbusFrameDelay to force a minimum silence between transactions:
modbusConn
uri: `modbus-tcp://gateway/`
modbusFrameDelay: 50ms
Applied to a connTuning rec it paces every conn that references it, which is
usually the better way to configure a whole gateway:
dis: "Gateway Tuning"
connTuning
modbusFrameDelay: 50ms
A value on the conn rec takes precedence over the tuning rec. The default is 0sec, which preserves the original back-to-back behavior.
The delay is measured from the end of one transaction to the start of the next, and it applies to every frame on the wire - including the read-modify-write pair issued for a bit-mask register.
Note the delay happens inside the window bounded by
modbusReadTimeout and modbusWriteTimeout (both of
which default to Conn.actorTimeout). Those timeouts cover queue wait plus
pacing plus the transaction itself, so on a gateway with N slaves they must be
large enough to cover the full serialized cycle or the polls will still time
out.