STRUCTURAL: links-report.md has no rows for this doc; check_links.py clean.
STALE-STATUS / CODE-REALITY fixes (file:line evidence):
- 'Galaxy Proxy' / GalaxyProxyDriver.DiscoverAsync retired (PR 7.2) -> GalaxyDriver.DiscoverAsync delegates to GalaxyDiscoverer (Browse/GalaxyDiscoverer.cs:42); removed bogus 'AlarmExtension primitive' + 'two-pass primitive-grouping' claims (IsAlarm comes straight from the gateway hierarchy, GalaxyDiscoverer.cs:71).
- DriverNodeManager.CreateAddressSpace / DriverNodeManager.MapDataType: no such class. Root folder is created by OtOpcUaNodeManager.CreateAddressSpace (OtOpcUaNodeManager.cs:225) as a single shared 'OtOpcUa' root, EventNotifier=None (cs:234-237), not per-driver ns;s={DriverInstanceId}/urn:OtOpcUa:{id}/SubscribeToEvents|HistoryRead. Data-type resolution is OtOpcUaNodeManager.ResolveBuiltInDataType (cs:177) plus per-driver maps (Galaxy Browse/DataTypeMap.Map).
- _securityByFullRef is a Galaxy-driver-internal cache (GalaxyDriver.cs:65/682), not a node-manager field; WriteAuthzPolicy and _writeIdempotentByFullRef do not exist. Rewrote SecurityClass row to the real NodePermissions/TriePermissionEvaluator authz path (TriePermissionEvaluator.cs:78) and WriteIdempotent row to the Polly-retry semantics from DriverAttributeInfo.cs:28-35.
- NodeId scheme table rewritten: string NodeIds under one shared namespace from Config-DB ids / driver refs (Phase7Applier.cs:119-167), not ns;s={DriverInstanceId}.
- Rediscovery: OPC UA Client does NOT implement IRediscoverable (OpcUaClientDriver.cs:31); only Galaxy (DeployWatcher time_of_last_deploy) and TwinCAT (symbol-version-changed 1809) do.
- AB CIP: folder-per-device (AbCipDriver.cs:912-950), not 'per program'; UDT members fan into sub-folders, controller browse into Discovered/.
INLINE COMPLETENESS: added Source (NodeSourceKind) row; documented the two-layer builder->actor->SDK-sink architecture; added EquipmentNodeWalker.cs + Phase7Applier.cs to Key source files.
Verified DataTypeMap.cs lives at the CLAUDE.md-cited path (Driver.Galaxy/Browse/DataTypeMap.cs); contained-name/tag-name + ValueRank/ArrayDim claims cross-checked against Browse/GalaxyDiscoverer.cs:49-71.
9.6 KiB
Address Space
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