How-to Guides
Recipes for specific tasks. Each assumes the bridge builds and runs (see the tutorial). For concepts see explanation.md; for exhaustive options see reference/configuration.md; for complete worked configs see sample-configurations.md.
Declare the site broker
Section titled “Declare the site broker”The site broker is the bridge’s external system, declared in the bridge’s own component.instances[]
exactly the way an adapter declares its OPC UA endpoints — reusing the library’s mqttBroker shape (no
schema change). Put it in the entry with id: "site":
"component": { "instances": [ { "id": "site", "siteBroker": { "host": "site-broker.dallas.example", "port": 8883, "clientId": "uns-bridge-gw-01" } } ]}The messaging section (top level) is the device bus; the siteBroker here is the site bus. If
exactly one entry declares a siteBroker you may name it anything — the bridge falls back to the sole broker
entry. But if two entries carry a siteBroker, one must be named "site" or startup fails as ambiguous.
Secure the site connection with TLS + per-device ACL
Section titled “Secure the site connection with TLS + per-device ACL”The site link is where device traffic leaves the device — secure it, and secure it at the broker, because
the relay carries no in-process guard (see explanation → security). Point
the siteBroker.credentials at your client cert/key/CA and use the TLS port:
"siteBroker": { "host": "site-broker.dallas.example", "port": 8883, "clientId": "uns-bridge-gw-01", "credentials": { "certPath": "/certs/client.pem", "keyPath": "/certs/client.key", "caPath": "/certs/ca.pem" }}Then give the broker a per-device ACL so each bridge may publish only under its own ecv1/{device}/# subtree
and read only its own cmd. The deploy/site-broker/ recipe set ships a ready acl.conf, a
gen-tls-certs.sh, and matching server/client certs — start from there; the bridge and the site broker
deploy as a pair.
Understand the automatic site Last-Will
Section titled “Understand the automatic site Last-Will”An abrupt device/bridge death is invisible unless the broker announces it. The bridge automatically registers
a Last-Will on the site connection that publishes a protobuf EdgeCommons state envelope with
status:"UNREACHABLE" on the bridge’s own state topic, so a site console watching
ecv1/+/+/+/state sees the device go dark immediately.
The topic is derived from the resolved runtime identity: ecv1/{device}/uns-bridge/main/state. Do not add an
lwt object under component.instances[site]; the bridge rejects it because this is a private bridge-console
contract and a misconfigured topic would break console reachability.
Turn a class on or off
Section titled “Turn a class on or off”Every uplinkable class can be switched off; a disabled class’s messages are dropped and counted. Set
enabled under uplink.classes.<class>:
"uplink": { "classes": { "log": { "enabled": false }, // don't ship log tailing across the WAN "app": { "enabled": true } // DO relay the free-form app class (off by default)} }appis opt-in — off by default, and off means its filter is never even subscribed.logis on by the code default, but the bundled sample config sets it off — set"log": { "enabled": false }unless you really want log tailing to cross the site link.- The six consumer classes (
state,cfg,evt,metric,data) are on by default. cmdis not on this list — it is never uplinked and has no policy knob.
Cap a class’s rate
Section titled “Cap a class’s rate”High-volume classes (data, metric) can flood the site link. Cap each with a token bucket —
maxRatePerSec is the sustained rate, burst the bucket capacity (default 2×rate; the bucket starts
full):
"uplink": { "classes": { "data": { "maxRatePerSec": 200, "burst": 400 }, "metric": { "maxRatePerSec": 50 }} }| You want… | Set |
|---|---|
| A steady ceiling on a class | maxRatePerSec |
| A larger momentary burst allowance | burst (defaults to 2×maxRatePerSec) |
| No cap | omit both (the default — unlimited) |
| Only an initial burst, then nothing | maxRatePerSec: 0 + a burst (prefer enabled: false to switch a class off) |
Over-cap traffic drops — it never queues. The live UNS path is deliberately not durable; if you need every sample, that is the streaming subsystem’s job, not the bridge’s.
Keep alarms across a WAN outage
Section titled “Keep alarms across a WAN outage”Events are the one class you don’t want to lose during a blip. The evt disconnect replay buffer is on by
default (1000 messages, drop-oldest) — you only touch it to resize or disable it:
"uplink": { "classes": { "evt": { "bufferWhileDisconnected": { "maxMessages": 5000 } } // bigger buffer for a chatty site} }bufferWhileDisconnected: { "enabled": false }(or"maxMessages": 0) turns it off — thenevtdrops on disconnect like every other class.bufferWhileDisconnectedon any class other thanevtis ignored with a warning (the scope is evt-only).- On reconnect the buffer replays in order, then clears; overflow while down drops the oldest.
The buffer is memory-only — it survives a WAN blip, not a bridge restart.
Proxy site→device request/reply (and the paired knob)
Section titled “Proxy site→device request/reply (and the paired knob)”Request/reply across the bridge works automatically — a site-side request() gets its reply_to rewritten
down and the reply carried back up (see explanation).
The one thing you must keep aligned is the TTL paired knob:
"messaging": { "requestTimeoutSeconds": 30 }, // top level: the requester's deadline"component": { "instances": [ { "id": "site", "reply": { "ttlSecs": 60, "maxPending": 1024 } // MUST be >= 2x requestTimeoutSeconds} ] }reply.ttlSecsdefaults to 60 s = 2× the framework’s 30 s request-deadline default. If you raisemessaging.requestTimeoutSeconds, raisereply.ttlSecsin step, or the bridge may tear down a reply path before the requester’s own deadline settles it.reply.maxPending(default 1024) bounds in-flight requests; overflow evicts the oldest (a stuck responder must not starve fresh commands).
Size the per-class subscription queues
Section titled “Size the per-class subscription queues”The relay pumps are serial per class; each class’s provider subscription has a bounded queue, and overflow
drops at the provider. data gets a deep queue, everything else a shallow one:
"queue": { "data": 512, "default": 64 }Raise queue.data if a bursty data producer outpaces the uplink momentarily; raise queue.default if a
downlink cmd burst (or another class) needs more slack.
Guard against relay loops in a complex topology
Section titled “Guard against relay loops in a complex topology”Loop protection is automatic (the hop tag), but two knobs and one rule matter:
maxHops(default 4) caps how many distinct bridges a message may traverse before it’s dropped. Lower it in a shallow topology to fail fast; raise it only if you genuinely chain more than four bridges.- Exactly one bridge per device bus. Two bridges on the same bus pair double-deliver everything — the
hop tag prevents loops, not duplication. On Kubernetes that means
replicas: 1+strategy: Recreate. - Inside a Kubernetes cluster there is no bridge — the in-cluster broker is the aggregation point; a bridge only appears at a boundary.
Deploy to a platform
Section titled “Deploy to a platform”HOST: run the binary against a config file naming the device broker (messaging) and the site broker
(component.instances[site]):
uns-bridge --platform HOST --transport MQTT ./config.json -c FILE ./config.json --thing gw-01# -t falls back to $EDGECOMMONS_THING_NAMEKubernetes (boundary bridge): deploy the same binary as a replicas: 1 / strategy: Recreate
Deployment between the on-prem device bus and the in-cluster broker. See
deploy/site-broker/k8s/boundary-bridge.example.yaml for a worked manifest, and deploy/site-broker/k8s/
for the in-cluster aggregation broker it bridges onto.
Greengrass: the site broker’s Greengrass recipe is in deploy/site-broker/greengrass/. On a
Greengrass core the bridge’s device bus is the Nucleus IPC pubsub and the site half is MQTT; the relay shares
the runtime’s IPC provider. Build with the greengrass cargo feature (Linux C-FFI). The bridge’s own
Greengrass packaging (recipe.yaml, gdk-config.json) runs it with --platform GREENGRASS --transport IPC -c GG_CONFIG -t {iot:thingName} and IPC pubsub accessControl for the local UNS topics.
Run the dual-broker end-to-end test
Section titled “Run the dual-broker end-to-end test”The bridge-level proof against two real brokers — one command, needs only Docker + cargo:
bash tests/e2e/run.shIt boots a throwaway two-EMQX rig on dedicated ports, runs the real bridge binary against the bundled sample
config, and asserts (with per-assertion PASS/FAIL): uplink of state, evt, and data protobuf messages
arrive topic-verbatim with hop tags, including a data message whose opaque body bytes are preserved;
downlink of an own-device cmd; the drop of a foreign-device cmd; a reply round-trip; the loop-drop of an
own-echo; and the bridge’s own heartbeat state + relay-counter metrics appearing and riding the relay. The
test is
#[ignore]d and gated on UNS_BRIDGE_E2E=1, so a plain cargo test never touches it.
Observe the bridge’s health and throughput
Section titled “Observe the bridge’s health and throughput”- Metrics — every 30 s the bridge publishes relay counters as
metrics onecv1/{device}/uns-bridge/main/metric/<name>(with the sample’smetricEmission.target: messaging). Watchrelay_uplinked/relay_downlinkedfor throughput,relay_dropped_*for policy drops,relay_loop_droppedfor loop protection firing,relay_reply_relayed/relay_reply_expiredfor the reply proxy,relay_evt_buffered/relay_evt_replayedfor disconnect handling, and the gaugesrelay_pending_repliesandsite_connectedfor live state. Full table: reference/metrics.md. - State keepalive — the bridge’s own
stateonecv1/{device}/uns-bridge/main/stateevery ~5 s; the site LWT flips it toUNREACHABLEon an abrupt death. - Logs — startup logs the resolved identity, hop id, filter counts, and the active uplink policy (disabled classes, rate-capped classes, evt buffer size); shutdown logs a one-line tally of every counter.