12 KiB
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 branchfeat/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.HandleRebuild → Phase7Applier.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.cs—Walk(IAddressSpaceBuilder, EquipmentNamespaceContent)materialises everyEquipmentrow as a folder and every Equipment-boundTag/VirtualTag/ScriptedAlarmas a variable (NodeId =DriverAttributeInfo.FullName, i.e. the tag'sTagConfig.FullName; VirtualTag uses itsVirtualTagId).- The only call sites are in
tests/Core/.../EquipmentNodeWalkerTests.cs. Nothing insrcever callsEquipmentNodeWalker.Walk, and nothing builds its input recordEquipmentNamespaceContent(the record exists; no producer exists). - The live rebuild —
src/Server/.../Runtime/OpcUa/OpcUaPublishActor.cs:HandleRebuild— callsMaterialiseHierarchy(the Area/Line/Equipment folders) andMaterialiseGalaxyTags(SystemPlatform only). It never callsEquipmentNodeWalker. OtOpcUaNodeManagerreferencesEquipmentNodeWalkeronly in a header comment — not inCreateAddressSpace.
3.2 The deployment composition/artifact drops equipment signals
Phase7CompositionResult(src/Server/.../OpcUaServer/Phase7Composer.cs) carriesUnsAreas,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'sTagsarray butBuildGalaxyTagPlansexplicitly skips any tag with a non-nullEquipmentId(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 registration —DriverFactoryBootstrap.Register(src/Server/.../Host/Drivers/DriverFactoryBootstrap.cs) wires AbCip/AbLegacy/FOCAS/Galaxy/ Modbus/S7/TwinCAT but not OpcUaClient, so anOpcUaClientDriverInstancesilently stubs → no values. (The driver itself,IDriver,ITagDiscovery,IReadable,IWritable,ISubscribable,…, exists and is otherwise complete; only the factoryRegisteris missing.) VirtualTag(script mirrors a live tag): theDependencyMuxActor+ a realIVirtualTagEvaluatorare registered (Runtime/ServiceCollectionExtensions.cs:100,Host/Program.cs:102), and driver values now reach the mux (DriverHostActor.ForwardToMux,b1b3f3f). ButVirtualTagContext.GetTagreads from a per-evaluation cache fed by anITagUpstreamSource, and no concreteITagUpstreamSourceis registered in the Host — and it is unverified whether aVirtualTagin anEquipmentnamespace can resolve a tag that lives in theSystemPlatformnamespace (cross-namespacectx.GetTag("/TestMachine_001/…")).
4. Goal / acceptance criteria
A deploy that includes an Equipment-kind namespace results in, on opc.tcp://…:4840:
- Structure: browsable
…/<area>/<line>/<equipment>/<signal>for everyUnsArea/UnsLine/Equipment/Tag(+VirtualTag,ScriptedAlarm) row — folders browse-named by their friendlyName, not their logical Id (see §6.4). - Values: each signal carries a live
Goodvalue (driver-sourced and/or VirtualTag-derived). - Reload-safe: survives a node restart with no re-deploy (must run on the
RestoreAppliedbootstrap path added inb1b3f3f, not just on a fresh apply). - Verifiable headlessly: the
scadaproj/otopcua-uns-loadertool'sverifypasses 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: ~1–2 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: low–medium (component is tested; this is
wiring + an idempotency pass). Effort: ~0.5–1 day.
WS-3 — Value path (pick one or both; see §6.1)
- 3a VirtualTag route: register a concrete
ITagUpstreamSourcein the Host that bridges theDependencyMuxActor's tag values into the VirtualTag read-cache; confirm/enable cross-namespacectx.GetTagresolution (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: ~2–3 days. - 3b OpcUaClient route: write+register
OpcUaClientDriverFactoryExtensions.Registerand add it toDriverFactoryBootstrap.Register; extend the SubscribeBulk pass to also subscribe Equipment-namespaceTagrefs (TagConfig.FullName; NodeId == FullName, so the existingForwardToMuxvalue 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:4840Galaxy mirror, vs a second cluster). Risk: high (unfinished driver + self-loop topology). Effort: ~2–4 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
- 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.
- 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. - 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).
- Browse-name source (WS-4) —
NamevsId. PickingNamemakes the company shape readable; confirm nothing keys off the Id-as-BrowseName.
7. Recommended sequencing
- WS-1 + WS-2 + WS-4 (structure-only): Equipment namespaces browse the real
…/area/line/equipment/signalshape withBadWaitingForInitialDataleaves. Independently shippable; de-risks the composition/materialisation half. - WS-3a (VirtualTag values): lights the structure up by mirroring the live Galaxy tags.
- WS-3b (OpcUaClient driver): only if a true remote-equipment driver path is wanted beyond the Galaxy mirror.
- WS-5 throughout.
Rough total: structure-only ≈ 2–3.5 days; +VirtualTag values ≈ +2–3 days.
8. Out of scope
- Authoring the company UNS rows (the
scadaproj/otopcua-uns-loadertool already generates them fromcompany-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 inDriverHostActor(commitsc1ce583,b1b3f3f). - Built-but-unwired:
Core/OpcUa/EquipmentNodeWalker.cs(+EquipmentNamespaceContent), tested only byEquipmentNodeWalkerTests.cs. - Composition gap:
OpcUaServer/Phase7Composer.cs(Phase7CompositionResult),Runtime/Drivers/DeploymentArtifact.cs(BuildGalaxyTagPlansskipsEquipmentId != 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.