Skip to content

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.


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.


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.


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)
} }
  • app is opt-in — off by default, and off means its filter is never even subscribed.
  • log is 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.
  • cmd is not on this list — it is never uplinked and has no policy knob.

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.


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 — then evt drops on disconnect like every other class.
  • bufferWhileDisconnected on any class other than evt is 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.ttlSecs defaults to 60 s = 2× the framework’s 30 s request-deadline default. If you raise messaging.requestTimeoutSeconds, raise reply.ttlSecs in 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).

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.

HOST: run the binary against a config file naming the device broker (messaging) and the site broker (component.instances[site]):

Terminal window
uns-bridge --platform HOST --transport MQTT ./config.json -c FILE ./config.json --thing gw-01
# -t falls back to $EDGECOMMONS_THING_NAME

Kubernetes (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.


The bridge-level proof against two real brokers — one command, needs only Docker + cargo:

Terminal window
bash tests/e2e/run.sh

It 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 on ecv1/{device}/uns-bridge/main/metric/<name> (with the sample’s metricEmission.target: messaging). Watch relay_uplinked / relay_downlinked for throughput, relay_dropped_* for policy drops, relay_loop_dropped for loop protection firing, relay_reply_relayed / relay_reply_expired for the reply proxy, relay_evt_buffered / relay_evt_replayed for disconnect handling, and the gauges relay_pending_replies and site_connected for live state. Full table: reference/metrics.md.
  • State keepalive — the bridge’s own state on ecv1/{device}/uns-bridge/main/state every ~5 s; the site LWT flips it to UNREACHABLE on 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.