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.
8.2 KiB
OPC UA Server
The OPC UA server component (src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs) hosts the OPC UA stack and exposes one browsable subtree per registered driver. The server itself is driver-agnostic — Galaxy/MXAccess, Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS, and OPC UA Client are all plugged in as IDriver implementations via the capability interfaces in src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/.
Composition
OtOpcUaServer subclasses the OPC Foundation StandardServer and wires:
- A
DriverHost(src/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs) which registers drivers and holds the per-instanceIDriverreferences. - One
DriverNodeManagerper registered driver (src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs), constructed inCreateMasterNodeManager. Each manager owns its own namespace URI (urn:OtOpcUa:{DriverInstanceId}) and exposes the driver as a subtree under the standardObjectsfolder. - A
CapabilityInvoker(src/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs) per driver instance, keyed on(DriverInstanceId, HostName, DriverCapability)against the sharedDriverResiliencePipelineBuilder. Every Read/Write/Discovery/Subscribe/HistoryRead/AlarmSubscribe call on the driver flows through this invoker so the Polly pipeline (retry / timeout / breaker / bulkhead) applies. The OTOPCUA0001 Roslyn analyzer enforces the wrapping at compile time. - An
IUserAuthenticator(LDAP in production, injected stub in tests) forUserNametoken validation in theImpersonateUserhook. - Optional
AuthorizationGate+NodeScopeResolver(Phase 6.2) that sit in front of every dispatch call. In lax mode the gate passes through when the identity lacks LDAP groups so existing integration tests keep working; strict mode (Authorization:StrictMode = true) denies those cases.
OtOpcUaServer.DriverNodeManagers exposes the materialized list so the hosting layer can walk each one post-start and call GenericDriverNodeManager.BuildAddressSpaceAsync(manager) — the manager is passed as its own IAddressSpaceBuilder.
Configuration
Server wiring used to live in appsettings.json. It now flows from the SQL Server Config DB: ServerInstance + DriverInstance + Tag + NodeAcl rows are published as a generation via sp_PublishGeneration and loaded into the running process by the generation applier. The Admin UI (Blazor Server, docs/v2/admin-ui.md) is the operator surface — drafts accumulate edits; sp_ComputeGenerationDiff drives the DiffViewer preview; a UNS drag-reorder carries a DraftRevisionToken so Confirm re-checks against the current draft and returns 409 if it advanced (decision #161). See docs/v2/config-db-schema.md for the schema.
Environmental knobs that aren't per-tenant (bind address, port, PKI path) still live in appsettings.json on the Server project; everything tenant-scoped moved to the Config DB.
Transport
The server binds one TCP endpoint per ServerInstance (default opc.tcp://0.0.0.0:4840). The ApplicationConfiguration is built programmatically in the OpcUaApplicationHost — there are no UA XML files. Security profiles (None, Basic256Sha256-Sign, Basic256Sha256-SignAndEncrypt) are resolved from the ServerInstance.Security JSON at startup; the default profile is still None for backward compatibility. User token policies (Anonymous, UserName) are attached based on whether LDAP is configured. See docs/security.md for hardening.
Session impersonation
OtOpcUaServer.OnImpersonateUser handles the three token types:
AnonymousIdentityToken→ default anonymousUserIdentity.UserNameIdentityToken→IUserAuthenticator.AuthenticateAsyncvalidates the credential (LdapUserAuthenticatorin production). On success, the resolved display name + LDAP-derived roles are wrapped in aRoleBasedIdentitythat implementsIRoleBearer.DriverNodeManager.OnWriteValuereads these roles viacontext.UserIdentity is IRoleBearerand appliesWriteAuthzPolicyper write.- Anything else →
BadIdentityTokenInvalid.
The Phase 6.2 AuthorizationGate runs on top of this baseline: when configured it consults the cluster's permission trie (loaded from NodeAcl rows) using the session's UserAuthorizationState and can deny Read / HistoryRead / Write / Browse independently per tag. See docs/v2/acl-design.md.
Dispatch
Every service call the stack hands to DriverNodeManager is translated to the driver's capability interface and routed through CapabilityInvoker:
| Service | Capability | Invoker method |
|---|---|---|
| Read | IReadable.ReadAsync |
ExecuteAsync(DriverCapability.Read, host, …) |
| Write | IWritable.WriteAsync |
ExecuteWriteAsync(host, isIdempotent, …) — honors WriteIdempotentAttribute (#143) |
| CreateMonitoredItems / DeleteMonitoredItems | ISubscribable.SubscribeAsync/UnsubscribeAsync |
ExecuteAsync(DriverCapability.Subscribe, host, …) |
| HistoryRead (raw / processed / at-time / events) | IHistoryProvider.*Async |
ExecuteAsync(DriverCapability.HistoryRead, host, …) |
| ConditionRefresh / Acknowledge | IAlarmSource.*Async |
via AlarmSurfaceInvoker (fans out per host) |
The host name fed to the invoker comes from IPerCallHostResolver.ResolveHost(fullReference) when the driver implements it (multi-host drivers: AB CIP, Modbus with per-device options). Single-host drivers fall back to DriverInstanceId, preserving pre-Phase-6.1 pipeline-key semantics (decision #144).
Redundancy
Redundancy.Enabled = true on the ServerInstance activates the RedundancyCoordinator + ServiceLevelCalculator (src/ZB.MOM.WW.OtOpcUa.Server/Redundancy/). Standard OPC UA redundancy nodes (Server/ServerRedundancy/RedundancySupport, ServerUriArray, Server/ServiceLevel) are populated on startup; ServiceLevel recomputes whenever any driver's DriverHealth changes. The apply-lease mechanism prevents two instances from concurrently applying a generation. See docs/Redundancy.md.
Server class hierarchy
OtOpcUaServer extends StandardServer
CreateMasterNodeManager— Iterates_driverHost.RegisteredDriverIds, builds oneDriverNodeManagerper driver with its ownCapabilityInvoker+ resilience options (tier fromDriverTypeRegistry, per-instance JSON overrides fromDriverInstance.ResilienceConfigviaDriverResilienceOptionsParser). The managers are wrapped in aMasterNodeManagerwith no additional core managers.OnServerStarted— HooksSessionManager.ImpersonateUserfor LDAP auth. Redundancy + server-capability population happens viaOpcUaApplicationHost.LoadServerProperties— ManufacturerOtOpcUa, ProductOtOpcUa.Server, ProductUriurn:OtOpcUa:Server.
ServerCapabilities
OpcUaApplicationHost populates Server/ServerCapabilities with StandardUA2017, en locale, 100 ms MinSupportedSampleRate, 4 MB message caps, and per-operation limits (1000 per Read/Write/Browse/TranslateBrowsePaths/MonitoredItems/HistoryRead; 0 for MethodCall/NodeManagement/HistoryUpdate).
Certificate handling
Certificate stores default to %LOCALAPPDATA%\OPC Foundation\pki\ (directory-based):
| Store | Path suffix |
|---|---|
| Own | pki/own |
| Trusted issuers | pki/issuer |
| Trusted peers | pki/trusted |
| Rejected | pki/rejected |
Security.AutoAcceptClientCertificates (default true) and RejectSHA1Certificates (default true) are honored. The server certificate is always created — even for None-only deployments — because UserName token encryption needs it.
Key source files
src/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OtOpcUaServer.cs—StandardServersubclass +ImpersonateUserhooksrc/ZB.MOM.WW.OtOpcUa.Server/OpcUa/DriverNodeManager.cs— per-driverCustomNodeManager2+ dispatch surfacesrc/ZB.MOM.WW.OtOpcUa.Server/OpcUa/OpcUaApplicationHost.cs— programmaticApplicationConfiguration+ lifecyclesrc/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs— driver registrationsrc/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs— Polly pipeline entry pointsrc/ZB.MOM.WW.OtOpcUa.Core/Authorization/— Phase 6.2 permission trie + evaluatorsrc/ZB.MOM.WW.OtOpcUa.Server/Security/AuthorizationGate.cs— stack-to-evaluator bridge