docs: scope Equipment-namespace materialization in the live deploy path
v2-ci / build (push) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 38s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Equipment-kind namespaces materialise only their Area/Line/Equipment folder skeleton on deploy, not the signals under them: EquipmentNodeWalker is fully built + unit-tested but has no production call site, the composition/artifact drops EquipmentId-bound tags, and no value source is wired (OpcUaClient driver factory missing; VirtualTag ITagUpstreamSource unregistered). Documents the gaps, workstreams, value-path options, and a structure-only-first sequencing.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
# Scope: Equipment-Namespace Materialization in the Live Deploy Path
|
||||
|
||||
**Status:** Scoping (not yet a task plan)
|
||||
**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: ~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 `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: ~2–3 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: ~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
|
||||
|
||||
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 ≈ 2–3.5 days; +VirtualTag values ≈ +2–3 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`.
|
||||
Reference in New Issue
Block a user