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.
5.6 KiB
Read/Write Operations
DriverNodeManager (src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs) wires the OPC UA stack's per-variable OnReadValue and OnWriteValue hooks to each driver's IReadable and IWritable capabilities. Every dispatch flows through CapabilityInvoker so the Polly pipeline (retry / timeout / breaker / bulkhead) applies uniformly across Galaxy, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client drivers.
OnReadValue
The hook is registered on every BaseDataVariableState created by the IAddressSpaceBuilder.Variable(...) call during discovery. When the stack dispatches a Read for a node in this namespace:
- If the driver does not implement
IReadable, the hook returnsBadNotReadable. - The node's
NodeId.Identifieris used directly as the driver-side full reference — it matchesDriverAttributeInfo.FullNameregistered at discovery time. - (Phase 6.2) If an
AuthorizationGate+NodeScopeResolverare wired, the gate is consulted first viaIsAllowed(identity, OpcUaOperation.Read, scope). A denied read never hits the driver. - The call is wrapped by
_invoker.ExecuteAsync(DriverCapability.Read, ResolveHostFor(fullRef), …). The resolved host isIPerCallHostResolver.ResolveHost(fullRef)for multi-host drivers; single-host drivers fall back toDriverInstanceId(decision #144). - The first
DataValueSnapshotfrom the batch populates the outgoingvalue/statusCode/timestamp. An empty batch surfacesBadNoData; any exception surfacesBadInternalError.
The hook is synchronous — the async invoker call is bridged with AsTask().GetAwaiter().GetResult() because the OPC UA SDK's value-hook signature is sync. Idempotent-by-construction reads mean this bridge is safe to retry inside the Polly pipeline.
OnWriteValue
OnWriteValue follows the same shape with two additional concerns: authorization and idempotence.
Authorization (two layers)
- SecurityClassification gate. Every variable stores its
SecurityClassificationin_securityByFullRefat registration time (populated fromDriverAttributeInfo.SecurityClass).WriteAuthzPolicy.IsAllowed(classification, userRoles)runs first, consulting the session's roles viacontext.UserIdentity is IRoleBearer.FreeAccesspasses anonymously,ViewOnlydenies everyone, andOperate / Tune / Configure / SecuredWrite / VerifiedWriterequireWriteOperate / WriteTune / WriteConfigureroles respectively. Denial returnsBadUserAccessDeniedwithout consulting the driver — drivers never enforce ACLs themselves; they only report classification as discovery metadata (feedbackfeedback_acl_at_server_layer.md). - Phase 6.2 permission-trie gate. When
AuthorizationGateis wired, it re-runs with the operation derived fromWriteAuthzPolicy.ToOpcUaOperation(classification). The gate consults the per-cluster permission trie loaded fromNodeAclrows, enforcing fine-grained per-tag ACLs on top of the role-based classification policy. Seedocs/v2/acl-design.md.
Dispatch
_invoker.ExecuteWriteAsync(host, isIdempotent, callSite, …) honors the WriteIdempotentAttribute semantics per decisions #44-45 and #143:
isIdempotent = true(tag flaggedWriteIdempotentin the Config DB) → runs through the standardDriverCapability.Writepipeline; retry may apply per the tier configuration.isIdempotent = false(default) → the invoker builds a one-off pipeline withRetryCount = 0. A timeout may fire after the device already accepted the pulse / alarm-ack / counter-increment; replay is the caller's decision, not the server's.
The _writeIdempotentByFullRef lookup is populated at discovery time from the DriverAttributeInfo.WriteIdempotent field.
Per-write status
IWritable.WriteAsync returns IReadOnlyList<WriteResult> — one numeric StatusCode per requested write. A non-zero code is surfaced directly to the client; exceptions become BadInternalError. The OPC UA stack's pattern of batching per-service is preserved through the full chain.
Array element writes
Array-element writes via OPC UA IndexRange are driver-specific. The OPC UA stack hands the dispatch an unwrapped NumericRange on the indexRange parameter of OnWriteValue; DriverNodeManager passes the full value object to IWritable.WriteAsync and the driver decides whether to support partial writes. Galaxy performs a read-modify-write inside the Galaxy driver (MXAccess has no element-level writes); other drivers generally accept only full-array writes today.
HistoryRead
DriverNodeManager.HistoryReadRawModified, HistoryReadProcessed, HistoryReadAtTime, and HistoryReadEvents route through the driver's IHistoryProvider capability with DriverCapability.HistoryRead. Drivers without IHistoryProvider surface BadHistoryOperationUnsupported per node. See docs/HistoricalDataAccess.md.
Failure isolation
Per decision #12, exceptions in the driver's capability call are logged and converted to a per-node BadInternalError — they never unwind into the master node manager. This keeps one driver's outage from disrupting sibling drivers in the same server process.
Key source files
src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs—OnReadValue/OnWriteValuehookssrc/ZB.MOM.WW.OtOpcUa.Server/Security/WriteAuthzPolicy.cs— classification-to-role policysrc/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs— Phase 6.2 trie gatesrc/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs—ExecuteAsync/ExecuteWriteAsyncsrc/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs,IWritable.cs,WriteIdempotentAttribute.cs