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

212 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](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 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 registration** — `DriverFactoryBootstrap.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.
---
## 7. Recommended sequencing
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`.