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.
This commit is contained in:
@@ -1,82 +1,72 @@
|
||||
# Address Space
|
||||
|
||||
The address space maps the Galaxy object hierarchy and attribute definitions into an OPC UA browse tree. `LmxNodeManager` builds the tree from data queried by `GalaxyRepositoryService`, while `AddressSpaceBuilder` provides a testable in-memory model of the same structure.
|
||||
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.
|
||||
|
||||
## Root ZB Folder
|
||||
## Driver root folder
|
||||
|
||||
Every address space starts with a single root folder node named `ZB` (NodeId `ns=1;s=ZB`). This folder is added under the standard OPC UA `Objects` folder via an `Organizes` reference. The reverse reference is registered through `MasterNodeManager.AddReferences` because `BuildAddressSpace` runs after `CreateAddressSpace` has already consumed the external references dictionary.
|
||||
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}`.
|
||||
|
||||
The root folder has `EventNotifier = SubscribeToEvents` enabled so alarm events propagate up to clients subscribed at the root level.
|
||||
## IAddressSpaceBuilder surface
|
||||
|
||||
## Area Folders vs Object Nodes
|
||||
`IAddressSpaceBuilder` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs`) offers three calls:
|
||||
|
||||
Galaxy objects fall into two categories based on `template_definition.category_id`:
|
||||
- `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).
|
||||
|
||||
- **Areas** (`category_id = 13`) become `FolderState` nodes with `FolderType` type definition and `Organizes` references. They represent logical groupings in the Galaxy hierarchy (e.g., production lines, cells).
|
||||
- **Non-area objects** (AppEngine, Platform, UserDefined, etc.) become `BaseObjectState` nodes with `BaseObjectType` type definition and `HasComponent` references. These represent runtime automation objects that carry attributes.
|
||||
Drivers drive ordering. Typical pattern: root → folder per equipment → variables per tag. `GenericDriverNodeManager` calls `DiscoverAsync` once on startup and once per rediscovery cycle.
|
||||
|
||||
Both node types use `contained_name` as the browse name. When `contained_name` is null or empty, `tag_name` is used as a fallback.
|
||||
## DriverAttributeInfo → OPC UA variable
|
||||
|
||||
## Variable Nodes for Attributes
|
||||
Each variable carries a `DriverAttributeInfo` (`src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs`):
|
||||
|
||||
Each Galaxy attribute becomes a `BaseDataVariableState` node under its parent object. The variable is configured with:
|
||||
| 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` |
|
||||
|
||||
- **DataType** -- Mapped from `mx_data_type` via `MxDataTypeMapper` (see [DataTypeMapping.md](DataTypeMapping.md))
|
||||
- **ValueRank** -- `OneDimension` (1) for arrays, `Scalar` (-1) for scalars
|
||||
- **ArrayDimensions** -- Set to `[array_dimension]` when the attribute is an array
|
||||
- **AccessLevel** -- `CurrentReadOrWrite` or `CurrentRead` based on security classification, with `HistoryRead` added for historized attributes
|
||||
- **Historizing** -- Set to `true` for attributes with a `HistoryExtension` primitive
|
||||
- **Initial value** -- `null` with `StatusCode = BadWaitingForInitialData` until the first MXAccess callback delivers a live value
|
||||
The initial value stays `null` with `StatusCode = BadWaitingForInitialData` until the first Read or `ISubscribable.OnDataChange` push lands.
|
||||
|
||||
## Primitive Grouping
|
||||
## CapturingBuilder + alarm sink registration
|
||||
|
||||
Galaxy objects can have primitive components (e.g., alarm extensions, history extensions) that attach sub-attributes to a parent attribute. The address space handles this with a two-pass approach:
|
||||
`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`.
|
||||
|
||||
### First pass: direct attributes
|
||||
## NodeId scheme
|
||||
|
||||
Attributes with an empty `PrimitiveName` are created as direct variable children of the object node. If a direct attribute shares its name with a primitive group, the variable node reference is saved for the second pass.
|
||||
|
||||
### Second pass: primitive child attributes
|
||||
|
||||
Attributes with a non-empty `PrimitiveName` are grouped by that name. For each group:
|
||||
|
||||
1. If a direct attribute variable with the same name already exists, the primitive's child attributes are added as `HasComponent` children of that variable node. This merges alarm/history sub-attributes (e.g., `InAlarm`, `Priority`) under the parent variable they describe.
|
||||
2. If no matching direct attribute exists, a new `BaseObjectState` node is created with NodeId `ns=1;s={TagName}.{PrimitiveName}`, and the primitive's attributes are added under it.
|
||||
|
||||
This structure means that browsing `TestMachine_001/SomeAlarmAttr` reveals both the process value and its alarm sub-attributes (`InAlarm`, `Priority`, `DescAttrName`) as children.
|
||||
|
||||
## NodeId Scheme
|
||||
|
||||
All node identifiers use string-based NodeIds in namespace index 1 (`ns=1`):
|
||||
All nodes live in the driver's namespace (not a shared `ns=1`). Browse paths are driver-defined:
|
||||
|
||||
| Node type | NodeId format | Example |
|
||||
|-----------|---------------|---------|
|
||||
| Root folder | `ns=1;s=ZB` | `ns=1;s=ZB` |
|
||||
| Area folder | `ns=1;s={tag_name}` | `ns=1;s=Area_001` |
|
||||
| Object node | `ns=1;s={tag_name}` | `ns=1;s=TestMachine_001` |
|
||||
| Scalar variable | `ns=1;s={tag_name}.{attr}` | `ns=1;s=TestMachine_001.MachineID` |
|
||||
| Array variable | `ns=1;s={tag_name}.{attr}` | `ns=1;s=MESReceiver_001.MoveInPartNumbers` |
|
||||
| Primitive sub-object | `ns=1;s={tag_name}.{prim}` | `ns=1;s=TestMachine_001.AlarmPrim` |
|
||||
|---|---|---|
|
||||
| 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 array attributes, the `[]` suffix present in `full_tag_reference` is stripped from the NodeId. The `full_tag_reference` (with `[]`) is kept internally for MXAccess subscription addressing. This means `MESReceiver_001.MoveInPartNumbers[]` in the Galaxy maps to NodeId `ns=1;s=MESReceiver_001.MoveInPartNumbers`.
|
||||
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.
|
||||
|
||||
## Topological Sort
|
||||
## Per-driver hierarchy examples
|
||||
|
||||
The hierarchy query returns objects ordered by `parent_gobject_id, tag_name`, but this does not guarantee that a parent appears before all of its children in all cases. `LmxNodeManager.TopologicalSort` performs a depth-first traversal to produce a list where every parent is guaranteed to precede its children. This allows the build loop to look up parent nodes from `_nodeMap` without forward references.
|
||||
- **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.
|
||||
|
||||
## Platform Scope Filtering
|
||||
See `docs/v2/driver-specs.md` for the per-driver discovery contracts.
|
||||
|
||||
When `GalaxyRepository.Scope` is set to `LocalPlatform`, the hierarchy and attributes passed to `BuildAddressSpace` are pre-filtered by `PlatformScopeFilter` inside `GalaxyRepositoryService`. The node manager receives only the local platform's objects and their ancestor areas, so the resulting browse tree is a subset of the full Galaxy. The filtering is transparent to `LmxNodeManager` — it builds nodes from whatever data it receives.
|
||||
## Rediscovery
|
||||
|
||||
Clients browsing a `LocalPlatform`-scoped server will see only the areas and objects hosted by that platform. Areas that exist in the Galaxy but contain no local descendants are excluded. See [Galaxy Repository — Platform Scope Filter](GalaxyRepository.md#platform-scope-filter) for the filtering algorithm and configuration.
|
||||
|
||||
## Incremental Sync
|
||||
|
||||
On address space rebuild (triggered by a Galaxy deploy change), `SyncAddressSpace` uses `AddressSpaceDiff` to identify which `gobject_id` values have changed between the old and new snapshots. Only the affected subtrees are torn down and rebuilt, preserving unchanged nodes and their active subscriptions. Affected subscriptions are snapshot before teardown and replayed after rebuild.
|
||||
|
||||
If no previous state is cached (first build), the full `BuildAddressSpace` path runs instead.
|
||||
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.Host/OpcUa/LmxNodeManager.cs` -- Node manager with `BuildAddressSpace`, `SyncAddressSpace`, and `TopologicalSort`
|
||||
- `src/ZB.MOM.WW.OtOpcUa.Host/OpcUa/AddressSpaceBuilder.cs` -- Testable in-memory model builder
|
||||
- `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
|
||||
|
||||
Reference in New Issue
Block a user