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/--configsource, which defaults by platform:HOST→FILE,GREENGRASS→GG_CONFIG(the deployment),KUBERNETES→CONFIGMAP(a mounted directory, hot-reloaded). Adapter settings live undercomponent; 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
dataclass —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’sidentityelement. Reads/writes/queries are thecmd/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).
matchis a Java regex evaluated withString.matches(), which requires the entire string to match — it is implicitly anchored at both ends.Sine.*matchesSine1(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
matchentirely (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
namespaceUrivalues come from? A namespace URI is whatever the server advertises — it is server- and configuration-specific. For KEPServerEX it is commonlyKepware Server(used in these examples and the adapter’svalidation/configs); the OPC UA Foundation base namespace ishttp://opcfoundation.org/UA/(index 0). Discover the exact strings your server uses with thesb/signalscontrol query — it echoes the resolvednamespaceindex and itsnamespaceUrifor every subscribed signal — or by reading the server’sNamespaceArray. Always substitute your server’s values.
Addressing: the UNS data class
Section titled “Addressing: the UNS data class”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.idin the body (the fullns=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 asiteroot on a multi-site broker, settopic.includeRoot: true(needs ≥ 2 hierarchy levels). Per-signal topic overrides are not supported.
1. Minimal local / dev run (HOST + MQTT)
Section titled “1. Minimal local / dev run (HOST + MQTT)”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.
{ "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:
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.ComponentConfigComponentConfig: 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):
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.
{ "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\\..*" } ] } ] } ] }}Messaging & credentials options
Section titled “Messaging & credentials options”| 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. |
Connection & security options
Section titled “Connection & security options”| 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 needskeyCertSign+ the CA constraint), and its SAN URI must equal theapplicationUrithe adapter presents. See the security how-to andvalidation/gen_certs.pyfor 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, withexcludematchers that strip the per-device diagnostics, statistics, and_Systemhousekeeping 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 UNSdataclass as the rest — consumers select alarms by thesignal.id/addressin the body, or you split them out with a separateevt-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
processgroup also excludes.*\.Alarms\..*: a node can match more than one subscription. Excluding the alarm subtree fromprocesskeeps 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. |
Multiple namespaces
Section titled “Multiple namespaces”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.
The timing pipeline at a glance
Section titled “The timing pipeline at a glance”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 aedgecommonssubsystem you run in a forwarding component, not anopcua-adapteroption.
6. Kubernetes (ConfigMap)
Section titled “6. Kubernetes (ConfigMap)”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.
apiVersion: v1kind: ConfigMapmetadata: 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_NAME ▸ POD_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 companiondeployment.yamlmounts this ConfigMap at/etc/edgecommons(read-only, whole volume), setsworkingDir: /tmp(the Java MQTT client needs a writable cwd), and wires the health (8081) and metrics (9090) ports.
7. One adapter, multiple servers
Section titled “7. One adapter, multiple servers”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. |
Where settings resolve from (precedence)
Section titled “Where settings resolve from (precedence)”publishIntervalMs, samplingRateMs, and queueSize resolve from the most specific source that
provides them:
signal-matcher value ▸ instances[].defaults ▸ component.global.defaults ▸ built-in defaultBuilt-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.