Reference — Data Types
The bridge is a relay, not an application codec — it does not interpret business payloads. What it does
read and write are a small set of protobuf envelope structures: the edgecommons message envelope (to append
a hop tag and, for a reply, to rewrite a header), the reserved _relay hop tag, the reply reply_to, and the
UNS class taxonomy that decides routing. This page is the reference for those structures. For the topics and
messages they ride on, see messaging-interface.md.
Protobuf envelope and diagnostic JSON
Section titled “Protobuf envelope and diagnostic JSON”Normal EdgeCommons messages on MQTT are protobuf EdgeCommonsMessage bytes. The bridge decodes those bytes,
mutates only the relay metadata it owns, and re-encodes protobuf before republishing.
| Kind | What it is | How the bridge treats it |
|---|---|---|
| EdgeCommons message | Protobuf bytes for the standard { header, identity, tags, body } envelope. |
Decoded; the hop tag is appended (and for replies, header.reply_to is rewritten or stripped); re-encoded and forwarded. Opaque body bytes are preserved inside the envelope. |
| Foreign / non-protobuf bytes | Anything that cannot decode as an edgecommons protobuf envelope, including a hand-written JSON object sent directly to MQTT. | Dropped as MalformedEnvelope; counted in relay_malformed_dropped. |
JSON appears in these docs only as a diagnostic projection after protobuf decode. It is useful for logs, tests, documentation, and UI inspection, but it is not the normal wire payload. If an application needs to carry opaque bytes such as an image, a PLC frame, or another protobuf, those bytes belong in the message body as the protobuf opaque body, with a content type.
The consequence for loop protection: every relayable message has an envelope that can carry the hop tag. The uplink/downlink class-disjointness still provides an additional structural guard (see messaging-interface.md), but non-protobuf bytes are not a relayable fallback path.
The EdgeCommons envelope
Section titled “The EdgeCommons envelope”The standard edgecommons envelope is encoded as protobuf. The bridge only ever reads/writes header (for
replies) and tags (for the hop tag); identity and body travel untouched. In particular, envelope tags
are orthogonal metadata and are unrelated to the opaque/application payload carried in body.
// Diagnostic JSON projection after protobuf decode, not wire bytes.{ "header": { "name": "reload-config", // the message/verb name "version": "1.0", "uuid": "…", "timestamp": "…", "correlation_id": "corr-1", // preserved verbatim across the bridge "reply_to": "edgecommons/reply-<uuid>" // REWRITTEN on downlink; DROPPED on the reply back-haul }, "identity": { "hier": [ … ], "path": "dallas/gw-01", "component": "opcua-adapter", "instance": "main" }, "tags": { "site": "dallas", // arbitrary business metadata — untouched "_relay": [ "gw-01/uns-bridge" ] // the RESERVED hop tag the bridge appends }, "body": { … } // untouched}The bridge never invents an identity or body. Protobuf bytes may be re-encoded after mutation, so byte
identity of the whole envelope is not the contract; semantic envelope identity is, except for the two touches
below. Opaque body bytes are preserved byte-for-byte inside the body.
The _relay hop tag
Section titled “The _relay hop tag”The reserved envelope tag tags._relay is the bridge’s loop-protection ledger.
| Aspect | Value |
|---|---|
| Key | _relay (the _ prefix marks a library/system-reserved tag key, alongside _bcast). |
| Type | A protobuf list value containing strings; diagnostic JSON renders it as an array of strings. |
| Element | A hop id: {device}/uns-bridge (this bridge’s device token + the component token). |
| Order | Insertion order — each bridge appends its own id to the end; foreign hops are preserved in order. |
The three rules the bridge applies before appending (see explanation):
- Own echo — if the array already contains this bridge’s own hop id → drop (
OwnEcho). - Max hops — if the array already holds
maxHopsids (default 4) → drop (MaxHopsExceeded). - Otherwise append this bridge’s id and forward.
Edge behaviors, exactly as implemented:
- An envelope with no
tagsmember grows one containing just_relayon first hop. - A non-list
_relay(a spec violation by some non-conforming relay) is normalized to a fresh list (with a warning); themaxHopscap still bounds any residual cycle. - A non-protobuf message is not hop-stamped or forwarded; it drops as
MalformedEnvelope. - Consumers should ignore
_relayfor business logic; it doubles as a “which bridges did this traverse” breadcrumb.
reply_to — the two rewrites
Section titled “reply_to — the two rewrites”header.reply_to is the only header field the bridge modifies, and it does so in exactly two places, only
after decoding protobuf commands/replies:
| Where | What the bridge does |
|---|---|
Downlink cmd with reply_to |
Replaces the site-side reply_to (e.g. edgecommons/reply-<uuid> on the site broker) with a freshly minted edgecommons/reply-<uuid> topic on the device bus, subscribes that topic, and records bridge topic → original site reply_to. A cmd without reply_to is a fire-and-forget notification — passed through untouched. |
| The reply back-haul | header.reply_to is dropped entirely (a reply carries none, and a device-bus topic is meaningless at the site). |
correlation_id, body, identity, and every other tag are preserved across both rewrites. The minted topic
uses the core’s standard edgecommons/reply- prefix, so it is a non-UNS topic (never matches a UNS filter) and
is structurally exempt from the reserved-class guard. A non-protobuf command or reply is dropped as malformed,
not proxied as opaque transport bytes.
The UNS class taxonomy (what routes where)
Section titled “The UNS class taxonomy (what routes where)”Routing is by the class token — the 5th topic level (ecv1/{device}/{component}/{instance}/{class}). The
eight closed UNS classes, and how the bridge treats each:
| Class | Leaf/Channeled | Reserved? (library-owned publish) | Uplink (device→site) | Downlink (site→device) |
|---|---|---|---|---|
state |
leaf | reserved | ✅ always | — |
cfg |
leaf | reserved | ✅ always | — |
evt |
channeled | open | ✅ always (+ disconnect replay buffer) | — |
metric |
channeled | reserved | ✅ always | — |
data |
channeled | open | ✅ always | — |
log |
channeled | reserved | ✅ always (default; sample disables) | — |
app |
channeled | open | ⚙️ opt-in (default off) | — |
cmd |
channeled | open | ❌ never | ✅ own-device only |
- Leaf classes (
state,cfg) end at the class token — no channel; their subscription filters have no trailing/#. Channeled classes require ≥ 1 channel token; their filters end in/#. - Reserved classes are library-owned on the publishing side (a component may not raw-publish to them), but the bridge relays them freely — it operates below the reserved-class guard by design.
- The uplink set (
state cfg evt metric data log, +appwhen enabled) and the downlink set (cmd) are disjoint — the structural loop guard.
The rehydration broadcast envelope
Section titled “The rehydration broadcast envelope”On a site-reconnect rising edge the bridge publishes two notification-style cmd envelopes on the device bus
(ecv1/{device}/_bcast/main/cmd/republish-state and …/republish-cfg):
{ "header": { "name": "republish-state", "version": "1.0" }, "body": {} }They carry no identity, no tags, and no reply_to — fire-and-forget. Each device component
answers by re-announcing its state keepalive and effective cfg. Answering is built into the edgecommons library
(the four-language device-side RepublishListener), on by default — components need no wiring. See
explanation → reconnect rehydration.
The LWT payload
Section titled “The LWT payload”The site Last-Will is a normal MQTT will registered by the reused provider on the site connection. It is derived by the bridge, not configured:
| Field | Value |
|---|---|
| topic | ecv1/{device}/uns-bridge/main/state (the bridge’s resolved state topic) |
| payload | protobuf EdgeCommons state envelope from the bridge identity |
| body | { "status": "UNREACHABLE" } |
| qos | 1 |
Because the will lands on the bridge’s own state topic, a site console tracking ecv1/+/+/+/state sees the
whole device flip to UNREACHABLE on an abrupt bridge/device death — no bespoke plumbing.
Metric value shapes
Section titled “Metric value shapes”The relay counters are emitted as edgecommons metrics (see metrics.md). Two value kinds:
| Kind | Emitted value | Examples |
|---|---|---|
| Counter | the interval delta since the previous 30 s snapshot (curr − prev, saturating so a restart never yields a negative), so deltas sum correctly in CloudWatch/EMF |
relay_uplinked, relay_downlinked, relay_dropped_*, relay_reply_*, relay_evt_* |
| Gauge | the current value | relay_pending_replies (in-flight replies), site_connected (1/0) |
Per-class counter metrics (relay_uplinked, relay_dropped_disabled, relay_dropped_rate,
relay_dropped_disconnected) carry one measure per class in the fixed order
state, cfg, evt, metric, data, log, app; scalar counters carry a single count measure; the
site_connected gauge carries a single connected measure (unit None; everything else is Count).
Drop reasons (for reading logs & counters)
Section titled “Drop reasons (for reading logs & counters)”Every non-forward decision has a reason, which maps to a counter:
| Reason | Meaning | Counter |
|---|---|---|
OwnEcho |
hop tag already holds our own id | relay_loop_dropped |
MaxHopsExceeded |
hop tag already holds maxHops ids |
relay_loop_dropped |
NotUnsTopic |
topic isn’t a valid ecv1/…/{class} UNS topic |
relay_routed_dropped |
ClassNotRelayed |
class doesn’t flow in this direction (e.g. cmd on uplink) |
relay_routed_dropped |
NotOwnDevice |
a downlink cmd for a different device |
relay_routed_dropped |
MalformedEnvelope |
payload cannot decode as a valid edgecommons protobuf envelope | relay_malformed_dropped |
| (disabled / rate / disconnected) | uplink policy verdicts, per class | relay_dropped_disabled / _rate / _disconnected |