Files
lmxopcua/docs/plans/2026-06-06-equipment-namespace-materialization-scope.md
T

12 KiB
Raw Blame History

Scope: Equipment-Namespace Materialization in the Live Deploy Path

STATUS UPDATE 2026-06-07 — WS-1/2/4 (structure) and WS-3a (VirtualTag live values) are DONE. The structure milestone shipped on master (febe462..9a67ebc). WS-3a — live values via VirtualTags shipped on branch feat/equipment-namespace-live-values (plan: 2026-06-07-equipment-namespace-live-values.md). Verified live in docker-dev: galaxy mirror 396/396 Good, and the company-shape Equipment namespace carries live Good values via VirtualTags — every company signal backed by a real galaxy source (396 of 1036; the other 640 cite synthetic refs the company UNS model invented beyond the 396 real galaxy attributes). Restart-safe (bootstrap restore re-serves Good, no re-deploy). WS-3b (OpcUaClient driver route) was not taken (the VirtualTag route was chosen). WS-5 (tests) done throughout.

Status: WS-1/2/4 + WS-3a DONE (2026-06-07); WS-3b not pursued Date: 2026-06-06 Author: investigation while building the Northwind UNS overlay (see scadaproj/otopcua-uns-loader/) Depends on: the driver value-streaming fixes already on master (c1ce583, b1b3f3f)


1. One-paragraph summary

OtOpcUa can build a SystemPlatform namespace (the Galaxy mirror) into the live OPC UA address space with streaming values, but it cannot do the same for an Equipment-kind namespace. The canonical UNS (Enterprise/Site/Area/Line/Equipment/Signal) that an Equipment namespace represents only ever materialises its skeleton (Area/Line/Equipment folders); the signals under equipment (Tag, VirtualTag, ScriptedAlarm rows) never appear, because the component that turns those rows into OPC UA variables — EquipmentNodeWalker — is fully built and unit-tested but never invoked in production, and the live rebuild path doesn't carry the data it needs. This document scopes the work to finish that pipeline.


2. What works vs. what doesn't (verified 2026-06-06)

Works — SystemPlatform / Galaxy mirror (reference implementation): A deploy materialises one folder per Galaxy object and one variable per Tag row, and the driver streams live values into them. Verified live: 396 tags across 40 machines, all Good, on opc.tcp://localhost:4840. Path: OpcUaPublishActor.HandleRebuildPhase7Applier.MaterialiseHierarchy + Phase7Applier.MaterialiseGalaxyTags, and values via the DriverHostActor SubscribeBulk pass (b1b3f3f).

Doesn't work — Equipment namespace: Deploying an Equipment namespace + a UnsArea/UnsLine/Equipment + one VirtualTag produced:

Phase7Applier: hierarchy materialised (areas=1, lines=1, equipment=1)   ← folders only

…and the equipment node had zero child variables — the VirtualTag never materialised. There was no equipment-tag/virtual-tag log line at all.


3. Root cause (precise)

Three gaps, in order of how fundamental they are:

