5.2 KiB
OtOpcUa v3 raw-path live-gate (Gitea #14) — PASS
Date: 2026-07-23 · Scope: the raw-subtree slice of #14 (decision: "raw gate now, flag B/C").
Rig: ScadaBridge docker rig (scadabridge:latest, rebuilt with the host-rewrite fix) against the
live otopcua-dev v3 cluster (otopcua-host:dev, built 2026-07-23, branch feat/mesh-phase5,
past the v3 merge ec6598ce). Related: scope doc 2026-07-23-otopcua-v3-dual-namespace-cutover-scope.md,
phase-2 plan 2026-07-23-otopcua-v3-nsu-hardening-and-browse-ux.md.
Headline
ScadaBridge is confirmed structurally v3-safe against a real dual-namespace server — durable
nsu= references are emitted by browse, stored on bindings, and resolved live against the server's
NamespaceArray to Good-quality reads. The gate also surfaced and fixed a real DCL robustness
bug (couldn't dial a server advertising 0.0.0.0). Items B/C remain deferred — the dev
cluster has no UNS nodes and no native alarms to gate them against.
Environment findings
- v3 cluster publishes both namespaces:
ns=2 = https://zb.com/otopcua/raw,ns=3 = https://zb.com/otopcua/uns. Raw lands at ns=2 — the exact index collision scope §4 item A predicted (v2's sole namespace and v3 raw both at ns=2). - Raw tree is sparsely populated: one materialized live Variable,
nsu=https://zb.com/otopcua/raw;s=sa-modbus2/plc/SiteAOnly(Int16, AccessLevel=1 → read-only). Other drivers (calc1, opcua1, pymodbus, gate-modbus, Area1) expose folders but no live leaf tags. - UNS (ns=3) is empty —
ns=3;s=OtOpcUa→BadNodeIdUnknown; no Equipment authored. - No native alarms — zero
HasCondition/HasEventSourcerefs anywhere in the raw tree.
The fix this gate produced
RealOpcUaClient took the server-advertised EndpointDescription.EndpointUrl verbatim for the
session channel. OtOpcUa advertises opc.tcp://0.0.0.0:4840/OtOpcUa (its
OpcUaApplicationHostOptions.PublicHostname defaults to 0.0.0.0), and dialling 0.0.0.0 from
inside the client container hits its own loopback → connect fails. Added
RealOpcUaClient.RewriteEndpointHostForReachability (swaps the advertised authority for the
reachable discovery host/port, preserves scheme+path, no-op when already reachable), called in both
ConnectAsync and VerifyEndpointAsync. Mirrors CoreClientUtils.SelectEndpoint and OtOpcUa's own
client (IEndpointDiscovery). Commit a1abbff7 on fix/opcua-discovery-host-rewrite; 9 unit tests
(RealOpcUaClientEndpointRewriteTests).
Wiring (how the gate was run)
- Rebuilt
scadabridge:latestwith the fix (bash docker/deploy.sh). docker network connect otopcua-dev_default scadabridge-site-a-a(and-b) — bridged the two docker networks so site-a can resolveotopcua-dev-site-a-1-1.data-connection create --site-id 1 --name OtOpcUa-v3-raw --protocol OpcUa→endpointUrl=opc.tcp://otopcua-dev-site-a-1-1:4840/OtOpcUa, securityMode none (id 3044).deploy artifacts --site-id 1— pushed the def to site-a's LocalDb + made it live in the DCL.- Template 2148 (
OtOpcUaV3Probe) + attributeSiteA(Int32,--data-source nsu=https://zb.com/otopcua/raw;s=sa-modbus2/plc/SiteAOnly); instance 98 (otopcua-v3-probe-1) on site-a;set-bindings [["SiteA",3044]];deploy instance.
Checks
| # | Check | Result |
|---|---|---|
| 1 | Connect via ConnectAsync (through the rewrite fix) |
PASS — data-connection browse returned the address space |
| 2 | Connect via VerifyEndpointAsync (second rewrite call site) |
PASS — verify-endpoint → {"success":true} |
| 3 | Browse emits durable nsu= throughout |
PASS — root nsu=…/raw;s=OtOpcUa, drivers + …;s=sa-modbus2/plc/SiteAOnly |
| 4 | Both namespaces visible / discoverable | PASS (partial) — raw browsable; uns registered but empty (nothing to browse) |
| 5 | Durable nsu= binding resolves live |
PASS — attribute stores nsu=…, subscription resolves it against the live NamespaceArray |
| 6 | Read round-trip, Good quality, live | PASS — SiteA Good, values changing 59689→59806→59822→59834 across snapshots |
| 7 | Write round-trip | N/A — the only materialized tag is read-only (AccessLevel=1); no writable v3 tag to gate |
Deferred (unchanged — need OtOpcUa-side authoring on the dev cluster)
- Item A — re-author legacy
ns=2bindings tonsu=(data; the collision is confirmed real here). - Item B — native-alarm dedup across raw+uns fan-out: no native alarms exist to drive it.
- Item C — UNS-bound → RawPath alarm routing: no UNS nodes and no alarms exist to drive it.
To gate B/C, the OtOpcUa dev cluster needs UNS Equipment authored (populate ns=3, referencing raw tags) and at least one alarm-configured tag raising a Part-9 condition that fans to raw + equipment notifiers.
Rig artifacts created (non-default rig state)
- ScadaBridge site-a-a / site-a-b attached to
otopcua-dev_default(dropped on nextdocker/deploy.sh). - Site-a config: DataConnection 3044
OtOpcUa-v3-raw, template 2148, instance 98 (deployed). Harmless; remove withinstance delete 98/template delete 2148/data-connection delete 3044if a clean rig is wanted.