From dbec3ee263bffe3ab9800ba6e404e4a09fb28d29 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 23 Jul 2026 16:54:48 -0400 Subject: [PATCH] =?UTF-8?q?docs(plans):=20OtOpcUa=20v3=20raw-path=20live-g?= =?UTF-8?q?ate=20PASS=20(#14)=20=E2=80=94=20fix=20validated=20live,=20B/C?= =?UTF-8?q?=20deferred?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...026-07-23-otopcua-v3-raw-path-live-gate.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/plans/2026-07-23-otopcua-v3-raw-path-live-gate.md diff --git a/docs/plans/2026-07-23-otopcua-v3-raw-path-live-gate.md b/docs/plans/2026-07-23-otopcua-v3-raw-path-live-gate.md new file mode 100644 index 00000000..06c1733c --- /dev/null +++ b/docs/plans/2026-07-23-otopcua-v3-raw-path-live-gate.md @@ -0,0 +1,79 @@ +# 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`/`HasEventSource` refs 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) + +1. Rebuilt `scadabridge:latest` with the fix (`bash docker/deploy.sh`). +2. `docker network connect otopcua-dev_default scadabridge-site-a-a` (and `-b`) — bridged the two + docker networks so site-a can resolve `otopcua-dev-site-a-1-1`. +3. `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**). +4. `deploy artifacts --site-id 1` — pushed the def to site-a's LocalDb + made it live in the DCL. +5. Template **2148** (`OtOpcUaV3Probe`) + attribute `SiteA` (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=2` bindings to `nsu=` (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 next `docker/deploy.sh`). +- Site-a config: DataConnection **3044** `OtOpcUa-v3-raw`, template **2148**, instance **98** (deployed). + Harmless; remove with `instance delete 98` / `template delete 2148` / `data-connection delete 3044` + if a clean rig is wanted.