Reference — Messaging Interface
Every topic and message the adapter publishes or accepts, and the CLI flags. Addressing follows the
Unified Namespace (UNS): ecv1/{device}/{component}/{instance}/{class}[/channel]. For the
data/control plane model, see explanation.md; for client recipes, the
how-to guides.
{device}— the resolved Thing name (the lasthierarchylevel).{component}— the component UNS token,modbus-adapter.{instance}— a device instance id (plc1, …) fordata/evt, and optionally oncmdtopics to address a device by topic; absent on the component-scopecmdinbox, thestatekeepalive, andmetric(whose envelope identity carriesmain).
Envelope
Section titled “Envelope”All messages use the EdgeCommons JSON envelope: {header, identity, tags, body}.
The library stamps the top-level identity ({hier, path, component, instance}) on every message
built from config. tags is arbitrary business metadata.
Request/reply carries header.reply_to + header.correlation_id; the reply is published to
reply_to with the same correlation_id.
"identity": { "hier": [ { "level": "site", "value": "lab" }, { "level": "shop", "value": "s1" }, { "level": "line", "value": "l1" }, { "level": "device", "value": "gw-01" } ], "path": "lab/s1/l1/gw-01", "component": "modbus-adapter", "instance": "plc1"}Topics
Section titled “Topics”| Class | Message | Direction | Topic | Scope | Reply |
|---|---|---|---|---|---|
data |
SouthboundSignalUpdate |
adapter → bus | ecv1/{device}/modbus-adapter/{instance}/data/{signal} |
— | — |
evt |
evt |
adapter → bus | ecv1/{device}/modbus-adapter/{instance}/evt/{severity}/{connection|write} |
— | — |
cmd |
sb/read |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/sb/read |
instance |
{ok,result} |
cmd |
sb/write |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/sb/write |
instance |
{ok,result} |
cmd |
sb/status |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/sb/status |
instance |
{ok,result} |
cmd |
sb/signals |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/sb/signals |
instance |
{ok,result} |
cmd |
sb/browse |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/sb/browse |
instance |
{ok,result} |
cmd |
sb/pause |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/sb/pause |
instance |
{ok,result} |
cmd |
sb/resume |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/sb/resume |
instance |
{ok,result} |
cmd |
reconnect |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/reconnect |
instance |
{ok,result} |
cmd |
repoll |
bus → adapter | ecv1/{device}/modbus-adapter[/{instance}]/cmd/repoll |
instance |
{ok,result} |
metric |
southbound_health, ModbusConnection, ModbusInventory, ModbusPoll, ModbusPublish, ModbusCommand |
adapter → bus (auto) | ecv1/{device}/modbus-adapter/metric/{metricName} |
— | — |
state |
keepalive | adapter → bus (auto) | ecv1/{device}/modbus-adapter/state |
— | — |
The Scope column is each verb’s declared command scope, the value a describe reply carries in
commands[].scope. Every verb this adapter serves is instance-scoped: it acts on one configured
slave.
Fleet consumers subscribe the six UNS wildcards — telemetry is one filter,
ecv1/+/+/+/data/#; events ecv1/+/+/+/evt/#; metrics ecv1/+/+/+/metric/#; state
ecv1/+/+/+/state. state/metric/cfg/log are library-owned reserved classes — a component
publish to them is rejected; the adapter only ever mints data/evt topics via the data()/events()
facades and cmd replies via the command inbox — never a hand-assembled topic string.
The command inbox
Section titled “The command inbox”The read/write/control surface is served through the library’s command inbox, which subscribes
both command scopes on the primary connection: the component scope
ecv1/{device}/modbus-adapter/cmd/# and the instance scope ecv1/{device}/modbus-adapter/+/cmd/#.
A request’s verb is the topic channel after the /cmd/ marker and must equal header.name.
Built-in verbs (ping, status, describe, reload-config, get-configuration) ship with every
component; the adapter adds the sb/* + reconnect/repoll verbs below.
Every request is addressed to one configured device. Each verb declares the instance scope, and
the addressing resolves in this order:
- An instance-scoped request (
ecv1/{device}/modbus-adapter/{instance}/cmd/…) addresses the device named by the topic’s instance token; no body selector is needed. - A component-scoped request (
ecv1/{device}/modbus-adapter/cmd/…) addresses the device named by theinstancefield in the request body. - A body
instancethat disagrees with the topic’s instance token is refused withBAD_ARGS, before the adapter runs the verb. - A request that names no instance at all addresses the sole configured device; on a multi-device
adapter it is
BAD_ARGS. - An addressed instance that names no configured device is
NO_SUCH_INSTANCE.
The reply body is {"ok": true, "result": <verb result>} on success, or
{"ok": false, "error": {"code", "message"}} on failure.
Error codes
Section titled “Error codes”The adapter uses the standardized southbound error-code set:
| Code | Meaning |
|---|---|
BAD_ARGS |
Malformed request — no addressed instance on a multi-device adapter, a body instance conflicting with the topic’s instance token, a bad sb/browse cursor or ref, or a browse request mixing the paged and hierarchical forms. |
PAUSED |
The whole operation is prohibited while the instance is paused — repoll on a paused instance. |
NO_SUCH_INSTANCE |
The instance selector names no configured device. |
WRITE_NOT_ALLOWED |
Every entry of an sb/write batch is refused by the instance’s writes.allow list. |
WRITE_FAILED |
Every attempted (allow-listed) write in the batch was rejected by the device. |
RECONNECT_FAILED |
A reconnect attempt could not re-establish the link. |
Sample object
Section titled “Sample object”The sb/read reply’s reads[] entries (below) always carry all five fields explicitly:
| Field | Type | Notes |
|---|---|---|
value |
number | boolean | string | Per the signal’s type (see data-types.md). |
quality |
string | Normalized GOOD | BAD | UNCERTAIN. |
qualityRaw |
string | Good, or the Modbus exception / timeout text on failure. |
sourceTs |
null | Modbus has no device timestamp. |
serverTs |
string | Adapter read time, ISO-8601 UTC. |
data-class samples (below) go through the data() facade instead, which omits a field rather
than emitting it null, and defaults an omitted quality to GOOD with qualityRaw: "unspecified"
(Modbus has no native quality codes) rather than the literal string "Good".
Per the southbound four-slot timestamp model, serverTs is the capture time: the adapter stamps it
the moment the register read completes (on the poll path and in sb/read replies alike), so a
batched publish carries the read-time stamp; sourceTs is absent (Modbus supplies no device time),
and receivedTs is not emitted (a direct-client adapter’s receipt and capture coincide).
Data plane
Section titled “Data plane”SouthboundSignalUpdate (adapter → bus, data class)
Section titled “SouthboundSignalUpdate (adapter → bus, data class)”Published through the library’s data() facade (gg.instance(id).data()), which constructs the body, sanitizes the channel, mints
the topic, and stamps the envelope identity — the adapter only ever calls
.signal(id).name(n).address(a).device(...).add_samples(...).signal_path(p).publish(). Topic
ecv1/{device}/modbus-adapter/{instance}/data/{signal} — {signal} is the sanitized signal name. The
stable signal.id and protocol-native signal.address stay in the body (consumers key on those, not
the topic channel). Quality has no Modbus-native meaning, so a successful read omits it and the facade
defaults it to GOOD with qualityRaw: "unspecified" (a synthesized-vs-device-reported marker); a
failed read passes an explicit BAD with the exception text as qualityRaw.
"body": { "device": { "adapter": "modbus", "instance": "plc1", "endpoint": "tcp://10.0.0.50:502 unit=1" }, "signal": { "id": "u1/holding/0/float32", "name": "Temperature", "address": { "unitId": 1, "table": "holding", "address": 0, "type": "float32", "wordOrder": "big", "byteOrder": "big" } }, "samples": [ { "value": 21.4, "quality": "GOOD", "qualityRaw": "unspecified", "serverTs": "2026-07-03T01:48:00Z" } ]}Published when a polled value changes (publishMode: onChange, gated by the signal’s deadband) or
every poll (always). One message carries one signal’s samples (one, or many when
publish.batchMs > 0).
sb/write (command)
Section titled “sb/write (command)”Each entry is gated by the instance’s writes.allow[] allow-list — matched on the stable
signal.id, before any device I/O (a signal not on the list is refused without touching the
device). An empty writes.allow makes the instance read-only. Body:
"body": { "instance": "plc1", "writes": [ { "name": "Setpoint", "value": 42.5 } ] }// result: { "id": "plc1", "written": 1, "results": [ { "signal": "Setpoint", "value": 42.5, "ok": true } ] }A single {name,value} object (no writes array) is also accepted. A signal-ref is either
{ "name": "<configured signal>" } (friendly; uses that signal’s table/type/order) or explicit
{ "unitId"?, "table", "address", "type", "wordOrder"?, "byteOrder"?, "scale"?, "offset"?, "count"? }.
Entries not on writes.allow, without value, an unresolvable ref, a read-only table
(discrete/input), or a bit signal are reported per-entry as {"ok": false, "error": …}. When
every entry is an allow-list refusal the reply is a WRITE_NOT_ALLOWED error; when every
attempted write is rejected by the device it is a WRITE_FAILED error. Each attempted write also
emits an evt/info/write/evt/warning/write audit event. Writes use FC5/FC15 (coil), FC6/FC16
(holding).
sb/read (command, request/reply)
Section titled “sb/read (command, request/reply)”// request body"body": { "instance": "plc1", "signals": [ { "name": "Temperature" }, { "unitId": 1, "table": "input", "address": 0, "type": "uint16" } ] }// reply body: { "ok": true, "result": { "id": "plc1", "reads": [// { "signal": { "id": "...", "address": {...} }, "value": 21.4, "quality": "GOOD", "qualityRaw": "Good", "sourceTs": null, "serverTs": "..." } ] } }Unresolvable refs are omitted (match by signal). A signal that errors returns an entry with
quality: BAD and the exception in qualityRaw.
Control plane
Section titled “Control plane”sb/status→result = { "id", "state", "connected", "paused", "metrics": { "read": {interval,total}, "write": {interval,total} } }.stateis the instance state token (ONLINE/PAUSED/BACKOFF/CONNECTING, see State keepalive) — the same token the keepalive publishes for that instance, so the pulled answer and the pushed one always agree.sb/signals→result = { "id", "signals": [ { "name", "unitId", "signalId", "address" }, ... ] }— the whole configured/polled inventory in one shot.sb/browse— a walk of the configured inventory (Modbus has no address-space discovery), in two mutually exclusive request forms:- Paged (body
{instance?, cursor?, max?}) →result = { "id", "entries": [ { "id", "name", "type" }, ... ], "cursor"? }.cursoris an opaque offset token, present only while more pages remain. Distinct fromsb/signals(single-shot, full). - Hierarchical — the
treeBrowserpanel mode, selected by the presence ofref(body{instance?, ref, depth?, maxRefs?}) →result = { "id", "mode": "hierarchical", "root": { "nodeId", "name", "nodeClass", "dataType", "refs": [ { "referenceType": "contains", "target": {...} } ] }, "refCount", "depth", "truncated" }.ref: "root"is the device node whosecontainsrefs are the signal inventory (bounded bymaxRefs); a signal id is a known leaf ("refs": []); an unknown ref isBAD_ARGS.depthandmaxRefsare clamped to 1..4 and 1..1000. Mixingref/depth/maxRefswithcursor/max, or sendingdepth/maxRefswithoutref, isBAD_ARGS.
- Paged (body
sb/pause(body{instance}) → suspends polling/publishing for the instance; confirmed + idempotent;result = { "id", "paused": true, "changed" }.sb/resume(body{instance}) → resumes a paused instance; confirmed + idempotent;result = { "id", "paused": false, "changed" }.reconnect(body{instance}) → drops and re-establishes the Modbus link (one bounded attempt);result = { "id", "connected" }or aRECONNECT_FAILEDerror.repoll(body{instance}) → forces one immediate poll cycle;result = { "id", "polled": <groups> }. Refused withPAUSEDwhile the instance is paused.
Events (evt class)
Section titled “Events (evt class)”Published through the library’s events() facade (gg.instance(id).events()): severity derives the channel
evt/{severity}/{type}, so the topic and the body can never disagree — identical in shape to the
OPC UA reference adapter.
"body": { "severity": "critical", "type": "connection", "message": "Modbus link down", "timestamp": "2026-07-03T01:48:00Z", "context": { "endpoint": "tcp://10.0.0.50:502 unit=1" }, "alarm": true, "active": true}evt/critical/connection— a Modbus link up/down transition per instance, modeled as a stateful alarm:raise_alarm("connection", ...)on drop (alarm:true, active:true),clear_alarm("connection", ...)on restore (active:false) — both ride the sameevt/critical/connectionchannel, so a console trackingevt/critical/#sees both ends. Context carries{endpoint}(the connection description, e.g. slave address).evt/info/write/evt/warning/write— a per-write audit record,infoon success andwarningon failure —emit("write", message, {signal, value, error?}, severity).
A fleet consumer subscribing ecv1/+/+/+/evt/critical/# sees only alarm-grade events without
per-adapter knowledge of the channel shape.
Metrics (metric class, reserved — automatic)
Section titled “Metrics (metric class, reserved — automatic)”The metric subsystem publishes health and Modbus operational metrics on the reserved metric class
(ecv1/{device}/modbus-adapter/metric/{metricName}) through MetricEmitter; the component never
addresses that topic itself. For every metric’s dimensions, measures, units, and diagnostic purpose,
see Reference - Metrics.
State keepalive (state class, reserved — automatic)
Section titled “State keepalive (state class, reserved — automatic)”The library’s heartbeat publishes the state keepalive on the reserved state class
(ecv1/{device}/modbus-adapter/state) every ~5 s — the component never addresses that topic
itself. The RUNNING keepalive also carries an instances array: one entry per configured slave
(component.instances[]), so a fleet consumer sees every slave’s up/down state under the one component
without a separate UNS instance per slave (identity, data, and lifecycle stay under main).
"body": { "status": "RUNNING", "uptimeSecs": 3600, "instances": [ { "instance": "plc1", "connected": true, "state": "ONLINE", "detail": "tcp://10.0.0.50:502 unit=1" }, { "instance": "plc2", "connected": true, "state": "PAUSED", "detail": "tcp://10.0.0.51:502 unit=1" }, { "instance": "plc3", "connected": false, "state": "CONNECTING" } ]}connected— live liveness, driven by the poll reads themselves: any response that arrives (data, or even a slave exception for e.g. an illegal address) marks the link up; a transport/IO error, aModbusIOException, or no response marks it down. It is not pymodbus’s cachedclient.connected(which reflects intent and lags a socket that died mid-session), so a mid-session southbound loss shows up promptly asconnected: falseon the next keepalive.state— the instance’s condition in the shared vocabulary, the same tokensb/statusreturns:ONLINE(the slave answers reads),PAUSED(sb/pauseis latched and the link is up, so the instance is deliberately quiet rather than stale),BACKOFF(the link is down and being retried),CONNECTING(the instance’s device has not come up yet). Link truth wins: a paused instance whose link is down reportsBACKOFF—CONNECTINGbefore its first connect — neverPAUSED, and the pause stays visible insb/status’spausedfield.detail— the connection describe string (tcp://host:port unit=N/rtu://COM@baud unit=N); omitted before that slave’s device has connected (connectedis thenfalse).instancesis present only on the RUNNING keepalive (the best-effortSTOPPEDshutdown state, and a keepalive with no configured slaves, omit it).
| Flag | Values | Notes |
|---|---|---|
--platform |
GREENGRASS | HOST | KUBERNETES | auto |
Default auto. |
--transport |
MQTT [path] | IPC |
HOST/K8s use MQTT; the path is the messaging config. |
-c/--config |
FILE <path> | ENV | GG_CONFIG | CONFIGMAP | … |
Default from the platform. |
-t/--thing |
<name> |
IoT Thing name; the {device} token of every UNS topic. |