Files
lmxopcua/docs/AddressSpace.md

6.3 KiB

Address Space

Each driver's browsable subtree is built by streaming nodes from the driver's ITagDiscovery.DiscoverAsync implementation into an IAddressSpaceBuilder. GenericDriverNodeManager (src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs) owns the shared orchestration; in v2 the SDK-driven materialization is handled by OtOpcUaNodeManager (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs) fed via SdkAddressSpaceSink (src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs). 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.

Driver root folder

Every driver's subtree starts with a root FolderState under the standard OPC UA Objects folder, wired with an Organizes reference. DriverNodeManager.CreateAddressSpace creates this folder with NodeId = ns;s={DriverInstanceId}, BrowseName = {DriverInstanceId}, and EventNotifier = SubscribeToEvents | HistoryRead so alarm and history-event subscriptions can target the root. The namespace URI is urn:OtOpcUa:{DriverInstanceId}.

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 calls DiscoverAsync once on startup and once per rediscovery cycle.

DriverAttributeInfo → OPC UA variable

Each variable carries a DriverAttributeInfo (src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs):

Field OPC UA target
FullName NodeId.Identifier — used as the driver-side lookup key for Read/Write/Subscribe
DriverDataType mapped to a built-in DataTypeIds.* NodeId via DriverNodeManager.MapDataType
IsArray ValueRank = OneDimension when true, Scalar otherwise
ArrayDim declared array length, carried through as metadata
SecurityClass stored in _securityByFullRef for WriteAuthzPolicy gating on write
IsHistorized flips AccessLevel.HistoryRead + Historizing = true
IsAlarm drives the MarkAsAlarmCondition pass (see below)
WriteIdempotent stored in _writeIdempotentByFullRef; fed to CapabilityInvoker.ExecuteWriteAsync

The initial value stays null with StatusCode = BadWaitingForInitialData until the first Read or ISubscribable.OnDataChange push lands.

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 live in the driver's namespace (not a shared ns=1). Browse paths are driver-defined:

Node type NodeId format Example
Driver root ns;s={DriverInstanceId} urn:OtOpcUa:galaxy-01;s=galaxy-01
Folder ns;s={parent}/{browseName} ns;s=galaxy-01/Area_001
Variable ns;s={DriverAttributeInfo.FullName} ns;s=DelmiaReceiver_001.DownloadPath
Alarm condition ns;s={FullReference}.Condition ns;s=DelmiaReceiver_001.Temperature.Condition

For Galaxy the FullName stays in the legacy tag_name.AttributeName format; Modbus uses unit:register:type; AB CIP uses the native program:tag.member path; etc. — the shape is the driver's choice.

Per-driver hierarchy examples

  • Galaxy Proxy: walks the DB-snapshot hierarchy (GalaxyProxyDriver.DiscoverAsync), streams Area objects as folders and non-area objects as variable-bearing folders, marks IsAlarm = true on attributes that have an AlarmExtension primitive. The v1 two-pass primitive-grouping logic is retained inside the Galaxy driver.
  • Modbus: streams one folder per device, one variable per register range from ModbusDriverOptions. No alarm surface.
  • AB CIP: uses AbCipTemplateCache to enumerate user-defined types, streams a folder per program with variables keyed on the native tag path.
  • OPC UA Client: re-exposes a remote server's address space — browses the upstream and relays nodes through the builder.

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: time_of_last_deploy advance; TwinCAT: symbol-version-changed; OPC UA Client: server namespace change). Core re-runs DiscoverAsync and diffs — see docs/IncrementalSync.md. Static drivers (Modbus, S7) don't implement IRediscoverable; their address space only changes 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/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