3.1 EquipmentNodeWalker is built + tested but never wired (the core gap)

  • src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.csWalk(IAddressSpaceBuilder, EquipmentNamespaceContent) materialises every Equipment row as a folder and every Equipment-bound Tag / VirtualTag / ScriptedAlarm as a variable (NodeId = DriverAttributeInfo.FullName, i.e. the tag's TagConfig.FullName; VirtualTag uses its VirtualTagId).
  • The only call sites are in tests/Core/.../EquipmentNodeWalkerTests.cs. Nothing in src ever calls EquipmentNodeWalker.Walk, and nothing builds its input record EquipmentNamespaceContent (the record exists; no producer exists).
  • The live rebuild — src/Server/.../Runtime/OpcUa/OpcUaPublishActor.cs:HandleRebuild — calls MaterialiseHierarchy (the Area/Line/Equipment folders) and MaterialiseGalaxyTags (SystemPlatform only). It never calls EquipmentNodeWalker.
  • OtOpcUaNodeManager references EquipmentNodeWalker only in a header comment — not in CreateAddressSpace.

3.2 The deployment composition/artifact drops equipment signals

  • Phase7CompositionResult (src/Server/.../OpcUaServer/Phase7Composer.cs) carries UnsAreas, UnsLines, EquipmentNodes (EquipmentId, DisplayName, UnsLineId — no tags), DriverInstancePlans, ScriptedAlarmPlans, GalaxyTags. There is no equipment-tag or virtual-tag list.
  • DeploymentArtifact.ParseComposition (src/Server/.../Runtime/Drivers/DeploymentArtifact.cs) reads the artifact's Tags array but BuildGalaxyTagPlans explicitly skips any tag with a non-null EquipmentId (line ~176). So even though equipment tags are serialised into the artifact, the composition the node consumes throws them away.

So even if EquipmentNodeWalker were wired into HandleRebuild, it would have no equipment-tag data to walk.

3.3 No value source for Equipment-namespace signals

Equipment signals can be valued two ways; neither is currently wired:

  • Driver-sourced Tag (e.g. an OPC UA Client remap of the Galaxy mirror): the OpcUaClient driver has no factory registrationDriverFactoryBootstrap.Register (src/Server/.../Host/Drivers/DriverFactoryBootstrap.cs) wires AbCip/AbLegacy/FOCAS/Galaxy/ Modbus/S7/TwinCAT but not OpcUaClient, so an OpcUaClient DriverInstance silently stubs → no values. (The driver itself, IDriver,ITagDiscovery,IReadable,IWritable,ISubscribable,…, exists and is otherwise complete; only the factory Register is missing.)
  • VirtualTag (script mirrors a live tag): the DependencyMuxActor + a real IVirtualTagEvaluator are registered (Runtime/ServiceCollectionExtensions.cs:100, Host/Program.cs:102), and driver values now reach the mux (DriverHostActor.ForwardToMux, b1b3f3f). But VirtualTagContext.GetTag reads from a per-evaluation cache fed by an ITagUpstreamSource, and no concrete ITagUpstreamSource is registered in the Host — and it is unverified whether a VirtualTag in an Equipment namespace can resolve a tag that lives in the SystemPlatform namespace (cross-namespace ctx.GetTag("/TestMachine_001/…")).

4. Goal / acceptance criteria

A deploy that includes an Equipment-kind namespace results in, on opc.tcp://…:4840:

  1. Structure: browsable …/<area>/<line>/<equipment>/<signal> for every UnsArea/UnsLine/ Equipment/Tag(+VirtualTag,ScriptedAlarm) row — folders browse-named by their friendly Name, not their logical Id (see §6.4).
  2. Values: each signal carries a live Good value (driver-sourced and/or VirtualTag-derived).
  3. Reload-safe: survives a node restart with no re-deploy (must run on the RestoreApplied bootstrap path added in b1b3f3f, not just on a fresh apply).
  4. Verifiable headlessly: the scadaproj/otopcua-uns-loader tool's verify passes against the company-shape namespace (extend it to browse the Equipment tree).

5. Workstreams

WS-1 — Carry equipment signals through the composition (foundational)

Extend Phase7CompositionResult with equipment Tag/VirtualTag/ScriptedAlarm plans (or reuse EquipmentNamespaceContent), populate them in Phase7Composer.Compose, serialise them in the deployment artifact, and parse them in DeploymentArtifact.ParseComposition (stop discarding EquipmentId != null tags). Risk: medium (touches composer + artifact format + planner; needs a format-compat story for already-sealed artifacts). Effort: ~12 days.

WS-2 — Materialise equipment signals in the live rebuild (wire the existing component)

In OpcUaPublishActor.HandleRebuild, after MaterialiseHierarchy, build EquipmentNamespaceContent from the composition and call the already-tested EquipmentNodeWalker.Walk. Make it idempotent and diff-aware to match the existing Galaxy-tag pass. Risk: lowmedium (component is tested; this is wiring + an idempotency pass). Effort: ~0.51 day.

WS-3 — Value path (pick one or both; see §6.1)

  • 3a VirtualTag route: register a concrete ITagUpstreamSource in the Host that bridges the DependencyMuxActor's tag values into the VirtualTag read-cache; confirm/enable cross-namespace ctx.GetTag resolution (Equipment VirtualTag reading a SystemPlatform mirror tag) and the dependency-graph re-evaluation trigger. Risk: high (cross-namespace resolution + dependency tracking are unproven end-to-end). Effort: ~23 days.
  • 3b OpcUaClient route: write+register OpcUaClientDriverFactoryExtensions.Register and add it to DriverFactoryBootstrap.Register; extend the SubscribeBulk pass to also subscribe Equipment-namespace Tag refs (TagConfig.FullName; NodeId == FullName, so the existing ForwardToMux value routing already applies — a ~30-line generalisation prototyped and reverted this session); decide the self-referential endpoint topology (a MAIN driver node OPC-UA-clienting into its own :4840 Galaxy mirror, vs a second cluster). Risk: high (unfinished driver + self-loop topology). Effort: ~24 days.

WS-4 — Browse-name fix (cosmetic but required for a usable shape)

Today the UNS folder browse name is the logical Id (observed nw-area-filling), not the friendly Name (filling). Confirm whether Phase7Applier.MaterialiseHierarchy / EquipmentNode.DisplayName should use UnsArea.Name/UnsLine.Name/Equipment.Name for the BrowseName (keeping the Id as the NodeId). Risk: low. Effort: ~0.5 day.

WS-5 — Tests + headless verification

Unit: composer carries equipment signals; HandleRebuild materialises them; round-trip artifact parse. Integration: a docker-dev deploy of a small Equipment namespace browses + reads Good. Extend otopcua-uns-loader verify to assert the Equipment tree. Effort: ~1 day.


6. Design decisions / open questions

  1. VirtualTag (3a) vs OpcUaClient (3b) for live values. VirtualTags reuse the live Galaxy mirror in-process (no second OPC UA session) but lean on unproven cross-namespace script resolution; OpcUaClient is the documented "remote-equipment → UNS" pattern but needs an unfinished driver factory and a self-referential session. Recommendation: prototype 3a first (smaller surface, no new driver), fall back to 3b if cross-namespace resolution proves intractable. A structure-only milestone (WS-1/2/4, no values) is independently shippable.
  2. Cross-namespace ctx.GetTag. Does an Equipment-namespace VirtualTag resolve a SystemPlatform Galaxy tag by browse path (/TestMachine_001/TestChangingInt) or by reference (TestMachine_001.TestChangingInt)? Determines the script-authoring contract. Must be settled before WS-3a.
  3. Artifact format compatibility. Adding equipment signals to the artifact changes its shape; ensure older sealed artifacts still parse (the parser is tolerant today — keep it so).
  4. Browse-name source (WS-4) — Name vs Id. Picking Name makes the company shape readable; confirm nothing keys off the Id-as-BrowseName.

  1. WS-1 + WS-2 + WS-4 (structure-only): Equipment namespaces browse the real …/area/line/equipment/signal shape with BadWaitingForInitialData leaves. Independently shippable; de-risks the composition/materialisation half.
  2. WS-3a (VirtualTag values): lights the structure up by mirroring the live Galaxy tags.
  3. WS-3b (OpcUaClient driver): only if a true remote-equipment driver path is wanted beyond the Galaxy mirror.
  4. WS-5 throughout.

Rough total: structure-only ≈ 23.5 days; +VirtualTag values ≈ +23 days.

8. Out of scope

  • Authoring the company UNS rows (the scadaproj/otopcua-uns-loader tool already generates them from company-uns.json).
  • Any change to the SystemPlatform/Galaxy path, which works.
  • The AdminUI UNS editor.

9. Key references

  • Works (reference): OpcUaPublishActor.HandleRebuild + Phase7Applier.MaterialiseGalaxyTags; SubscribeBulk in DriverHostActor (commits c1ce583, b1b3f3f).
  • Built-but-unwired: Core/OpcUa/EquipmentNodeWalker.cs (+ EquipmentNamespaceContent), tested only by EquipmentNodeWalkerTests.cs.
  • Composition gap: OpcUaServer/Phase7Composer.cs (Phase7CompositionResult), Runtime/Drivers/DeploymentArtifact.cs (BuildGalaxyTagPlans skips EquipmentId != null).
  • Value gaps: Host/Drivers/DriverFactoryBootstrap.cs (no OpcUaClient registration); Core.VirtualTags/ITagUpstreamSource.cs (no Host registration found).
  • The consuming tool + the company model: scadaproj/otopcua-uns-loader/, scadaproj/company-uns.json.