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.4 KiB
Incremental Sync
Two distinct change-detection paths feed the running server: driver-backend rediscovery (Galaxy's time_of_last_deploy, TwinCAT's symbol-version-changed, OPC UA Client's upstream namespace change) and generation-level config publishes from the Admin UI. Both flow into re-runs of ITagDiscovery.DiscoverAsync, but they originate differently.
Driver-backend rediscovery — IRediscoverable
Drivers whose backend has a native change signal implement IRediscoverable (src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs):
public interface IRediscoverable
{
event EventHandler<RediscoveryEventArgs>? OnRediscoveryNeeded;
}
public sealed record RediscoveryEventArgs(string Reason, string? ScopeHint);
The driver fires the event with a reason string (for the diagnostic log) and an optional scope hint — a non-null hint lets Core scope the rebuild surgically to that subtree; null means "the whole address space may have changed".
Drivers that implement the capability today:
- Galaxy — polls
galaxy.time_of_last_deployin the Galaxy repository DB and fires on change. This is Galaxy-internal change detection, not the platform-wide mechanism. - TwinCAT — observes ADS symbol-version-changed notifications (
0x0702). - OPC UA Client — subscribes to the upstream server's
Server/NamespaceArraychange notifications.
Static drivers (Modbus, S7, AB CIP, AB Legacy, FOCAS) do not implement IRediscoverable — their tags only change when a new generation is published from the Config DB. Core sees absence of the interface and skips change-detection wiring for those drivers (decision #54).
Config-DB generation publishes
Tag-set changes authored in the Admin UI (UNS edits, CSV imports, driver-config edits) accumulate in a draft generation and commit via sp_PublishGeneration. The delta between the currently-published generation and the proposed next one is computed by sp_ComputeGenerationDiff, which drives:
- The DiffViewer in Admin (
src/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/DiffViewer.razor) so operators can preview what will change before clicking Publish. - The 409-on-stale-draft flow (decision #161) — a UNS drag-reorder preview carries a
DraftRevisionTokenso Confirm returns409 Conflict / refresh-requiredif the draft advanced between preview and commit.
After publish, the server's generation applier invokes IDriver.ReinitializeAsync(driverConfigJson, ct) on every driver whose DriverInstance.DriverConfig row changed in the new generation. Reinitialize is the in-process recovery path for Tier A/B drivers; if it fails the driver is marked DriverState.Faulted and its nodes go Bad quality — but the server process stays running. See docs/v2/driver-stability.md.
Drivers whose discovery depends on Config DB state (Modbus register maps, S7 DBs, AB CIP tag lists) re-run their discovery inside ReinitializeAsync; Core then diffs the new node set against the current address space.
Rebuild flow
When a rediscovery is triggered (by either source), GenericDriverNodeManager re-runs ITagDiscovery.DiscoverAsync into the same CapturingBuilder it used at first build. The new node set is diffed against the current:
- Diff — full-name comparison of the new
DriverAttributeInfoset against the existing_variablesByFullRefmap. Added / removed / modified references are partitioned. - Snapshot subscriptions — before teardown, Core captures the current monitored-item ref-counts for every affected reference so subscriptions can be replayed after rebuild.
- Teardown — removed / modified variable nodes are deleted via
CustomNodeManager2.DeleteNode. Driver-side subscriptions for those references are unwound viaISubscribable.UnsubscribeAsync. - Rebuild — added / modified references get fresh
BaseDataVariableStatenodes via the standardIAddressSpaceBuilder.Variable(...)path. Alarm-flagged references re-register theirIAlarmConditionSinkthroughCapturingBuilder. - Restore subscriptions — for every captured reference that still exists after rebuild, Core re-opens the driver subscription and restores the original ref-count.
Exceptions during teardown are swallowed per decision #12 — a driver throw must not leave the node tree half-deleted.
Scope hint
When RediscoveryEventArgs.ScopeHint is non-null (e.g. a folder path), Core restricts the diff to that subtree. This matters for Galaxy Platform-scoped deployments where a time_of_last_deploy advance may only affect one platform's subtree, and for OPC UA Client where an upstream change may be localized. Null scope falls back to a full-tree diff.
Active subscriptions survive rebuild
Subscriptions for unchanged references stay live across rebuilds — their ref-count map is not disturbed. Clients monitoring a stable tag never see a data-change gap during a deploy, only clients monitoring a tag that was genuinely removed see the subscription drop.
Key source files
src/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs— backend-change capabilitysrc/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs— discovery orchestrationsrc/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs—ReinitializeAsynccontractsrc/ZB.MOM.WW.OtOpcUa.Admin/Services/GenerationService.cs— publish-flow driverdocs/v2/config-db-schema.md—sp_PublishGeneration+sp_ComputeGenerationDiffdocs/v2/admin-ui.md— DiffViewer + draft-revision-token flow