Modbus
The panel is a Modbus master — it cyclically reads (and writes) the registers of the connected devices into the tag table. Two transports are supported, both at the same time:
| Transport | Physical layer | When |
|---|---|---|
| RTU | RS485 terminals of the board | I/O modules, drives, sensors on a cable |
| TCP | Ethernet or WiFi | energy meters with a LAN module, RTU↔TCP gateways, cabinet devices with LAN |
Slave mode (the panel as a Modbus server for a supervisory system) is a planned extension — it is not part of the product yet.
A tag with a Modbus source
Every Modbus tag has: unit (device address), fc (coil / discrete /
holding / input), address (0-based, not the 3xxxx/4xxxx notation),
word_order (big/little — the word order of 32-bit values) and
optionally transport: tcp + ip + port.
- Without
transport(or withtransport: rtu) the tag goes over RS485 — the behaviour of older projects does not change. f32/u32tags are read from two registers. The word order differs from vendor to vendor and is the most common source of nonsensical values — when importing from the catalog it is set correctly for you.- Scaling:
value = raw × gain + offset. Analog tags need a deadband — without one a noisy value repaints the screen for nothing.
tags:
- name: voltage_l1
datatype: f32
source: { modbus: { transport: tcp, ip: 192.168.0.50, port: 502,
unit: 1, fc: input, address: 0, word_order: big } }
Limits and behaviour
| Limit | Value | Why |
|---|---|---|
| Distinct TCP targets (IP:port) per project | 4 | ~6–8 kB of RAM per connection on the ESP32-S3 |
| Tags in total | 512 | product limit |
- Tags on the same IP:port share a single connection — 20 tags from
one energy meter is still just 1 target. Different
unitvalues on the same IP (an RTU↔TCP gateway with several devices behind it) also count as 1 target. - The editor enforces the limit both on import and at compile time — a project over the limit is rejected on your desk, not as a stuttering panel on the machine.
- Target outage: tags hold their last value, one event goes into the diagnostic buffer when the target drops out and one when it returns. The panel reconnects automatically (1 s backoff); polling of the other targets keeps running.
- The poller runs outside the PLC cycle — a slow or unreachable Modbus device never extends the logic cycle.
Catalog of Modbus devices
An "EDS for Modbus": the directory devices/modbus/*.yaml, one file =
one device (vendor, model, datasheet, word_order) plus a register map
(address, fc, data type, scale, unit, description). The catalog is
curated by inovaea and is signed like all the other catalogs.
Using it in the editor: the Modbus RTU master or Modbus TCP
master page → Add Modbus device… → pick from the catalog → tick the
registers → choose the transport (RS485 / TCP + IP) → Import tags.
The tags get a prefix derived from the model (sdm630_voltage), the
right data type, word order and scaling.
First entries: Eastron SDM120 (single-phase meter) and SDM630 (three-phase meter).
Your own device
A device that is not in the catalog does not have to be written by hand:
- Add Modbus device… → in the right-hand pane (while nothing is
selected) fill in vendor and model → New device. This
creates the file
devices/modbus/<vendor>_<model>.yamlwith one sample register. - In the device window → the Registers tab → tick Edit register
map. The row below the table adds registers (name, address, class,
data type, unit); the
xon a row deletes a register. - Save to catalog writes the map back into the
.yaml.
The file is the single source of truth — the project only keeps the path to it, so the entry appears on disk right away, not only when the project is saved.
| Rule | Why |
|---|---|
file name <vendor>_<model>.yaml, lower-case ASCII |
the catalog is versioned in git and must be readable on every OS |
uid: 0x0000 for customer entries |
the numbered space belongs to the curator, otherwise uids would clash |
| one register per line | the parser reads line by line; a flow map wrapped onto two lines loses everything after the first line |
| renaming a model creates a NEW file | the old one may be used in another project, so it is not deleted |
The manual route remains: copy devices/modbus/_template.yaml and fill
in the register map from the device manual.
Modbus slave — the panel answers
The panel is not only a master. It can also be a slave: a supervisory SCADA, PLC or Home Assistant reads its state and sends setpoints. The panel still runs fully without them — SCADA is a window to the outside, not a condition for operation.
modbus_slave:
rtu: false # answer on the RS485 line
tcp: true # answer over TCP
unit: 9 # unit ID of the panel
port: 502
word_order: big # word order of 32-bit values
tags:
- { tag: boiler_temp } # 4x 0-1, read-only
- { tag: temp_setpoint, writable: true } # 4x 2-3
- { tag: pump } # 0x 0, bit
In the editor: RS485 → Modbus RTU slave or Ethernet → Modbus TCP slave. The page shows exactly the map the integrator will read — both the protocol address and the number from the manual.
How addresses are assigned
Automatically, in list order: bool → coils (0x) from 0, everything
else → holding registers (4x) from 0, where i32/u32/f32 take up
two registers. The address goes into the pack explicitly, so
reordering tags in the editor does not shift the map. The order in the
list can be changed with the arrow — it is part of the configuration,
not cosmetics.
A write is a command
A tag without writable ticked answers a write with exception
0x02; it never passes silently. The same rule as with WebSocket and
MQTT: the panel decides about a write, not whoever asks for it.
Further deliberate decisions:
FC6(write single register) on a 32-bit value is rejected — a half-written value is worse than a refusal.FC16first validates the whole range and only then writes — a half-executed write would leave the machine in an undefined state.- Reading a range that lies even partly outside the map returns an exception. Zeros would send the integrator looking for the fault at their end.
- A broadcast (address 0) on RTU is executed but not answered — the standard requires it; otherwise two slaves would transmit at once.
Pitfalls
| Pitfall | Fix |
|---|---|
| Master and slave on the same RS485 line | a line has one master — packbuild rejects this at compile time and the editor shows it in red right on the page |
| Wrong word order | word_order must match what the SCADA expects — the same trap as on the master side |
| Several SCADA clients at once | the TCP slave serves one connection; each further one would cost ~6–8 kB of RAM, which the S3 does not have to spare |
Diagnostics: VUI! prints mbslv=<requests>,<exceptions>. A growing
exception count means the SCADA is reading outside the map or trying to
write to a tag without permission.
Typical pitfalls
| Pitfall | Fix |
|---|---|
| Nonsensical f32 values | swap word_order (big ↔ little) |
| Address off by one | the vendor documentation counts from 1 or uses the 3xxxx notation — in ctrl32 the address is always the 0-based protocol address |
| Device does not answer over RTU | check the baud rate/parity, the A/B wires and the termination |
| TCP target behind NAT or in another segment | the panel and the device belong on the same (separate) machine network |