Adds deferment.md — a source-verified inventory of new-driver status, all 17
open issues, in-code deferrals, open live gates and plan bookkeeping. Every
claim was checked against src/ and git, not against documentation.
Headline finding: three subsystems are authored, persisted and shipped but
never executed —
* Node ACLs: IPermissionEvaluator/TriePermissionEvaluator have zero
production consumers and OtOpcUaNodeManager never references them, yet
ClusterAcls.razor authors NodeAcl rows and ConfigComposer.cs:51 ships them
in every artifact. An authored deny rule has no effect.
* IRediscoverable / IHostConnectivityProbe raise into the void (#518/#507);
nothing ever writes a DriverHostStatus row.
* DriverTypeRegistry is vestigial, so no factory passes a tier and every
driver runs Tier A with the Tier-C protections dormant.
Also records the Calculation driver as picker-visible but unauthorable
(DriverConfigModal has no case) — the "registered but unauthorable" class
recurring after the Sql picker defect — and notes that no parity test guards
DriverConfigModal/DeviceModal, which is why it survived review.
Documentation corrections (source-verified):
* ReadWriteOperations.md claimed "a denied read never hits the driver" via
four types that do not exist in src/. Bannered + struck through; a security
review reading that page would have concluded a per-node ACL gate exists.
* CLAUDE.md: the Change Detection sentence was false (DriverHost consumes
nothing); the mesh Phase 4 and Phase 5 live gates and the auto-down 1-vs-1
gate had all PASSED; the ScriptedAlarmState table was already dropped.
* driver-expansion tracking: Modbus RTU and SQL poll are merged, not
pending — its command table would have made someone rebuild two shipped
drivers in a fresh worktree.
* drivers/README.md: dead DriverTypeRegistry paragraph, retired
SystemPlatform namespace kind, missing Sql + Calculation rows, missing
Modbus RTU-over-TCP transport.
* TwinCAT.md/Galaxy.md promised an address-space rebuild that never happens.
* Historian.md gained the #491 unproven-value-capture pointer.
* IncrementalSync.md and AddressSpace.md bannered as wholesale v2-era (the
rewrite is recorded as still owed, not done here); four v2 status docs
bannered as historical; three wrong-name-for-a-live-type fixes.
Left deliberately untouched: the secrets NoOpSecretReplicator line, which
makes a security claim and needs the real behaviour identified rather than a
rename.
11 KiB
Address Space
⚠️ Accuracy warning (audited 2026-07-27) — this page is v2-era and contradicts the shipped v3 address space. Corrections, in order of how badly they mislead:
- There is no "single custom namespace". v3 exposes two:
https://zb.com/otopcua/raw(s=<RawPath>) andhttps://zb.com/otopcua/uns(s=<Area>/<Line>/<Equipment>/<EffectiveName>), registered together atOtOpcUaNodeManager.cs:45. Identity authority isV3NodeIds+AddressSpaceRealm. The retiredhttps://zb.com/otopcua/nssurvives only in a comment marking it retired (V3NodeIds.cs:17).Phase7Applier/Phase7Composer/GalaxyTagPlando not exist, nor do the file paths cited for them. Renamed by40e8a23etoAddressSpaceApplier/AddressSpaceComposer(src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/); the composition result type isAddressSpaceComposition(AddressSpaceComposer.cs:13).- The
SystemPlatformnamespace kind is retired — Galaxy is a standard Equipment-kind driver.GenericDriverNodeManageris test scaffolding, not a production dispatch path (GenericDriverNodeManager.cs:71).- The §Rediscovery section is wrong: nothing consumes
OnRediscoveryNeeded(#518), and the driver list is stale (MQTT and MTConnect now raise it too).For the current design see the "v3 OPC UA Address Space (Batch 4)" section of
CLAUDE.md,docs/Raw.md, anddocs/Uns.md.
Address-space construction is a two-layer system. The driver-facing layer is the streaming builder: a driver implements ITagDiscovery.DiscoverAsync (src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs) and emits Folder / Variable / AddProperty calls into an IAddressSpaceBuilder as it walks its backend — no buffering of the whole tree. GenericDriverNodeManager (src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs) wraps that builder to capture alarm-condition sinks and routes alarm events from the driver to them. The SDK materialization layer turns the resulting node descriptions into live OPC UA nodes: OpcUaPublishActor drives the write-only IOpcUaAddressSpaceSink, whose production binding SdkAddressSpaceSink (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs) forwards to OtOpcUaNodeManager (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs), a CustomNodeManager2 subclass that owns the FolderState / BaseDataVariableState instances. The same code path serves Galaxy object hierarchies, Modbus PLC registers, AB CIP tags, TwinCAT symbols, FOCAS CNC parameters, and OPC UA Client aggregations — Galaxy is one driver of seven, not the driver.
Root folder
OtOpcUaNodeManager.CreateAddressSpace creates a single shared root FolderState (NodeId = OtOpcUa, BrowseName = OtOpcUa, EventNotifier = None) under the standard OPC UA Objects folder, wired with an Organizes reference. Every driver's folders and variables hang beneath this one root; the server is published under a single ApplicationUri = urn:OtOpcUa (the OpcUaApplicationHostOptions.ApplicationUri default) and all nodes live in the server's single custom namespace, not a per-driver urn:OtOpcUa:{DriverInstanceId}. The UNS Area → Line → Equipment folder skeleton under the root is materialised by Phase7Applier.MaterialiseHierarchy (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs); SystemPlatform (Galaxy) tags are materialised by Phase7Applier.MaterialiseGalaxyTags.
IAddressSpaceBuilder surface
IAddressSpaceBuilder (src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs) offers three calls:
Folder(browseName, displayName)— creates a childFolderStateand returns a child builder scoped to it.Variable(browseName, displayName, DriverAttributeInfo attributeInfo)— creates aBaseDataVariableStateand returns anIVariableHandlethe driver keeps for alarm wiring.AddProperty(browseName, DriverDataType, value)— attaches aPropertyStatefor static metadata (e.g. equipment identification fields).
Drivers drive ordering. Typical pattern: root → folder per equipment → variables per tag. GenericDriverNodeManager.BuildAddressSpaceAsync calls DiscoverAsync once on startup and once per rediscovery cycle, tearing down the previous alarm subscription and clearing its sink registry before each re-walk so a redeploy doesn't double-fire alarm events.
DriverAttributeInfo → OPC UA variable
Each variable carries a DriverAttributeInfo (src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs):
| Field | Role |
|---|---|
FullName |
driver-side full reference used as the lookup key for Read/Write/Subscribe; also seeds the variable's string NodeId |
DriverDataType |
resolved to a built-in DataTypeIds.* NodeId at materialization time — OtOpcUaNodeManager.ResolveBuiltInDataType maps the data-type name string; each driver first maps its native type into DriverDataType (e.g. Galaxy via Browse/DataTypeMap.Map) |
IsArray / ArrayDim |
declared 1-D-array length carried as metadata; the Galaxy discoverer sets ArrayDim only when the gateway reports a positive dimension |
SecurityClass |
write-authorization tier (SecurityClassification); enforced server-side by the NodePermissions ACL evaluator (TriePermissionEvaluator) mapping each OpcUaOperation to a required permission bit. The Galaxy driver also caches it per full reference (_securityByFullRef) to answer GetSecurityClassification |
IsHistorized |
marks the attribute as feeding historian / HistoryRead |
IsAlarm |
drives the MarkAsAlarmCondition pass (see below) |
WriteIdempotent |
when true the attribute's writes are safe to replay, so the capability invoker may apply Polly retry; defaults false so pulses / acks / counters aren't auto-retried |
Source |
NodeSourceKind discriminator (Driver / Virtual / ScriptedAlarm) that decides which subsystem dispatches the node's Read/Write/Subscribe |
The variable is created with StatusCode = BadWaitingForInitialData and a null value until the first Read or ISubscribable.OnDataChange push lands. Note the production SDK sink (OtOpcUaNodeManager.EnsureVariable) currently materialises every variable as ValueRank = Scalar, read-only AccessLevel, and Historizing = false — the IsArray/IsHistorized intent lives in DriverAttributeInfo but is not yet projected onto the SDK node.
CapturingBuilder + alarm sink registration
GenericDriverNodeManager.BuildAddressSpaceAsync wraps the supplied builder in a CapturingBuilder before calling DiscoverAsync. The wrapper observes every Variable() call: when a returned IVariableHandle.MarkAsAlarmCondition(AlarmConditionInfo) fires, the sink is registered in the manager's _alarmSinks dictionary keyed by the variable's FullReference. Subsequent IAlarmSource.OnAlarmEvent pushes are routed to the matching sink by SourceNodeId. This keeps the alarm-wiring protocol declarative — drivers just flag DriverAttributeInfo.IsAlarm = true and the materialization of the OPC UA AlarmConditionState node is handled by the server layer. See docs/AlarmTracking.md.
NodeId scheme
All nodes share the server's single custom namespace (NamespaceIndex); NodeIds are string identifiers, not numeric. The string values come from the source rows / driver references — there is no per-driver namespace prefix:
| Node type | NodeId (string identifier) | Example |
|---|---|---|
| Shared root | OtOpcUa |
OtOpcUa |
| UNS Area / Line / Equipment folder | the Config-DB UnsAreaId / UnsLineId / EquipmentId |
EQ_Press_07 |
| Galaxy tag variable | the MXAccess reference (Phase7Applier uses GalaxyTagPlan.MxAccessRef) |
DelmiaReceiver_001.DownloadPath |
| Equipment tag variable | the driver full reference from DriverAttributeInfo.FullName |
driver-specific |
For Galaxy the variable FullName is the tag_name.AttributeName MXAccess reference; AB CIP uses tag.Name or tag.Name.member for UDT members; the shape is the driver's choice. Browse-path resolution (OPC UA TranslateBrowsePathsToNodeIds) is the canonical way clients map a browse path to one of these flat NodeIds.
Per-driver hierarchy examples
- Galaxy:
GalaxyDriver.DiscoverAsyncdelegates toGalaxyDiscoverer(src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GalaxyDiscoverer.cs), which walks the hierarchy fromIGalaxyHierarchySource— one folder per Galaxy object (browse name =contained_name, falling back totag_name), one variable per dynamic attribute (full reference =tag_name.AttributeName). It copies the gateway-suppliedIsAlarmflag through toDriverAttributeInfoand, for alarm-bearing attributes, callsMarkAsAlarmConditionwith the five sub-attribute refs built byAlarmRefBuilder. - Modbus: streams one folder per device, one variable per register range from
ModbusDriverOptions. No alarm surface. - AB CIP:
AbCipDriver.DiscoverAsyncemits anAbCiproot, then a folder per configured device. Pre-declared tags become variables under the device folder; UDT (Structure) tags fan out into a sub-folder with one variable per member; when controller browse is enabled,IAbCipTagEnumeratoradds discovered tags under aDiscovered/sub-folder. (AbCipTemplateCachecaches UDT layouts for the libplctag enumerator.) - OPC UA Client: re-exposes a remote server's address space —
OpcUaClientDriver.DiscoverAsyncbrowses the upstream fromBrowseRootinto aRemotefolder (pass 1), then batch-reads DataType/AccessLevel/ValueRank/Historizing per variable before registering them (pass 2).
See docs/v2/driver-specs.md for the per-driver discovery contracts.
Rediscovery
Drivers that implement IRediscoverable fire OnRediscoveryNeeded when their backend signals a change. Galaxy's DeployWatcher raises it when the observed time_of_last_deploy advances; TwinCAT raises it on the ADS symbol-version-changed signal (DeviceSymbolVersionInvalid, error 1809). Core re-runs DiscoverAsync and diffs — see docs/IncrementalSync.md. Drivers that don't implement IRediscoverable (Modbus, S7, OPC UA Client) only change their address space when a new generation is published from the Config DB.
Key source files
src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs— orchestration +CapturingBuildersrc/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs,src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs— OPC UA materialization (write-only sink fed by the actor system)src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs— materialises the UNS folder hierarchy + Galaxy tags into the sinksrc/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs— walks Config-DB Equipment-namespace rows into the buildersrc/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs— builder contractsrc/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs— driver discovery capabilitysrc/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs— per-attribute descriptor