Skip to content

Sample Configurations

Complete, ready-to-adapt configurations for the OPC UA adapter (com.mbreissi.edgecommons.OpcUaAdapter), one per realistic deployment scenario. Each sample is a valid config document; the prose after it explains what every option does and how it changes runtime behavior — data rate, latency, addressing, security, and reconnect/health.

For the exhaustive option list see reference/configuration.md; for the topic/message contract see reference/messaging-interface.md; for the reasoning behind the timing and security models see explanation.md; and for task recipes see how-to-guides.md.

How config reaches the adapter. The adapter reads one JSON document from the -c/--config source, which defaults by platform: HOSTFILE, GREENGRASSGG_CONFIG (the deployment), KUBERNETESCONFIGMAP (a mounted directory, hot-reloaded). Adapter settings live under component; the sibling sections (hierarchy, identity, tags, messaging, logging, heartbeat, metricEmission, credentials, streaming) are standard edgecommons sections validated against the canonical config schema.

UNS addressing (read once). This adapter is on the edgecommons Unified Namespace. Signal updates ride the UNS data class — ecv1/{device}/{component}/{instance}/data/{signalPath} — with topics minted by the library, not templated in config. The site hierarchy is declared once at the top level (hierarchy + identity) and carried in every message’s identity element. Reads/writes/queries are the cmd/sb/* verbs. Each config below uses these forms.

This page is organized as:

  • Matching and topic resolution — the two mechanisms every example below relies on (which nodes get subscribed, and which topic each signal publishes to). Read this first.
  • §1–§3, §6–§7 — one config per platform/security shape (HOST dev, Greengrass IPC, secured server, Kubernetes, multiple servers).
  • §4 — a large address space pruned to a precise, rate-controlled subset with broad include + exclude, multiple areas, and grouped timing.
  • §5 — northbound publishing: the local data plane, heartbeat/health to AWS IoT Core, and high-volume telemetry via gg.streams() streaming.

Matching and topic resolution (read this first)

Section titled “Matching and topic resolution (read this first)”

Two pieces of behavior drive every example: how a subscription selects address-space nodes (include/exclude), and how a selected signal’s value is addressed on the bus (the publish topic). Both are precise and easy to get subtly wrong, so they are spelled out here once.

Include vs exclude: which nodes get subscribed

Section titled “Include vs exclude: which nodes get subscribed”

At connect time the adapter browses the whole address space (every Variable node) and tests each node against the subscription’s matchers. A node is monitored iff it matches at least one include matcher and no exclude matcher. There is no ordering between matchers — it is a set union of include minus a set union of exclude; the first include that matches supplies the node’s timing/topic.

Each matcher is namespace + regex. The two halves are evaluated like this:

Step include matcher exclude matcher
1. Namespace The matcher’s namespaceUri is resolved to the server’s current namespace index (or the literal namespace index is used). The node’s namespace index must equal it, or the matcher is skipped for that node. An unresolvable namespaceUri resolves to index -1 → the matcher matches nothing (logged as a warning). same
2. match regex Tested against the node’s identifier, browse name, and display name — a match on any one selects the node. Tested against the identifier only. A regex written against a display name does nothing on exclude.

This asymmetry is deliberate: you usually select signals by their human-readable names but exclude specific ones by their stable id. See explanation.md.

The whole-string-match rule (most common mistake). match is a Java regex evaluated with String.matches(), which requires the entire string to match — it is implicitly anchored at both ends. Sine.* matches Sine1 (whole string), but a substring pattern like \.Diagnostics\. matches nothing, because no real identifier equals .Diagnostics.. To match “any id containing .Diagnostics.”, write .*\.Diagnostics\..*. Anchors (^, $) are allowed and harmless but redundant. Escape literal dots as \\. in JSON.

Three idioms follow from this:

  • Match a whole namespace (the broadest include) — omit match entirely (it defaults to .*): { "namespaceUri": "Kepware Server" } subscribes to every variable node in that namespace.
  • Match a subtree/prefix — anchor and trail with .*: ^Channel1\\.Device1\\..*.
  • Exclude a subtree anywhere in the id — surround with .*: .*\\._Statistics\\..*.

Where do namespaceUri values come from? A namespace URI is whatever the server advertises — it is server- and configuration-specific. For KEPServerEX it is commonly Kepware Server (used in these examples and the adapter’s validation/ configs); the OPC UA Foundation base namespace is http://opcfoundation.org/UA/ (index 0). Discover the exact strings your server uses with the sb/signals control query — it echoes the resolved namespace index and its namespaceUri for every subscribed signal — or by reading the server’s NamespaceArray. Always substitute your server’s values.

Every SouthboundSignalUpdate is published to a UNS data topic the library mints — you do not template it. The grammar is:

ecv1/{device}/{component}/{instance}/data/{signalPath}
Segment Resolves to Source
{device} the resolved thing name = the last hierarchy.levels entry -t/--thing / platform
{component} the component UNS token → opcua-adapter binary
{instance} the instance id (e.g. kep1) instances[].id
{signalPath} the node’s bare identifier sanitized to one channel token (/ \ + # and control chars → _) runtime

The site hierarchy is not in the topic — it rides the top-level envelope identity element, declared once via hierarchy + identity:

"hierarchy": { "levels": ["site", "shop", "line", "device"] },
"identity": { "site": "plant1", "shop": "assembly", "line": "5" }

Worked example. Given -t edge-gw-01, the hierarchy/identity above, and instance id = "kep1":

Signal (node id) Published data topic identity.path on the envelope
ns=2;s=Channel1.Device1.Flow ecv1/edge-gw-01/opcua-adapter/kep1/data/Channel1.Device1.Flow plant1/assembly/5/edge-gw-01
ns=2;s=Line/5/Alarm ecv1/edge-gw-01/opcua-adapter/kep1/data/Line_5_Alarm (/ sanitized to _) plant1/assembly/5/edge-gw-01

The stable signal.id in the body (the full ns=2;s=… form) is what consumers key on; the {signalPath} is only the routing address. A fleet consumer subscribes one wildcard, ecv1/+/+/+/data/#. To route topics under a site root on a multi-site broker, set topic.includeRoot: true (needs ≥ 2 hierarchy levels). Per-signal topic overrides are not supported.


The smallest config that connects to a plain (unsecured) OPC UA server and republishes a set of signals to a local MQTT broker. This is the shape you use against a simulator or a lab KEPServerEX while developing.

The dual-MQTT transport needs broker details. You can supply them inline under messaging (shown here) or as a separate file passed positionally as --transport MQTT ./messaging.json.

config.json
{
"hierarchy": { "levels": ["site", "shop", "line", "device"] },
"identity": { "site": "lab", "shop": "s1", "line": "l1" },
"messaging": {
"local": { "type": "mqtt", "host": "localhost", "port": 1883, "clientId": "opcua-adapter" }
},
"logging": { "level": "INFO" },
"metricEmission": {
"target": "messaging",
"targetConfig": { "destination": "local" }
},
"component": {
"global": {
"defaults": { "publishIntervalMs": 1000, "samplingRateMs": 500, "queueSize": 100 }
},
"instances": [
{
"id": "sim1",
"connection": { "endpoint": "opc.tcp://localhost:4840/", "securityPolicy": "None" },
"publish": { "batchMs": 1000 },
"writes": { "allow": [] },
"subscriptions": [
{
"id": "all",
"include": [ { "namespaceUri": "urn:edgecommons:sim", "match": "Sine.*" } ]
}
]
}
]
}
}

Run it:

Terminal window
java -jar target/opcua-adapter-1.0.0.jar --platform HOST --transport MQTT \
-c FILE ./config.json -t my-thing
# (or, with a separate broker file: --transport MQTT ./messaging.json)
Option Effect on runtime behavior
hierarchy / identity The UNS enterprise hierarchy (last level = the device = thing name) and the values for every other level. Stamped onto every message’s top-level identity (path = lab/s1/l1/<thing>) and used for fleet-wide addressing. Level names must match ^[A-Za-z0-9_-]+$. Changing them changes every consumer’s identity view (not the data topic, which is device/component/instance-scoped).
messaging.local The local MQTT broker the adapter publishes to and listens on. host/port point at the broker; clientId is the MQTT client identity (keep it unique per process or the broker drops the older session). On HOST this is one half of the dual-MQTT transport; add an iotCore block (see §3/§5) to also connect to AWS IoT Core. Without a reachable broker the adapter cannot publish or accept commands.
metricEmission.target Where southbound_health plus the OPC UA operational metrics (OpcUaCommand, OpcUaSubscription, OpcUaBrowse, OpcUaConnection) go (log / messaging / cloudwatch / cloudwatchcomponent / prometheus). With messaging, metrics are auto-published on the UNS metric class (ecv1/{device}/{component}/metric/{metricName}) — the topic is minted, so set targetConfig.destination (local/iotcore), not a targetConfig.topic (the schema rejects it). With log they only appear in logs. Observability only.
component.global.defaults Fallback timing for every instance/signal that does not set its own. publishIntervalMs (server→adapter delivery cadence), samplingRateMs (how often the server samples the value; 0 = as fast as the server allows), queueSize (server-side per-signal buffer). See the precedence table at the end.
instances[].id Stable, unique instance id (required). Appears as the UNS {instance} topic segment and as device.instance in messages. Each instance is one OPC UA server with its own connection thread, so renaming it changes topic routing and message identity.
instances[].adapter The southbound adapter type that should service this instance. This is a single-protocol binary: it treats every listed instance as an OPC UA server (it does not filter on this field), and the published device.adapter is always "opcua". Set it to "opcua" for clarity and forward-compatibility with the shared southbound convention (where one config can describe instances for several adapter binaries, e.g. an OPC UA and a Modbus adapter).
connection.endpoint The OPC UA server URL (opc.tcp://host:port/). The adapter connects here on a dedicated thread and retries on failure, so a server that is slow to boot delays only this instance, not the component. Empty by default (you must set it).
connection.securityPolicy: "None" Unencrypted, anonymous channel — no certificates required. Fine for a trusted LAN/dev. For a secured server see §3.
publish.batchMs Client-side coalescing window. 1000 here means the adapter buffers a signal’s samples and emits one message per signal per second (each may carry several samples), reducing message count. Set 0 to emit one message the instant each sample arrives (lowest latency, most messages). Defaults to the resolved instance publishIntervalMs when omitted. (The topic is UNS-minted — ecv1/{device}/{component}/{instance}/data/{signalPath}; there is no publish.topic.)
writes.allow The stable signal.ids the sb/write verb may write (or "*" = allow all). Here [] accepts no writes (secure-by-default). Reads are always available via the sb/read verb; there is no separate read topic.
subscriptions[].include The signal matchers to subscribe to. With only { "match": "Sine.*" } and no timing overrides, these signals inherit global.defaults. (Sine.* whole-string-matches identifiers/names like Sine1, Sine2.)

2. Greengrass v2 deployment (IPC) — on-device shape

Section titled “2. Greengrass v2 deployment (IPC) — on-device shape”

On --platform GREENGRASS there is no messaging broker block and no config file: messaging uses Greengrass IPC (--transport IPC, the default for this platform) and the config arrives from the deployment’s ComponentConfiguration. The sample below is the ComponentConfig block exactly as it sits in recipe.yaml (YAML, because the recipe is YAML); a cloud deployment overrides the same keys via aws greengrassv2.

# recipe.yaml — ComponentConfiguration.DefaultConfiguration.ComponentConfig
ComponentConfig:
logging:
level: "INFO"
heartbeat:
intervalSecs: 5
destination: "local" # state keepalive over IPC on GREENGRASS
measures: { cpu: true, memory: true, disk: false, fds: true, files: true }
hierarchy: { levels: ["site", "shop", "line", "device"] }
identity: { site: "plant1", shop: "assembly", line: "5" }
metricEmission:
target: "log"
targetConfig:
logFileName: "/greengrass/v2/logs/{ComponentFullName}.metric.log"
component:
global:
defaults: { publishIntervalMs: 1000, samplingRateMs: 500, queueSize: 100 }
instances:
- id: "kep1"
adapter: "opcua"
connection: { endpoint: "opc.tcp://192.168.1.50:49320/", securityPolicy: "None" }
publish: { batchMs: 1000 }
writes: { allow: [ "ns=2;s=Channel1.Device1.Setpoint" ] }
subscriptions:
- id: "process"
include:
- { namespaceUri: "Kepware Server", match: "^Channel1\\.Device1\\..*" }

Run on-device (config comes from the deployment, so no -c):

Terminal window
java -jar opcua-adapter-1.0.0.jar --platform GREENGRASS -t my-thing
# package/publish: gdk component build && gdk component publish
Difference from HOST Effect on runtime behavior
No messaging section; transport is IPC The adapter publishes/subscribes through the Nucleus’s local IPC pub/sub (and the IoT Core mqttproxy) instead of a TCP MQTT broker. The recipe’s accessControl grants the IPC and mqttproxy topics; the message envelope on the wire is identical to HOST.
heartbeat.destination: "local" Routes the UNS state keepalive over the local/IPC transport (on GREENGRASS that is IPC). On HOST it goes to the local MQTT broker. Use "iotcore" to push it to AWS IoT Core instead (see §5). Heartbeat config is {enabled, intervalSecs, measures, destination}.
metricEmission.target: "log" with a /greengrass/v2/logs/... path Health and OPC UA operational metrics are written to the Nucleus-managed component log directory rather than published — convenient because Greengrass already rotates and ships those logs.
connection / publish / subscriptions Identical semantics to HOST. The OPC UA side does not know or care about the platform; only the transport and config source change. You can lift an instance verbatim between HOST and GREENGRASS.
Cloud override A aws greengrassv2 create-deployment merge config patches these same keys per device/group, so per-site endpoint/tags/subscriptions differences are deployment data, not code.

The component reports ready as soon as its first instance is connected and subscribing — a signal orchestrators can gate on. Each instance reconnects independently with retry, so one unreachable server never blocks the others.


3. Secured OPC UA server (policy + certificates + user)

Section titled “3. Secured OPC UA server (policy + certificates + user)”

A mutually-authenticated, encrypted channel using Basic256Sha256 / SignAndEncrypt, plus a UserName identity token. This is the production shape against a hardened KEPServerEX. It also shows the dual-MQTT messaging block (local broker and AWS IoT Core) you would use on a HOST gateway.

config.json
{
"hierarchy": { "levels": ["site", "shop", "line", "device"] },
"identity": { "site": "plant1", "shop": "assembly", "line": "5" },
"messaging": {
"local": { "type": "mqtt", "host": "localhost", "port": 1883, "clientId": "opcua-adapter" },
"northbound": {
"endpoint": "xxxx-ats.iot.us-east-1.amazonaws.com",
"port": 8883,
"clientId": "opcua-adapter",
"credentials": { "certPath": "creds/device.cert.pem", "keyPath": "creds/device.key", "caPath": "creds/root-CA.crt" }
}
},
"credentials": { "vault": { "path": "/var/lib/opcua/{InstanceId}/vault" } },
"component": {
"global": { "defaults": { "publishIntervalMs": 1000, "samplingRateMs": 500, "queueSize": 50 } },
"instances": [
{
"id": "kep1",
"connection": {
"endpoint": "opc.tcp://192.168.1.50:49320",
"securityPolicy": "Basic256Sha256",
"messageMode": "SignAndEncrypt",
"clientCertificate": { "source": "vault", "secret": "opcua/kep1/appcert" },
"trust": {
"pkiDir": "/var/lib/opcua/{InstanceId}/pki",
"serverCertificate": { "source": "file", "path": "/etc/opcua/kep1-server.pem" }
},
"user": { "source": "vault", "secret": "opcua/kep1/login" }
},
"publish": { "batchMs": 1000 },
"writes": { "allow": [ "ns=2;s=Channel1.Device1.Setpoint" ] },
"subscriptions": [
{ "id": "process",
"include": [ { "namespaceUri": "Kepware Server", "match": "^Channel1\\.Device1\\..*" } ] }
]
}
]
}
}
Option Effect on runtime behavior
messaging.northbound Adds the cloud half of the dual-MQTT transport. The adapter connects to the local broker and to AWS IoT Core over mutual TLS on 8883. credentials.certPath/keyPath/caPath are the device’s X.509 identity for IoT Core; a missing/expired cert means the cloud leg fails to connect (the local leg is unaffected). Note: connecting both legs does not by itself send signal updates to the cloud — the adapter’s signal-update publish goes to the local bus only; see §5 for what actually traverses the IoT Core leg and how.
credentials Enables the encrypted local vault. Required whenever any source: "vault" reference is used (here for the client cert and the OPC UA user). Without it, those references cannot resolve and the secure connection fails to start. vault.path supports template variables.
Option Effect on runtime behavior
connection.securityPolicy Milo SecurityPolicy name: None, Basic256Sha256, Aes128_Sha256_RsaOaep, Aes256_Sha256_RsaPss, etc. Anything other than None requests an encrypted, signed channel: the adapter must present an application instance certificate the server trusts, and must trust the server’s. An unrecognized value falls back to None with a warning.
connection.messageMode MessageSecurityMode: None, Sign (integrity only), or SignAndEncrypt (integrity + confidentiality). Under any non-None policy a messageMode of None is auto-upgraded to SignAndEncrypt (with a warning).
connection.clientCertificate The adapter’s identity (cert + private key), selected by source — see the next table. Secure connections only. Default source is vault.
connection.trust.pkiDir Directory holding the trust store; the adapter creates trusted/, rejected/, issuers/ under it (template path; default pki/{InstanceId}). A server cert is accepted only if it (or its issuer) is in trusted/; an untrusted cert is written to rejected/ for an operator to inspect and promote. There is no auto-trust mode.
connection.trust.serverCertificate Optionally pins the expected server certificate up front so the first connection succeeds without a manual promote step: { "source": "file", "path": … } or { "source": "vault", "secret": …, "field": "caPem" }.
connection.user A UserName identity token, independent of channel security (KEPServerEX rejects anonymous logins even on its None endpoint). source: "vault" reads a BasicAuth ({username, password}) secret; the inline form { "username": …, "password": … } is for dev only — keep configs with inline passwords out of version control. The server applies that user’s authorization, so an under-privileged account can yield empty subscriptions even when the connection succeeds. Omit for anonymous.
connection.applicationUri (not shown) Leave unset; the adapter derives it from the client certificate’s SubjectAltName URI, which the server requires to match. Setting it wrong lets the channel open and then fails the session.

clientCertificate.source — the three identity sources

Section titled “clientCertificate.source — the three identity sources”
source Keys What it does
vault (default) secret Reads a TlsBundle ({certPem, keyPem, caPem}) from the credentials vault. Recommended — the private key is encrypted at rest. Requires a credentials section.
file certPath, keyPath Reads PEM certificate and private-key files (paths support template variables). The key sits on disk in clear.
pkcs11 modulePath, slotIndex (default 0), pin or pinEnv, keyLabel, certLabel Key + certificate live on a PKCS#11 token (HSM/TPM); the private key never leaves the hardware. Prefer pinEnv (env var name) over an inline pin.

Two spec requirements that trip everyone up: the client certificate’s key usage must include digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment (a self-signed cert also needs keyCertSign + the CA constraint), and its SAN URI must equal the applicationUri the adapter presents. See the security how-to and validation/gen_certs.py for a compliant cert generator.


4. Real-world selective subscription at scale

Section titled “4. Real-world selective subscription at scale”

A production OPC UA server is not a handful of sine waves — a single KEPServerEX can expose thousands of signals across multiple channels and devices, plus alarm folders and a noisy _System/_Statistics diagnostics tree. The realistic pattern is therefore: subscribe broadly, then prune, and split the result into groups that each get the cadence, queue depth, deadband, and topic they deserve.

This config bridges a packaging line on a KEPServerEX with two channels (Channel1 = line PLCs, Channel2 = the energy/utilities meters) into three subscription groups:

  • process — the fast process variables. A broad include of both channels, with exclude matchers that strip the per-device diagnostics, statistics, and _System housekeeping so they do not flood the data plane. Low latency, with a small absolute deadband to kill sensor jitter.
  • alarms — every alarm signal across all channels, captured at the server’s fastest sampling so no transition is missed. No deadband (you never want to deadband a discrete alarm). (These publish on the same UNS data class as the rest — consumers select alarms by the signal.id/address in the body, or you split them out with a separate evt-emitting consumer.)
  • diagnostics — the slow housekeeping you do want (server clock, comms counters) at a 10 s cadence so it costs almost nothing.
// component section only — drop into any of the platform shapes above
"component": {
"global": {
"defaults": { "publishIntervalMs": 1000, "samplingRateMs": 500, "queueSize": 100 }
},
"instances": [
{
"id": "kep1",
"connection": {
"endpoint": "opc.tcp://192.168.1.50:49320",
"securityPolicy": "None",
"user": { "source": "vault", "secret": "opcua/kep1/login" }
},
"defaults": { "publishIntervalMs": 1000, "samplingRateMs": 500, "queueSize": 100 },
"publish": { "batchMs": 1000 },
"writes": { "allow": [ "ns=2;s=Channel1.Device1.Setpoint" ] },
"subscriptions": [
{
"id": "process",
"publishIntervalMs": 200,
"include": [
{ "namespaceUri": "Kepware Server", "match": "^Channel1\\..*",
"samplingRateMs": 100, "queueSize": 50,
"deadband": { "type": "Absolute", "value": 0.5 } },
{ "namespaceUri": "Kepware Server", "match": "^Channel2\\..*",
"samplingRateMs": 1000, "queueSize": 10,
"deadband": { "type": "Percent", "value": 1.0 } }
],
"exclude": [
{ "namespaceUri": "Kepware Server", "match": ".*\\._System\\..*" },
{ "namespaceUri": "Kepware Server", "match": ".*\\._Statistics\\..*" },
{ "namespaceUri": "Kepware Server", "match": ".*\\.Diagnostics\\..*" },
{ "namespaceUri": "Kepware Server", "match": ".*\\.Alarms\\..*" }
]
},
{
"id": "alarms",
"publishIntervalMs": 250,
"include": [
{ "namespaceUri": "Kepware Server", "match": ".*\\.Alarms\\..*",
"samplingRateMs": 0, "queueSize": 100 }
]
},
{
"id": "diagnostics",
"publishIntervalMs": 10000,
"include": [
{ "namespaceUri": "Kepware Server", "match": "^_System\\..*",
"samplingRateMs": 5000, "queueSize": 2 },
{ "namespaceUri": "Kepware Server", "match": ".*\\._Statistics\\..*",
"samplingRateMs": 5000, "queueSize": 2 }
]
}
]
}
]
}

What this achieves, signal by signal:

Signal Group / matcher Outcome
Channel1.Device1.Flow process^Channel1\..* sampled every 100 ms, ±0.5-unit deadband, delivered every 200 ms, batched to ecv1/{device}/opcua-adapter/kep1/data/Channel1.Device1.Flow
Channel2.Meter1.kWh process^Channel2\..* sampled every 1 s, 1 %-of-range deadband (slow meter), delivered every 200 ms
Channel1.Device1.Diagnostics.Successful Reads exclude.*\.Diagnostics\..* dropped — matched by process include but pruned by exclude
Channel1.Device1.Alarms.HiHi alarms.*\.Alarms\..* captured at server-fastest sampling, published to ecv1/{device}/opcua-adapter/kep1/data/Channel1.Device1.Alarms.HiHi
_System._Time_Second diagnostics^_System\..* delivered every 10 s, tiny queue — near-zero cost

Why the process group also excludes .*\.Alarms\..*: a node can match more than one subscription. Excluding the alarm subtree from process keeps each alarm signal in exactly one group (alarms), so it is monitored once, with the alarm group’s timing and topic — not twice.

Subscription & signal-matcher options (complete)

Section titled “Subscription & signal-matcher options (complete)”
Option Scope Effect on runtime behavior
subscriptions[] per group Each entry is an independent OPC UA subscription with its own publishIntervalMs. Split signals by how fresh they must be so each group gets the cadence it needs without over-publishing the rest.
subscriptions[].id per group Identifier used in logs and the sb/signals control query. Defaults to a random UUID — set it so logs are readable.
subscriptions[].publishIntervalMs per group How often the server delivers that subscription’s accumulated samples to the adapter. 200 ms → low latency; 10000 ms → cheap housekeeping. Overrides the instance/global default for this subscription only.
include[] / exclude[] per group The matcher lists. A node is monitored iff it matches some include and no exclude — see matching semantics. exclude is optional.
namespaceUri per matcher Pins the OPC UA namespace by its URI (preferred). Resolved to the server’s current index at connect time and re-resolved on rebuild, so a server that renumbers after a restart is followed automatically. An unresolved URI skips the matcher (with a warning).
namespace per matcher Literal namespace index, used only when namespaceUri is absent (default 0). Indexes are volatile across servers/restarts — use only for servers you know to be stable.
match per matcher Java regex, whole-string match (see the match rule). On include it tests identifier/browse name/display name; on exclude the identifier only. Defaults to .* (whole namespace) when omitted.
topic per include matcher Ignored. All signals publish on the UNS data class; split streams downstream by signal.id/address, or with an evt-emitting consumer.
samplingRateMs per matcher How often the server samples the underlying value. 0 = as fast as the server allows; a larger value throttles a noisy source. A signal changing faster than this is only observed at sample boundaries — sampling sets the resolution. Inherits the instance/global default when omitted.
queueSize per matcher Server-side buffer holding samples taken between two publishes. On overflow the oldest samples are discarded. Keep queueSize ≥ ceil(publishIntervalMs / samplingRateMs) or you silently drop data — here the process group is 200/100 = 2, so 50 is generous. Inherits the instance/global default when omitted.
deadband per matcher A server-side filter applied before the queue: the server ignores changes smaller than the threshold, so jitter never enters the pipeline. type: "Absolute" suppresses changes below value engineering units; type: "Percent" expresses value as a fraction of the signal’s range (requires the server to advertise that range); type: "None" (default) disables it. Types are case-sensitive. Never deadband discrete/alarm signals.

The example above uses multiple areas (channels) within one namespace URI. Servers that expose genuinely separate namespaces — an aggregating gateway, or a vendor server that namespaces by area/protocol — are handled the same way: give each matcher its own namespaceUri.

"include": [
{ "namespaceUri": "urn:acme:packaging:line5", "match": "^Filler\\..*" },
{ "namespaceUri": "urn:acme:utilities:meters", "match": "^Meter\\..*", "samplingRateMs": 2000 },
{ "namespaceUri": "http://opcfoundation.org/UA/", "match": "CurrentTime" }
]

Each namespaceUri is resolved independently against the server’s namespace table; the regex still matches whole-string against id/browse/display name within that namespace.

A value passes through three stages, each with its own control (full discussion in explanation.md):

You want… Set
One current value per signal per second samplingRateMs and publishIntervalMs both ≈ 1000, small queueSize.
Every change, low latency small samplingRateMs (e.g. 100), small publishIntervalMs (e.g. 200), queueSize ≥ publish/sample.
Fewer, larger messages raise batchMs (the adapter coalesces a signal’s samples into one message).
One message per change batchMs: 0.
Drop sensor noise at the source add a deadband.

samplingRateMs sets resolution, publishIntervalMs sets latency, batchMs sets message granularity — and they compound: sampling at 100 ms, publishing at 200 ms, batching at 1 s yields messages each carrying ~10 samples arriving ~1 s after the values were read.


5. Northbound: from the local bus to the cloud

Section titled “5. Northbound: from the local bus to the cloud”

Everything in §1–§4 publishes to the local bus — Greengrass IPC on the GREENGRASS platform, the local MQTT broker on HOST/KUBERNETES. That is the adapter’s data plane: it sends every SouthboundSignalUpdate through the library data() facade, which mints the UNS data topic and targets the default provider channel (the local broker on HOST, IPC on Greengrass). On-box consumers — other components, a historian bridge, the rules engine — read those topics.

What the adapter sends to the cloud itself. The one northbound path the adapter wires directly is its own operational telemetry — the heartbeat, southbound_health, and the OPC UA operational metric families (OpcUaCommand, OpcUaSubscription, OpcUaBrowse, OpcUaConnection). The library can deliver them straight to AWS IoT Core alongside the local bus: on HOST/KUBERNETES the dual-MQTT provider holds the northbound mTLS session next to the local one. Opt in with messaging.northbound plus a heartbeat / metric target set to destination: "northbound":

{
"messaging": {
"local": { "type": "mqtt", "host": "localhost", "port": 1883, "clientId": "opcua-line1" },
"northbound": {
"endpoint": "a1b2c3d4e5f6g7-ats.iot.us-east-1.amazonaws.com",
"port": 8883,
"clientId": "opcua-line1",
"credentials": {
"certPath": "/greengrass/v2/thingCert.crt",
"keyPath": "/greengrass/v2/privKey.key",
"caPath": "/greengrass/v2/rootCA.pem"
}
}
},
// Heartbeat, health, and operational metrics go to IoT Core (low rate);
// signal data stays on the local bus.
// The state/metric topics are UNS-minted — only the destination is configurable, not the topic.
"heartbeat": {
"intervalSecs": 30,
"destination": "iotcore",
"measures": { "cpu": true, "memory": true }
},
"metricEmission": {
"target": "messaging",
"targetConfig": { "destination": "iotcore" }
}
}

On GREENGRASS the same destination: "northbound" routes through the Nucleus’ IoT Core connection, so messaging.northbound is not needed there.

Forwarding the signal data itself. The adapter does not push signal telemetry off-box — it publishes locally and stops there. Getting that data to the cloud is a deployment choice, handled by a separate consumer of the local topics:

  • Low-rate, actionable items (status, alarms, a few values someone acts on) — re-publish to AWS IoT Core, either with a Greengrass/IoT-Core topic bridge (rules engine) or a small on-box subscriber. IoT Core is priced per message, so keep this sparse.
  • High-rate, high-volume telemetry for analytics or a historian — the library’s streaming subsystem, gg.streams(), which batches and compresses into a durable on-disk buffer that drains to Kinesis or Kafka and survives WAN outages. See the Streaming guide and the streaming reference for its configuration; it is a edgecommons subsystem you run in a forwarding component, not an opcua-adapter option.

On --platform KUBERNETES the config source defaults to CONFIGMAP: the whole ConfigMap is mounted as a directory (typically /etc/edgecommons) so the adapter watches the kubelet ..data swap and hot-reloads in place on kubectl apply — no restart. The broker config lives in the same ConfigMap (in-cluster broker via Service DNS); identity comes from the Downward API, so usually no CLI args are needed.

k8s/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: opcua-adapter-config
labels: { app.kubernetes.io/name: opcua-adapter }
data:
config.json: |-
{
"messaging": {
"local": { "type": "mqtt", "host": "emqx.default.svc.cluster.local", "port": 1883, "clientId": "opcua-adapter" }
},
"logging": { "level": "INFO" },
"metricEmission": { "target": "prometheus" },
"hierarchy": { "levels": ["site", "device"] },
"identity": { "site": "plant1" },
"component": {
"global": { "defaults": { "publishIntervalMs": 1000, "samplingRateMs": 500, "queueSize": 100 } },
"instances": [
{
"id": "kep1",
"connection": { "endpoint": "opc.tcp://opcua.default.svc.cluster.local:4840/", "securityPolicy": "None" },
"publish": { "batchMs": 1000 },
"writes": { "allow": [ "ns=2;s=Channel1.Device1.Setpoint" ] },
"subscriptions": [
{ "id": "process",
"include": [ { "namespaceUri": "Kepware Server", "match": "^Channel1\\.Device1\\..*" } ] }
]
}
]
}
}
Option Effect on runtime behavior
Mounted as a whole-volume ConfigMap (never subPath) Preserves the ..data symlink swap so the CONFIGMAP source detects changes and hot-reloads subscriptions/timing without a pod restart. Mounting a single key via subPath breaks hot reload.
component section identical to other platforms The component name ({ComponentName}) is fixed by the adapter binary, so it does not appear in the config — only the component object (global/instances) does. The same component/connection/subscriptions shape works verbatim on HOST, GREENGRASS, and KUBERNETES.
messaging.local.host = a Service DNS name The in-cluster MQTT broker reached via Kubernetes Service DNS (emqx.default.svc.cluster.local). Point it at your broker Service.
metricEmission.target: "prometheus" Exposes southbound_health and the OPC UA operational metrics on the pod’s metrics port (default :9090) for Prometheus scraping instead of publishing them — the idiomatic k8s path.
No -t/--thing arg Identity resolves from the Downward API (EDGECOMMONS_THING_NAMEPOD_NAME). The Deployment also gates traffic on the HTTP health probes (/startupz, /livez, /readyz) the library serves on :8081.
connection / subscriptions Same OPC UA semantics as every other platform — only the config source (ConfigMap) and the metrics/identity wiring differ. Editing the ConfigMap and re-applying changes the live subscription set on the fly.

Deploy with kubectl apply -f k8s/; the companion deployment.yaml mounts this ConfigMap at /etc/edgecommons (read-only, whole volume), sets workingDir: /tmp (the Java MQTT client needs a writable cwd), and wires the health (8081) and metrics (9090) ports.


Because each instance is independent, a single deployment can bridge several OPC UA servers by listing several instances — they share only the process. Mix security and timing per server freely.

"component": {
"global": { "defaults": { "publishIntervalMs": 500, "samplingRateMs": 200, "queueSize": 20 } },
"instances": [
{
"id": "sim1",
"connection": { "endpoint": "opc.tcp://10.0.0.11:4840/", "securityPolicy": "None" },
"publish": { "batchMs": 0 },
"subscriptions": [
{ "id": "sines",
"include": [ { "namespaceUri": "urn:edgecommons:sim", "match": "Sine.*" } ] }
]
},
{
"id": "kep1",
"connection": {
"endpoint": "opc.tcp://10.0.0.50:49320",
"securityPolicy": "Basic256Sha256", "messageMode": "SignAndEncrypt",
"clientCertificate": { "source": "vault", "secret": "opcua/kep1/appcert" },
"user": { "source": "vault", "secret": "opcua/kep1/login" }
},
"publish": { "batchMs": 1000 },
"writes": { "allow": [ "ns=2;s=Plant.Line5.Setpoint" ] },
"subscriptions": [
{ "id": "live",
"include": [ { "namespaceUri": "Kepware Server", "match": "^Plant\\.Line5\\..*" } ] }
]
}
]
}
Behavior Detail
Independent connections Each instance connects on its own thread and reconnects with retry. A server that is down or slow to boot delays only its own instance; the others keep running.
Distinct topics {InstanceId} (here sim1 / kep1) keeps each server’s signal updates on separate topics, so consumers can subscribe per server.
Per-instance everything Security, user, timing, and batchMs are all per instance — sim1 streams every change immediately (batchMs: 0) over a plain channel, while kep1 batches once a second over an encrypted, authenticated channel.
Readiness The component reports ready once the first instance is connected and subscribing, so orchestrators are not blocked waiting for every server.

publishIntervalMs, samplingRateMs, and queueSize resolve from the most specific source that provides them:

signal-matcher value ▸ instances[].defaults ▸ component.global.defaults ▸ built-in default

Built-in defaults: publishIntervalMs = 1000, samplingRateMs = 0 (server’s fastest), queueSize = 100. publishIntervalMs is a subscription setting (a matcher cannot set it); samplingRateMs and queueSize are signal-matcher settings (and also accepted on defaults as the fallback). batchMs defaults to the resolved instance publishIntervalMs when omitted. The publish topic is not configured — it is UNS-minted (ecv1/{device}/{component}/{instance}/data/{signalPath}).

For the full option matrix, defaults, and template variables, see reference/configuration.md. For the topic/message payloads see reference/messaging-interface.md.