Files
lmxopcua/docs/AddressSpace.md
Joseph Doherty 985b7aba26 Doc refresh (task #202) — core architecture docs for multi-driver OtOpcUa
Rewrite seven core-architecture docs to match the shipped multi-driver platform.
The v1 single-driver LmxNodeManager framing is replaced with the Core +
capability-interface model — Galaxy is now one driver of seven, and each doc
points at the current class names + source paths.

What changed per file:
- OpcUaServer.md — OtOpcUaServer as StandardServer host; per-driver
  DriverNodeManager + CapabilityInvoker wiring; Config-DB-driven configuration
  (sp_PublishGeneration, DraftRevisionToken, Admin UI); Phase 6.2
  AuthorizationGate integration.
- AddressSpace.md — GenericDriverNodeManager.BuildAddressSpaceAsync walks
  ITagDiscovery.DiscoverAsync and streams DriverAttributeInfo through
  IAddressSpaceBuilder; CapturingBuilder registers alarm-condition sinks;
  per-driver NodeId schemes replace the fixed ns=1;s=ZB root.
- ReadWriteOperations.md — OnReadValue / OnWriteValue dispatch to
  IReadable.ReadAsync / IWritable.WriteAsync through CapabilityInvoker,
  honoring WriteIdempotentAttribute (#143); two-layer authorization
  (WriteAuthzPolicy + Phase 6.2 AuthorizationGate).
- Subscriptions.md — ISubscribable.SubscribeAsync/UnsubscribeAsync is the
  capability surface; STA-thread story is now Galaxy-specific (StaPump inside
  Driver.Galaxy.Host), other drivers are free-threaded.
- AlarmTracking.md — IAlarmSource is optional; AlarmSurfaceInvoker wraps
  Subscribe/Ack/Unsubscribe with fan-out by IPerCallHostResolver and the
  no-retry AlarmAcknowledge pipeline (#143); CapturingBuilder registers sinks
  at build time.
- DataTypeMapping.md — DriverDataType + SecurityClassification are the
  driver-agnostic enums; per-driver mappers (GalaxyProxyDriver inline,
  AbCipDataType, ModbusDriver, etc.); SecurityClassification is metadata only,
  ACL enforcement is at the server layer.
- IncrementalSync.md — IRediscoverable covers backend-change signals;
  sp_ComputeGenerationDiff + DiffViewer drive generation-level change
  detection; IDriver.ReinitializeAsync is the in-process recovery path.
2026-04-20 01:33:28 -04:00

6.1 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/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs) owns the shared orchestration; DriverNodeManager (src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs) implements IAddressSpaceBuilder against the OPC Foundation stack's CustomNodeManager2. 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/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/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/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs — orchestration + CapturingBuilder
  • src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs — OPC UA materialization (IAddressSpaceBuilder impl + NestedBuilder)
  • src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs — builder contract
  • src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs — driver discovery capability
  • src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs — per-attribute descriptor