Skip to content

Unified Namespace (UNS)

Every EdgeCommons component addresses the bus through one Unified Namespace (UNS): a single topic grammar, a closed set of message classes, and a config-driven identity stamped on every envelope. A consumer — a monitoring console, a site historian, an MES bridge — subscribes a small, uniform set of wildcards and needs zero per-component knowledge; every message it receives is self-identifying through its top-level identity element, never through topic parsing.

This page is the concept reference the other guides build on; the exact API is in the UNS API reference.

ecv1 / {device} / {component} / [ {instance} / ] {class} [ / {channel...} ]
Segment What it is
ecv1 The fused root literal: EdgeCommons namespace + grammar major version. Grammar bumps are ecv1 → ecv2; payload versions stay in header.version.
{device} The physical node — the resolved thing name (the last level of the hierarchy).
{component} The component’s sanitized short name (the segment after the last .).
{instance} Optional. The logical instance the message pertains to; present only for instance-scoped messages. Component-scoped messages omit it entirely.
{class} One of the eight closed message classes.
{channel...} Class-specific sub-path (a metric name, a signal path, a command verb).

Two deliberate properties:

  • The topic addresses the endpoint, not the hierarchy. The enterprise’s logical hierarchy (site/area/line/…) lives in the message’s identity element, not the topic — so topic depth is constant regardless of hierarchy depth, and broker-side scoping is by device (ecv1/{device}/#).
  • Depth-guarded by construction. The whole grammar stays within AWS IoT Core’s 7-slash topic limit; the uns() builder rejects an over-deep channel at build time instead of letting IoT Core silently drop the message.

A multi-site broker deployment can opt in to a site segment after the root with the top-level topic.includeRoot: true config: ecv1/{site}/{device}/... (the site is the first hierarchy value; the setting is a no-op with a single-level hierarchy).

hierarchy and identity are read from the merged effective config. In hierarchical-config deployments, inherited hierarchy layers can define non-device identity values such as site and line once while component layers supply only component-specific settings. The runtime device still comes from the thing name, and identity.device is rejected.

Class Carries Publisher Example tail
state Lifecycle + health + keepalive — the liveness beacon library — reserved state
metric Operational metrics, incl. the built-in sys system measures library — reserved metric/signals-ingested
cfg Effective-config snapshot (redacted), at startup + on change library — reserved cfg
log Structured log records for remote tail (class reserved; no built-in publisher) library — reserved log/error
data The telemetry data plane — signal updates, processed outputs data() facade data/press12/temperature
evt Events and alarms events() facade evt/critical/overtemp
cmd Commands — request/reply, addressed to the recipient’s inbox any cmd/reload-config
app Arbitrary application pub/sub between components app() facade app/order/received

The first four are reserved platform classes: they are library-owned, and a raw messaging().publish(...) to a reserved-class topic is rejected (ReservedTopicException / ReservedTopicError / EdgeCommonsError::ReservedTopic), so no component can forge another’s health or config. state and cfg are leaf classes (no channel); every other class requires at least one channel token. cmd is the only class whose identity path names the recipient; everything else names the publisher.

A consumer’s complete subscription set is two uniform wildcards per class — a component-scope filter and an instance-scope filter — with no per-component branches:

ecv1/+/+/state ecv1/+/+/cfg ecv1/+/+/evt/#
ecv1/+/+/metric/# ecv1/+/+/data/# ecv1/+/+/log/#
ecv1/+/+/+/state ecv1/+/+/+/cfg ecv1/+/+/+/evt/#
ecv1/+/+/+/metric/# ecv1/+/+/+/data/# ecv1/+/+/+/log/#

The ecv1/+/+/{class} filters catch component-scoped messages (no instance token); the ecv1/+/+/+/{class} filters catch instance-scoped messages. Together they cover every component’s liveness, config, events, metrics, telemetry, and log tail on the bus. Group and display by reading each message’s identity — never by parsing the topic. The uns() API builds these filters for you (gg.uns().filter(class, scope)), scoped site-wide or narrowed to a device/component/instance.

To watch a device’s components come alive from any MQTT client:

Terminal window
mosquitto_sub -h localhost -p 1883 -t 'ecv1/+/+/state' -t 'ecv1/+/+/+/state' -v

Every envelope built through a config-bound builder carries a top-level identity element — a sibling of header/tags/body:

{
"header": { "name": "state", "version": "1.0", "timestamp": "", "uuid": "", "correlation_id": "" },
"identity": {
"hier": [
{ "level": "site", "value": "dallas" },
{ "level": "factory", "value": "finishing" },
{ "level": "zone", "value": "zone-3" },
{ "level": "device", "value": "gw-01" }
],
"path": "dallas/finishing/zone-3/gw-01",
"component": "opcua-adapter",
"instance": "kep1"
},
"tags": { "app": "line-ctl" },
"body": { }
}
  • hier is the ordered enterprise hierarchy; its last entry is always the device (the resolved thing name). There is no standalone device wire field — the in-memory MessageIdentity exposes a computed device accessor.
  • path is the precomputed /-join of the hierarchy values, so consumers group and display without re-joining.
  • component / instance are the addressing suffix — the same tokens as in the topic.
  • tags carries business context only (app, org, cost center). The old tags.thing key is removed — identity is where the device lives now.

The hierarchy is declared once in config — an ordered, freely-named list of levels whose deepest level is the physical node — and the location values above the device come from the top-level identity config block:

{
"hierarchy": { "levels": ["site", "factory", "zone", "device"] },
"identity": { "site": "dallas", "factory": "finishing", "zone": "zone-3" }
}

The last level’s value is always the resolved thing name (from -t, the Kubernetes Downward API, or AWS_IOT_THING_NAME) — putting a device key in identity is a startup error. With no hierarchy/identity config at all, the zero-config default is levels: ["device"] and the UNS works out of the box as ecv1/{thing}/{component}/main/{class}. Identity resolves once at startup (fail-fast if values don’t cover the declared levels), and the library stamps it on every message. See the Configuration guide.

A component commonly serves many instances (an OPC UA adapter with kep1, plc-2, …). The {instance} topic segment and the identity.instance field are resolved per message through an instance-scoped handle — gg.instance("kep1") — whose uns() mints topics and whose message builder stamps envelopes with that token. Component-level messages carry no instance token (component scope); a token is present only when you address a specific instance. See gg.instance().

Building and validating topics — gg.uns()

Section titled “Building and validating topics — gg.uns()”

Never concatenate UNS topics by hand. gg.uns() is the topic builder + validator bound to your component’s resolved identity — it enforces the token charset, the class rules, and the ≤ 7-slash IoT Core depth budget at build time:

String t = gg.getUns().topic(UnsClass.APP, "order/received");
// -> ecv1/gw-01/my-component/app/order/received
gg.getMessaging().publish(t, msg);
// address a peer's command inbox from a received message's identity:
String cmd = gg.getUns().topicFor(peer.getIdentity(), UnsClass.CMD, "reload-config");
// build a consumer filter:
String f = gg.getUns().filter(UnsClass.STATE, UnsScope.all()); // ecv1/+/+/+/state

gg.messaging() takes literal topics — it is fully usable for external, non-UNS topics (bridging a legacy MQTT system, the cloudwatch/metric/put contract). The only restriction is the reserved-class guard above. See the Messaging guide.

For the three app-usable classes, don’t hand-build the topic and body with uns() + messaging() — use the matching class-publish facade: data(), events(), or app(). They mint the same UNS topic, but also construct and validate the body, so every consumer sees a consistent shape instead of each publisher re-inventing one (see the full API reference for every method signature in all four languages).

// data(): publish a signal reading — quality defaults to GOOD, serverTs to now
gg.instance("kep1").data().signal("press12/temperature")
.name("Line1 Temp")
.addSample(21.5)
.publish();
// events(): a one-shot informational event, and a stateful alarm pair on the same channel
gg.getEvents().emit("door-open", "front door opened");
gg.getEvents().raiseAlarm("connection-lost", "Modbus link down", null);
gg.getEvents().clearAlarm("connection-lost", null);
// app(): free-form inter-component pub/sub
gg.getApp().publish("OrderReceived", "order/received", body);

Three things worth knowing before you reach for them:

  • The quality default is a guarantee, not a convenience. data() never lets a sample reach the bus without a quality — if your source has no notion of one, the facade fills "GOOD" and marks qualityRaw: "unspecified", so a consumer can always distinguish a synthesized verdict from a device-reported one. The only thing data() ever rejects outright is a missing signal.id.
  • The evt channel is derived, not chosen. events() builds evt/{severity}/{type} from the body’s own severity + type fields — you can’t publish a body whose topic disagrees with it. raiseAlarm/clearAlarm default severity to critical so the raise and its later clear land on the same channel.
  • Channel routing is uniform across all three: a per-call override, then a config-driven publish.channel default (instance, then global), then local. Only data() accepts a stream:<name> route (for bulk telemetry via streaming) — falling back to a local publish if no stream is configured rather than dropping the message; events()/app() are local/northbound only, since alarms and app messages are low-rate control-plane traffic.

Raw uns() + messaging() publishing to data/evt/app is fully supported — these classes are non-reserved, and the facade is the recommended, not the only, path. Use the raw form for a body shape the facade genuinely shouldn’t own (data() even offers a publishBody()/publish_body() escape hatch for exactly that — topic + identity handled, body untouched).

  • A component’s command inbox is ecv1/{device}/{component}/cmd/{verb} at component scope, or ecv1/{device}/{component}/{instance}/cmd/{verb} for a command addressed to a specific instance — the ecv1/{device}/{me}/cmd/# and ecv1/{device}/{me}/+/cmd/# subscriptions together receive only your own commands, no body-filtering. Verbs are lowercase-hyphenated, optionally family-namespaced (sb/status).
  • Config fetch (the CONFIG_COMPONENT source): a request to ecv1/{device}/config/cmd/get-configuration, where config is a reserved-by-convention logical component name (the config server is its sole subscriber). Config push arrives on the target component’s own inbox: ecv1/{device}/{component}/cmd/set-config.
  • Reserved tokens: the root ecv1; the logical component config; the pseudo-component _bcast (device-wide command broadcast, ecv1/{device}/_bcast/cmd/{verb}); and the whole _-prefix for system pseudo-components. Don’t name components or instances with these.
  • Reply topics are non-UNS by design: the ephemeral edgecommons/reply-... request/reply topics are not ecv1/-rooted and pass the guard untouched.

In all four languages (byte-identical topics, structurally identical envelopes, pinned by the cross-language uns-test-vectors/ conformance suite):

  • The grammar, the eight classes, the reserved-class publish guard, and gg.uns() / gg.instance(id).
  • The top-level identity envelope element + the hierarchy/identity/topic config blocks.
  • The library-owned publishers: the state keepalive (heartbeat), UNS metric topics, and the cfg effective-config announcements.
  • The request() internal deadline, MQTT Last Will, and the CONFIG_COMPONENT command remap.
  • The _bcast republish-state/republish-cfg broadcast listener every component runs.
  • The data()/events()/app() class-publish facades — see Publishing telemetry and events above and the full API reference.
  • The commands() facade (the component command inbox + ping/reload-config/ get-configuration) in all four languages (pinned by uns-test-vectors/commands.json).