Files
lmxopcua/docs/AddressSpace.md
T
Joseph Doherty 9071a3aae0 docs(audit): AddressSpace.md — accuracy + completeness pass
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.
2026-06-03 15:42:21 -04:00

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 child FolderState and returns a child builder scoped to it.
  • Variable(browseName, displayName, DriverAttributeInfo attributeInfo) — creates a BaseDataVariableState and returns an IVariableHandle the driver keeps for alarm wiring.
  • AddProperty(browseName, DriverDataType, value) — attaches a PropertyState for 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.DiscoverAsync delegates to GalaxyDiscoverer (src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GalaxyDiscoverer.cs), which walks the hierarchy from IGalaxyHierarchySource — one folder per Galaxy object (browse name = contained_name, falling back to tag_name), one variable per dynamic attribute (full reference = tag_name.AttributeName). It copies the gateway-supplied IsAlarm flag through to DriverAttributeInfo and, for alarm-bearing attributes, calls MarkAsAlarmCondition with the five sub-attribute refs built by AlarmRefBuilder.
  • Modbus: streams one folder per device, one variable per register range from ModbusDriverOptions. No alarm surface.
  • AB CIP: AbCipDriver.DiscoverAsync emits an AbCip root, 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, IAbCipTagEnumerator adds discovered tags under a Discovered/ sub-folder. (AbCipTemplateCache caches UDT layouts for the libplctag enumerator.)
  • OPC UA Client: re-exposes a remote server's address space — OpcUaClientDriver.DiscoverAsync browses the upstream from BrowseRoot into a Remote folder (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 + CapturingBuilder
  • src/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 sink
  • src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs — walks Config-DB Equipment-namespace rows into the builder
  • src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs — builder contract
  • src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs — driver discovery capability
  • src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs — per-attribute descriptor