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.
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 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 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, marksIsAlarm = trueon attributes that have anAlarmExtensionprimitive. 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
AbCipTemplateCacheto 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 +CapturingBuildersrc/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs— OPC UA materialization (IAddressSpaceBuilderimpl +NestedBuilder)src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs— builder contractsrc/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs— driver discovery capabilitysrc/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs— per-attribute descriptor