From 9cad9ed0fc7b48ce37194dbbb98ef2a674105455 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 7 Jul 2026 12:38:39 -0400 Subject: [PATCH] docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src Adds /// where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean. --- .../CommandBase.cs | 3 + .../Commands/AcknowledgeCommand.cs | 5 +- .../Commands/AlarmsCommand.cs | 4 - .../Commands/ConfirmCommand.cs | 5 +- .../Commands/DisableCommand.cs | 5 +- .../Commands/EnableCommand.cs | 5 +- .../Commands/RedundancyCommand.cs | 1 - .../Commands/ShelveCommand.cs | 5 +- .../Commands/WriteCommand.cs | 4 - .../DefaultApplicationConfigurationFactory.cs | 4 +- .../Adapters/DefaultEndpointDiscovery.cs | 6 +- .../Adapters/DefaultSessionAdapter.cs | 2 +- .../Adapters/DefaultSessionFactory.cs | 9 +- .../Adapters/DefaultSubscriptionAdapter.cs | 2 +- .../IApplicationConfigurationFactory.cs | 1 + .../Adapters/IEndpointDiscovery.cs | 1 + .../Adapters/ISessionAdapter.cs | 10 + .../Adapters/ISubscriptionAdapter.cs | 3 + .../ClientStoragePaths.cs | 11 +- .../IOpcUaClientService.cs | 13 + .../OpcUaClientService.cs | 5 +- .../OpcUaClientServiceFactory.cs | 3 +- .../Services/AvaloniaUiDispatcher.cs | 3 +- .../Services/ISettingsService.cs | 1 + .../Services/JsonSettingsService.cs | 9 +- .../Services/SynchronousUiDispatcher.cs | 3 +- .../ViewModels/AlarmsViewModel.cs | 1 + .../ViewModels/BrowseTreeViewModel.cs | 1 + .../ViewModels/SubscriptionsViewModel.cs | 5 + .../Views/ShelveAlarmWindow.axaml.cs | 2 + .../ClusterRoleInfo.cs | 16 +- .../ZB.MOM.WW.OtOpcUa.Cluster/RoleParser.cs | 1 + .../ServiceCollectionExtensions.cs | 2 + .../Browsing/IBrowseSession.cs | 8 + .../Browsing/IDriverBrowser.cs | 1 + .../Engines/IVirtualTagEvaluator.cs | 6 +- .../Interfaces/IAdminOperationsClient.cs | 1 + .../Interfaces/IFleetDiagnosticsClient.cs | 1 + .../Observability/OtOpcUaTelemetry.cs | 2 + .../OpcUa/DeferredAddressSpaceSink.cs | 76 ++--- .../OpcUa/DeferredServiceLevelPublisher.cs | 3 +- .../OpcUa/IServiceLevelPublisher.cs | 3 +- .../Types/CorrelationId.cs | 3 + .../Types/EquipmentScriptPaths.cs | 1 + .../Entities/ClusterNode.cs | 4 +- .../Entities/ClusterNodeCredential.cs | 2 +- .../Entities/DriverHostStatus.cs | 4 +- .../Entities/DriverInstance.cs | 6 +- .../Entities/Equipment.cs | 10 +- .../Entities/EquipmentImportBatch.cs | 6 +- .../Entities/ExternalIdReservation.cs | 2 +- .../Entities/LdapGroupRoleMapping.cs | 2 +- .../Entities/Namespace.cs | 2 +- .../Entities/NodeAcl.cs | 2 +- .../Entities/Script.cs | 2 +- .../Entities/ScriptedAlarm.cs | 8 +- .../Entities/ScriptedAlarmState.cs | 6 +- .../Entities/ServerCluster.cs | 2 +- .../Entities/Tag.cs | 2 +- .../Entities/UnsArea.cs | 2 +- .../Entities/UnsLine.cs | 2 +- .../Entities/VirtualTag.cs | 9 +- .../Enums/AdminRole.cs | 4 +- .../Enums/CredentialKind.cs | 2 +- .../Enums/NamespaceKind.cs | 2 +- .../Enums/ReservationKind.cs | 2 +- .../LocalCache/GenerationSealedCache.cs | 4 +- .../LocalCache/ILocalConfigCache.cs | 4 +- .../LocalCache/LiteDbConfigCache.cs | 23 +- .../LocalCache/ResilientConfigReader.cs | 4 +- .../OtOpcUaConfigDbContext.cs | 7 +- .../Services/ILdapGroupRoleMappingService.cs | 6 +- .../Services/LdapGroupRoleMappingService.cs | 19 +- .../Validation/DraftValidator.cs | 12 +- .../DataValueSnapshot.cs | 4 - .../DriverAttributeInfo.cs | 6 +- .../DriverCapability.cs | 4 +- .../DriverHealth.cs | 2 +- .../DriverTier.cs | 4 +- .../DriverTypeRegistry.cs | 17 +- .../EquipmentTagRefResolver.cs | 1 + .../Historian/IHistorianDataSource.cs | 12 +- .../Historian/IHistorizationOutbox.cs | 4 + .../Historian/NullHistorianDataSource.cs | 7 +- .../IAddressSpaceBuilder.cs | 9 +- .../IAlarmSource.cs | 3 + .../IDriver.cs | 10 +- .../IDriverConfigEditor.cs | 2 +- .../IDriverFactory.cs | 8 +- .../IDriverHealthPublisher.cs | 4 + .../IDriverProbe.cs | 4 + .../IDriverSupervisor.cs | 9 +- .../IHistoryProvider.cs | 6 +- .../IHostConnectivityProbe.cs | 1 + .../IPerCallHostResolver.cs | 4 +- .../IReadable.cs | 2 +- .../IRediscoverable.cs | 2 +- .../ITagDiscovery.cs | 3 +- .../IWritable.cs | 3 +- .../OpcUaOperation.cs | 4 +- .../PollGroupEngine.cs | 11 +- .../WriteIdempotentAttribute.cs | 2 +- .../AlarmHistorianEvent.cs | 4 +- .../IAlarmHistorianSink.cs | 7 +- .../SqliteStoreAndForwardSink.cs | 91 ++---- .../AlarmConditionState.cs | 8 +- .../IAlarmStateStore.cs | 4 +- .../MessageTemplate.cs | 5 +- .../Part9StateMachine.cs | 1 - .../ScriptedAlarmDefinition.cs | 8 +- .../ScriptedAlarmEngine.cs | 99 +++---- .../ScriptedAlarmSource.cs | 2 +- .../AlarmPredicateContext.cs | 5 - .../ScriptContext.cs | 9 +- .../CompiledScriptCache.cs | 18 +- .../DependencyExtractor.cs | 10 +- .../ForbiddenTypeAnalyzer.cs | 71 +++-- .../ScriptEvaluator.cs | 20 +- .../ScriptSandbox.cs | 13 +- .../TimedScriptEvaluator.cs | 4 +- .../DependencyGraph.cs | 2 + .../IHistoryWriter.cs | 4 +- .../ITagUpstreamSource.cs | 2 + .../VirtualTagEngine.cs | 47 +-- .../VirtualTagSource.cs | 30 +- .../Authorization/AuthorizationDecision.cs | 5 +- .../Authorization/IPermissionEvaluator.cs | 3 +- .../Authorization/NodeScope.cs | 2 +- .../Authorization/PermissionTrie.cs | 3 +- .../Authorization/PermissionTrieBuilder.cs | 8 +- .../Authorization/PermissionTrieCache.cs | 7 +- .../Authorization/TriePermissionEvaluator.cs | 14 +- .../Authorization/UserAuthorizationState.cs | 10 +- .../Hosting/DriverFactoryRegistry.cs | 4 +- .../Hosting/DriverFactoryRegistryAdapter.cs | 7 +- .../Hosting/DriverHost.cs | 9 +- .../Observability/DriverHealthReport.cs | 2 + .../Observability/LogContextEnricher.cs | 3 +- .../OpcUa/EquipmentNodeWalker.cs | 15 +- .../OpcUa/GenericDriverNodeManager.cs | 24 +- .../OpcUa/IdentificationFolderBuilder.cs | 4 +- .../Resilience/AlarmSurfaceInvoker.cs | 9 +- .../Resilience/CapabilityInvoker.cs | 9 +- .../Resilience/DriverResilienceOptions.cs | 8 +- .../DriverResilienceOptionsParser.cs | 3 +- .../DriverResiliencePipelineBuilder.cs | 3 +- .../DriverResilienceStatusTracker.cs | 2 + .../Stability/MemoryRecycle.cs | 2 +- .../Stability/MemoryTracking.cs | 10 +- .../Stability/ScheduledRecycleScheduler.cs | 2 +- .../Stability/WedgeDetector.cs | 2 +- .../AbCipCommandBase.cs | 10 +- .../Commands/ProbeCommand.cs | 4 +- .../Commands/ReadCommand.cs | 7 +- .../Commands/SubscribeCommand.cs | 10 +- .../Commands/WriteCommand.cs | 2 +- .../Commands/ProbeCommand.cs | 2 +- .../Commands/ReadCommand.cs | 3 +- .../Commands/SubscribeCommand.cs | 4 - .../Commands/WriteCommand.cs | 2 - .../SnapshotFormatter.cs | 3 + .../Commands/ProbeCommand.cs | 7 - .../Commands/ReadCommand.cs | 8 +- .../Commands/SubscribeCommand.cs | 18 +- .../Commands/WriteCommand.cs | 18 +- .../FocasCommandBase.cs | 3 +- .../Commands/ProbeCommand.cs | 8 +- .../Commands/ReadCommand.cs | 8 +- .../Commands/SubscribeCommand.cs | 14 +- .../Commands/WriteCommand.cs | 10 - .../ModbusCommandBase.cs | 3 +- .../Commands/ProbeCommand.cs | 4 +- .../Commands/ReadCommand.cs | 12 +- .../Commands/SubscribeCommand.cs | 15 - .../Commands/WriteCommand.cs | 8 +- .../S7CommandBase.cs | 3 +- .../Commands/BrowseCommand.cs | 6 +- .../Commands/SubscribeCommand.cs | 4 +- .../Commands/WriteCommand.cs | 3 +- .../TwinCATCommandBase.cs | 20 +- .../TwinCATTagCommandBase.cs | 4 +- .../AbCipDriverOptions.cs | 14 +- .../AbCipAlarmProjection.cs | 8 +- .../AbCipDataTypeExtensions.cs | 2 +- .../AbCipDriver.cs | 185 +++--------- .../AbCipDriverFactoryExtensions.cs | 2 +- .../AbCipHostAddress.cs | 2 +- .../AbCipStatusMapper.cs | 3 + .../AbCipSystemTagFilter.cs | 9 +- .../AbCipTagPath.cs | 1 + .../AbCipTemplateCache.cs | 1 + .../AbCipUdtMemberLayout.cs | 5 +- .../AbCipUdtReadPlanner.cs | 3 +- .../IAbCipTagEnumerator.cs | 12 +- .../IAbCipTagRuntime.cs | 16 +- .../IAbCipTemplateReader.cs | 2 + .../LibplctagTagEnumerator.cs | 8 +- .../LibplctagTagRuntime.cs | 61 +--- .../LibplctagTemplateReader.cs | 24 +- .../PlcFamilies/AbCipPlcFamilyProfile.cs | 1 + .../AbLegacyDriverOptions.cs | 2 +- .../AbLegacyEquipmentTagParser.cs | 8 - .../AbLegacyPlcFamilyProfile.cs | 1 + .../AbLegacyAddress.cs | 2 + .../AbLegacyDriver.cs | 146 ++-------- .../AbLegacyDriverFactoryExtensions.cs | 2 +- .../IAbLegacyTagRuntime.cs | 7 + .../LibplctagLegacyTagRuntime.cs | 17 +- .../FocasDriverOptions.cs | 5 +- .../FocasAlarmProjection.cs | 2 +- .../FocasDriver.cs | 100 +++---- .../FocasDriverFactoryExtensions.cs | 2 + .../IFocasClient.cs | 28 +- .../SynchronizedFocasClient.cs | 2 +- .../Wire/FocasConstants.cs | 2 +- .../Wire/FocasWireClient.cs | 43 +++ .../Wire/FocasWireProtocol.cs | 2 + .../Wire/WireFocasClient.cs | 96 ++----- .../GalaxyBrowseSession.cs | 26 +- .../GalaxyDriverBrowser.cs | 21 +- .../GalaxyDriverOptions.cs | 4 +- .../GalaxySecretRef.cs | 3 +- .../Browse/AlarmRefBuilder.cs | 1 + .../Browse/DataTypeMap.cs | 3 +- .../Browse/DeployWatcher.cs | 1 + .../Browse/GalaxyDiscoverer.cs | 1 + .../Browse/GatewayGalaxyDeployWatchSource.cs | 5 +- .../Browse/GatewayGalaxyHierarchySource.cs | 5 +- .../Browse/IGalaxyDeployWatchSource.cs | 1 + .../Browse/IGalaxyHierarchySource.cs | 1 + .../GalaxyDriver.cs | 124 ++++---- .../GalaxyDriverFactoryExtensions.cs | 2 + .../GalaxyDriverProbe.cs | 4 + .../Health/HostConnectivityForwarder.cs | 4 +- .../Health/HostStatusAggregator.cs | 1 + .../Health/PerPlatformProbeWatcher.cs | 1 + .../Runtime/EventPump.cs | 4 +- .../Runtime/GalaxyMxSession.cs | 3 + .../Runtime/GalaxySubscriptionHandle.cs | 2 +- .../Runtime/GatewayGalaxyAlarmAcknowledger.cs | 6 +- .../Runtime/GatewayGalaxyAlarmFeed.cs | 4 +- .../Runtime/GatewayGalaxyDataWriter.cs | 6 +- .../Runtime/GatewayGalaxySubscriber.cs | 15 +- .../Runtime/IGalaxyAlarmAcknowledger.cs | 1 + .../Runtime/IGalaxyDataWriter.cs | 1 + .../Runtime/IGalaxySubscriber.cs | 3 + .../Runtime/MxAccessSeverityMapper.cs | 1 + .../Runtime/MxValueDecoder.cs | 1 + .../Runtime/ReconnectSupervisor.cs | 3 +- .../Runtime/SubscriptionRegistry.cs | 20 +- .../Runtime/TracedGalaxyDataWriter.cs | 6 +- .../Runtime/TracedGalaxySubscriber.cs | 19 +- .../GatewayHistorianDataSource.cs | 16 +- .../HistorianGatewayClientAdapter.cs | 8 - .../IHistorianGatewayClient.cs | 32 +++ .../Mapping/HistorianTypeMapper.cs | 1 + .../Recorder/GatewayHistorianValueWriter.cs | 1 + .../HistorizationOutboxEntrySerializer.cs | 4 + .../DirectLogicAddress.cs | 21 +- .../MelsecAddress.cs | 4 + .../ModbusAddressParser.cs | 37 ++- .../ModbusDataType.cs | 2 +- .../ModbusFamily.cs | 4 +- .../ModbusModiconAddress.cs | 1 + .../ModbusDriverOptions.cs | 29 +- .../ModbusEquipmentTagParser.cs | 5 +- .../ModbusAutoProhibition.cs | 6 +- .../ModbusDriver.cs | 155 +++------- .../ModbusDriverFactoryExtensions.cs | 13 +- .../ModbusTcpTransport.cs | 16 +- .../OpcUaClientBrowseSession.cs | 29 +- .../OpcUaClientDriverBrowser.cs | 7 +- .../NamespaceMap.cs | 6 + .../OpcUaClientDriver.cs | 272 +++++++----------- .../S7DriverOptions.cs | 6 +- .../S7CpuTypeMap.cs | 3 + .../ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs | 132 ++++----- .../S7DriverFactoryExtensions.cs | 16 +- .../TwinCATDataType.cs | 5 +- .../TwinCATDriverOptions.cs | 2 +- .../AdsTwinCATClient.cs | 94 ++---- .../ITwinCATClient.cs | 11 +- .../TwinCATDriver.cs | 140 ++++----- .../TwinCATDriverFactoryExtensions.cs | 17 +- .../TwinCATStatusMapper.cs | 4 +- .../TwinCATSystemSymbolFilter.cs | 1 + .../Browsing/BrowseSessionReaper.cs | 2 + .../Browsing/BrowseSessionRegistry.cs | 8 + .../Browsing/IBrowserSessionService.cs | 17 ++ .../Certificates/CertificateStoreManager.cs | 3 + .../Clients/AdminOperationsClient.cs | 27 +- .../Clients/AdminProbeService.cs | 1 + .../Clients/FleetDiagnosticsClient.cs | 5 +- .../Clients/ServiceCollectionExtensions.cs | 1 + .../Drivers/Pickers/AbLegacyAddressBuilder.cs | 5 + .../Drivers/Pickers/FocasAddressBuilder.cs | 4 + .../Drivers/Pickers/ModbusAddressBuilder.cs | 5 + .../Drivers/Pickers/S7AddressBuilder.cs | 2 + .../Shared/Drivers/ResilienceFormModel.cs | 22 ++ .../EndpointRouteBuilderExtensions.cs | 2 + .../Hubs/AlertSignalRBridge.cs | 1 + .../Hubs/DriverStatusHub.cs | 4 + .../Hubs/DriverStatusSignalRBridge.cs | 1 + .../Hubs/HubRouteBuilderExtensions.cs | 1 + .../Hubs/HubServiceCollectionExtensions.cs | 2 + .../Hubs/IDriverStatusSnapshotStore.cs | 8 + .../Hubs/IInProcessBroadcaster.cs | 1 + .../Hubs/ScriptLogSignalRBridge.cs | 1 + .../ScriptAnalysis/IScriptTagCatalog.cs | 4 + .../ScriptAnalysis/ScriptAnalysisEndpoints.cs | 3 + .../ScriptAnalysis/ScriptAnalysisService.cs | 27 +- .../Uns/EquipmentInput.cs | 2 +- .../Uns/IUnsTreeService.cs | 38 +-- .../Uns/TagEditors/AbCipTagConfigModel.cs | 4 + .../Uns/TagEditors/AbLegacyTagConfigModel.cs | 4 + .../Uns/TagEditors/FocasTagConfigModel.cs | 4 + .../Uns/TagEditors/ModbusTagConfigModel.cs | 4 + .../Uns/TagEditors/NativeAlarmModel.cs | 6 + .../TagEditors/OpcUaClientTagConfigModel.cs | 3 + .../Uns/TagEditors/S7TagConfigModel.cs | 4 + .../Uns/TagEditors/TagArrayConfig.cs | 9 + .../Uns/TagEditors/TagConfigEditorMap.cs | 2 + .../Uns/TagEditors/TagConfigJson.cs | 26 ++ .../Uns/TagEditors/TagConfigValidator.cs | 3 + .../Uns/TagEditors/TagHistorizeConfig.cs | 6 + .../Uns/TagEditors/TwinCATTagConfigModel.cs | 4 + .../ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagInput.cs | 2 +- .../ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsNode.cs | 5 + .../Uns/UnsTreeService.cs | 18 +- .../Uns/VirtualTagInput.cs | 2 +- .../AdminOperations/AdminOperationsActor.cs | 2 +- .../AdminOperations/ConfigComposer.cs | 2 + .../Audit/AuditWriterActor.cs | 1 + .../Coordinators/ConfigPublishCoordinator.cs | 11 +- .../Fleet/FleetStatusBroadcaster.cs | 2 +- .../Drivers/DriverFactoryBootstrap.cs | 2 + .../Engines/RoslynVirtualTagEvaluator.cs | 6 +- .../Health/HealthEndpoints.cs | 3 + .../Observability/ObservabilityExtensions.cs | 2 + .../OpcUa/LdapOpcUaUserAuthenticator.cs | 5 +- .../OpcUa/OtOpcUaServerHostedService.cs | 3 + src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs | 6 +- .../AddressSpaceApplier.cs | 36 +-- .../AddressSpaceComposer.cs | 41 +-- .../AddressSpacePlan.cs | 9 +- .../OpcUaApplicationHost.cs | 4 + .../OtOpcUaNodeManager.cs | 96 ++----- .../SdkAddressSpaceSink.cs | 57 +--- .../SdkServiceLevelPublisher.cs | 3 +- .../Security/IOpcUaUserAuthenticator.cs | 6 +- .../Security/OpcUaDataPlaneRoles.cs | 2 +- .../Drivers/CapturingAddressSpaceBuilder.cs | 4 + .../Drivers/DeploymentArtifact.cs | 12 +- .../Drivers/DriverHostActor.cs | 21 +- .../Drivers/DriverInstanceActor.cs | 12 +- .../Drivers/DriverSpawnPlan.cs | 1 + .../Health/DbHealthProbeActor.cs | 3 +- .../Historian/HistorianAdapterActor.cs | 5 +- .../OpcUa/OpcUaPublishActor.cs | 9 +- .../DependencyMuxTagUpstreamSource.cs | 15 +- .../EfAlarmConditionStateStore.cs | 10 +- .../ScriptedAlarms/ScriptedAlarmHostActor.cs | 1 + .../Scripting/DpsScriptLogPublisher.cs | 7 +- .../ServiceCollectionExtensions.cs | 2 + .../VirtualTags/VirtualTagActor.cs | 1 + .../VirtualTags/VirtualTagHostActor.cs | 1 + .../Auth/AutoLoginAuthenticationHandler.cs | 5 + .../CookieAuthenticationStateProvider.cs | 3 +- .../Endpoints/AuthEndpoints.cs | 2 +- .../Jwt/JwtTokenService.cs | 1 + .../Ldap/LdapOptions.cs | 5 +- .../Ldap/OtOpcUaGroupRoleMapper.cs | 8 +- .../Ldap/OtOpcUaLdapAuthService.cs | 6 +- .../Ldap/RoleMapper.cs | 1 + .../ServiceCollectionExtensions.cs | 3 +- 375 files changed, 1899 insertions(+), 2493 deletions(-) diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/CommandBase.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/CommandBase.cs index 7aca25ff..86bddc6e 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/CommandBase.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/CommandBase.cs @@ -67,11 +67,13 @@ public abstract class CommandBase : ICommand /// Executes the command-specific workflow against the configured OPC UA endpoint. /// /// The CLI console used for output and cancellation handling. + /// A task that represents the asynchronous operation. public abstract ValueTask ExecuteAsync(IConsole console); /// /// Creates a from the common command options. /// + /// The connection settings built from the current command options. protected ConnectionSettings CreateConnectionSettings() { var securityMode = SecurityModeMapper.FromString(Security); @@ -97,6 +99,7 @@ public abstract class CommandBase : ICommand /// and returns both the service and the connection info. /// /// The cancellation token that aborts connection setup for the command. + /// The connected client service along with its connection info. protected async Task<(IOpcUaClientService Service, ConnectionInfo Info)> CreateServiceAndConnectAsync( CancellationToken ct) { diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/AcknowledgeCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/AcknowledgeCommand.cs index 0d685a81..7a407fc7 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/AcknowledgeCommand.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/AcknowledgeCommand.cs @@ -36,10 +36,7 @@ public class AcknowledgeCommand : CommandBase [CommandOption("comment", 'c', Description = "Operator comment for the acknowledgment")] public string Comment { get; init; } = string.Empty; - /// - /// Connects to the server and acknowledges the specified alarm condition. - /// - /// The CLI console used for output and cancellation handling. + /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/AlarmsCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/AlarmsCommand.cs index e2821984..401fe121 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/AlarmsCommand.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/AlarmsCommand.cs @@ -38,10 +38,6 @@ public class AlarmsCommand : CommandBase [CommandOption("refresh", Description = "Request a ConditionRefresh after subscribing")] public bool Refresh { get; init; } - /// - /// Connects to the server, subscribes to alarm events, and streams operator-facing alarm state changes to the console. - /// - /// The CLI console used for output and cancellation handling. /// public override async ValueTask ExecuteAsync(IConsole console) { diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ConfirmCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ConfirmCommand.cs index da7893b9..75c89c6a 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ConfirmCommand.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ConfirmCommand.cs @@ -36,10 +36,7 @@ public class ConfirmCommand : CommandBase [CommandOption("comment", 'c', Description = "Operator comment for the confirmation")] public string Comment { get; init; } = string.Empty; - /// - /// Connects to the server and confirms the specified acknowledged alarm condition. - /// - /// The CLI console used for output and cancellation handling. + /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/DisableCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/DisableCommand.cs index a9619bbb..30a138b6 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/DisableCommand.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/DisableCommand.cs @@ -24,10 +24,7 @@ public class DisableCommand : CommandBase [CommandOption("node", 'n', Description = "Condition node ID of the alarm to disable", IsRequired = true)] public string NodeId { get; init; } = default!; - /// - /// Connects to the server and disables the specified alarm condition. - /// - /// The CLI console used for output and cancellation handling. + /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/EnableCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/EnableCommand.cs index 20d3446e..4789da47 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/EnableCommand.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/EnableCommand.cs @@ -24,10 +24,7 @@ public class EnableCommand : CommandBase [CommandOption("node", 'n', Description = "Condition node ID of the alarm to enable", IsRequired = true)] public string NodeId { get; init; } = default!; - /// - /// Connects to the server and enables the specified alarm condition. - /// - /// The CLI console used for output and cancellation handling. + /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/RedundancyCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/RedundancyCommand.cs index 3f1a3459..b4e479f9 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/RedundancyCommand.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/RedundancyCommand.cs @@ -15,7 +15,6 @@ public class RedundancyCommand : CommandBase { } - /// Connects to the server and prints redundancy mode, service level, and partner-server identity data. /// public override async ValueTask ExecuteAsync(IConsole console) { diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ShelveCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ShelveCommand.cs index 752726ac..89d63875 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ShelveCommand.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/ShelveCommand.cs @@ -38,10 +38,7 @@ public class ShelveCommand : CommandBase [CommandOption("duration", 'd', Description = "Shelving duration in seconds (must be > 0; in seconds, converted to milliseconds for the OPC UA call; required for --kind Timed)")] public double DurationSeconds { get; init; } - /// - /// Connects to the server and shelves or unshelves the specified alarm condition. - /// - /// The CLI console used for output and cancellation handling. + /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/WriteCommand.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/WriteCommand.cs index f31a3a34..63f88933 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/WriteCommand.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI/Commands/WriteCommand.cs @@ -31,10 +31,6 @@ public class WriteCommand : CommandBase [CommandOption("value", 'v', Description = "Value to write", IsRequired = true)] public string Value { get; init; } = default!; - /// - /// Connects to the server, converts the supplied value to the node's current data type, and issues the write. - /// - /// The CLI console used for output and cancellation handling. /// public override async ValueTask ExecuteAsync(IConsole console) { diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultApplicationConfigurationFactory.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultApplicationConfigurationFactory.cs index 18e683a7..3165f322 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultApplicationConfigurationFactory.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultApplicationConfigurationFactory.cs @@ -12,9 +12,7 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi { private static readonly ILogger Logger = Log.ForContext(); - /// Creates an OPC UA application configuration from the provided connection settings. - /// The connection settings to use. - /// Token to cancel the operation. + /// public async Task CreateAsync(ConnectionSettings settings, CancellationToken ct) { // Resolve the canonical PKI path lazily on first use so constructing a diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultEndpointDiscovery.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultEndpointDiscovery.cs index a8719460..0add4d9c 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultEndpointDiscovery.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultEndpointDiscovery.cs @@ -11,10 +11,7 @@ internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery { private static readonly ILogger Logger = Log.ForContext(); - /// Selects an OPC UA endpoint matching the requested security mode. - /// The application configuration. - /// The endpoint URL to query. - /// The requested message security mode. + /// public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, MessageSecurityMode requestedMode) { @@ -53,6 +50,7 @@ internal static class EndpointSelector /// Thrown when no endpoint matches ; the message lists the /// security mode + policy combinations the server returned so operators can diagnose mismatches. /// + /// The best-matching endpoint, with its URL host rewritten to match if needed. public static EndpointDescription SelectBest( IEnumerable allEndpoints, string endpointUrl, diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSessionAdapter.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSessionAdapter.cs index 4588381e..41440590 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSessionAdapter.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSessionAdapter.cs @@ -109,7 +109,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter byte[] continuationPoint, CancellationToken ct) { // Pass the caller's token so a cancelled browse does not block on the continuation - // round-trip (Client.Shared-015). + // round-trip. var (_, nextCp, nextRefs) = await _session.BrowseNextAsync(null, false, continuationPoint, ct); return (nextCp, nextRefs ?? []); } diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSessionFactory.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSessionFactory.cs index 4f44bd30..44dc77f8 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSessionFactory.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSessionFactory.cs @@ -11,14 +11,7 @@ internal sealed class DefaultSessionFactory : ISessionFactory { private static readonly ILogger Logger = Log.ForContext(); - /// Creates a new OPC UA session. - /// The OPC UA application configuration. - /// The endpoint description to connect to. - /// The name for the session. - /// The session timeout in milliseconds. - /// The user identity for the session. - /// The cancellation token. - /// An adapter wrapping the created session. + /// public async Task CreateSessionAsync( ApplicationConfiguration config, EndpointDescription endpoint, diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSubscriptionAdapter.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSubscriptionAdapter.cs index c96e9128..b5b3c4ab 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSubscriptionAdapter.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/DefaultSubscriptionAdapter.cs @@ -105,7 +105,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter { try { - // Forward the caller's token so the delete can be cancelled (Client.Shared-014). + // Forward the caller's token so the delete can be cancelled. await _subscription.DeleteAsync(true, ct); } catch (Exception ex) diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/IApplicationConfigurationFactory.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/IApplicationConfigurationFactory.cs index 29d8ae30..f03aa7f8 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/IApplicationConfigurationFactory.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/IApplicationConfigurationFactory.cs @@ -13,5 +13,6 @@ internal interface IApplicationConfigurationFactory /// /// The connection settings to configure. /// Cancellation token for the operation. + /// The validated . Task CreateAsync(ConnectionSettings settings, CancellationToken ct = default); } \ No newline at end of file diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/IEndpointDiscovery.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/IEndpointDiscovery.cs index 4f802729..b3fe2456 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/IEndpointDiscovery.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/IEndpointDiscovery.cs @@ -14,6 +14,7 @@ internal interface IEndpointDiscovery /// The OPC UA application configuration. /// The endpoint URL to discover. /// The requested message security mode. + /// The best-matching endpoint description, with its URL hostname rewritten to match when they differ. EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, MessageSecurityMode requestedMode); } \ No newline at end of file diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/ISessionAdapter.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/ISessionAdapter.cs index d19a285c..8af4e721 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/ISessionAdapter.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/ISessionAdapter.cs @@ -58,6 +58,7 @@ internal interface ISessionAdapter : IDisposable /// /// The node whose current runtime value should be read. /// The cancellation token that aborts the server read if the client cancels the request. + /// The current data value read from the server. Task ReadValueAsync(NodeId nodeId, CancellationToken ct = default); /// @@ -66,6 +67,7 @@ internal interface ISessionAdapter : IDisposable /// The node whose value should be updated. /// The typed OPC UA data value to write to the server. /// The cancellation token that aborts the write if the client cancels the request. + /// The status code returned by the server for the write operation. Task WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct = default); /// @@ -75,6 +77,7 @@ internal interface ISessionAdapter : IDisposable /// The starting node for the hierarchical browse. /// The node classes that should be returned to the caller. /// The cancellation token that aborts the browse request. + /// A continuation point (or null when complete) and the references found. Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync( NodeId nodeId, uint nodeClassMask = 0, CancellationToken ct = default); @@ -83,6 +86,7 @@ internal interface ISessionAdapter : IDisposable /// /// The continuation token returned by a prior browse result page. /// The cancellation token that aborts the browse-next request. + /// The next continuation point (or null when exhausted) and the additional references. Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync( byte[] continuationPoint, CancellationToken ct = default); @@ -91,6 +95,7 @@ internal interface ISessionAdapter : IDisposable /// /// The node to inspect for child objects or variables. /// The cancellation token that aborts the child lookup. + /// true when the node has at least one forward hierarchical child reference; otherwise false. Task HasChildrenAsync(NodeId nodeId, CancellationToken ct = default); /// @@ -101,6 +106,7 @@ internal interface ISessionAdapter : IDisposable /// The inclusive end of the requested history window. /// The maximum number of raw samples to return to the client. /// The cancellation token that aborts the history read. + /// The raw historical data values for the requested window. Task> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues, CancellationToken ct = default); @@ -113,6 +119,7 @@ internal interface ISessionAdapter : IDisposable /// The OPC UA aggregate function to evaluate over the history window. /// The processing interval, in milliseconds, for each aggregate bucket. /// The cancellation token that aborts the aggregate history read. + /// The processed/aggregate data values for the requested window. Task> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, NodeId aggregateId, double intervalMs, CancellationToken ct = default); @@ -121,6 +128,7 @@ internal interface ISessionAdapter : IDisposable /// /// The requested publishing interval for monitored items on the new subscription. /// The cancellation token that aborts subscription creation. + /// The newly created subscription adapter. Task CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct = default); /// @@ -130,11 +138,13 @@ internal interface ISessionAdapter : IDisposable /// The method node to invoke. /// The ordered input arguments supplied to the server method call. /// The cancellation token that aborts the method invocation. + /// The output arguments returned by the method call. Task?> CallMethodAsync(NodeId objectId, NodeId methodId, object[] inputArguments, CancellationToken ct = default); /// /// Closes the underlying session gracefully before the adapter is disposed or replaced during failover. /// /// The cancellation token that aborts the close request. + /// A task that represents the asynchronous operation. Task CloseAsync(CancellationToken ct = default); } diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/ISubscriptionAdapter.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/ISubscriptionAdapter.cs index 21aeeb0c..afedf4c7 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/ISubscriptionAdapter.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/Adapters/ISubscriptionAdapter.cs @@ -28,6 +28,7 @@ internal interface ISubscriptionAdapter : IDisposable /// /// The client handle returned when the monitored item was created. /// The cancellation token that aborts the monitored-item removal. + /// A task that represents the asynchronous operation. Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct = default); /// @@ -46,11 +47,13 @@ internal interface ISubscriptionAdapter : IDisposable /// Requests a condition refresh for this subscription. /// /// The cancellation token that aborts the condition refresh request. + /// A task that represents the asynchronous operation. Task ConditionRefreshAsync(CancellationToken ct = default); /// /// Removes all monitored items and deletes the subscription. /// /// The cancellation token that aborts subscription deletion. + /// A task that represents the asynchronous operation. Task DeleteAsync(CancellationToken ct = default); } diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs index d1309527..a1882ecc 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/ClientStoragePaths.cs @@ -2,8 +2,8 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared; /// /// Resolves the canonical under-LocalAppData folder for the shared OPC UA client's PKI -/// store + persisted settings. Renamed from LmxOpcUaClient to OtOpcUaClient -/// in task #208; a one-shot migration shim moves a pre-rename folder in place on first +/// store + persisted settings. Renamed from LmxOpcUaClient to OtOpcUaClient; +/// a one-shot migration shim moves a pre-rename folder in place on first /// resolution so existing developer boxes keep their trusted server certs + saved /// connection settings on upgrade. /// @@ -14,10 +14,10 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared; /// public static class ClientStoragePaths { - /// Canonical client folder name. Post-#208. + /// Canonical client folder name. public const string CanonicalFolderName = "OtOpcUaClient"; - /// Pre-#208 folder name. Used only by the migration shim. + /// Legacy folder name, used only by the migration shim. public const string LegacyFolderName = "LmxOpcUaClient"; private static readonly Lock _migrationLock = new(); @@ -28,6 +28,7 @@ public static class ClientStoragePaths /// one-shot legacy-folder migration before returning so callers that depend on this /// path (PKI store, settings file) find their existing state at the canonical name. /// + /// The absolute path to the client's canonical top-level folder. public static string GetRoot() { var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); @@ -37,6 +38,7 @@ public static class ClientStoragePaths } /// Subfolder for the application's PKI store — used by both CLI + UI. + /// The absolute path to the client's PKI store folder. public static string GetPkiPath() => Path.Combine(GetRoot(), "pki"); /// @@ -45,6 +47,7 @@ public static class ClientStoragePaths /// folder existed + was moved to canonical, false when no migration was needed or /// canonical was already present. /// + /// true when a legacy folder existed and was moved to canonical; otherwise false. public static bool TryRunLegacyMigration() { var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs index 42726851..e0d31b9e 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/IOpcUaClientService.cs @@ -24,12 +24,14 @@ public interface IOpcUaClientService : IDisposable /// /// The endpoint, security, and authentication settings used to establish the session. /// The cancellation token that aborts the connect workflow. + /// The connection metadata for the newly established session. Task ConnectAsync(ConnectionSettings settings, CancellationToken ct = default); /// /// Disconnects from the active OPC UA endpoint and tears down subscriptions owned by the client. /// /// The cancellation token that aborts disconnect cleanup. + /// A task that represents the asynchronous operation. Task DisconnectAsync(CancellationToken ct = default); /// @@ -37,6 +39,7 @@ public interface IOpcUaClientService : IDisposable /// /// The node whose value should be retrieved. /// The cancellation token that aborts the read request. + /// The current data value of the node. Task ReadValueAsync(NodeId nodeId, CancellationToken ct = default); /// @@ -45,6 +48,7 @@ public interface IOpcUaClientService : IDisposable /// The node whose value should be updated. /// The raw value supplied by the CLI or UI workflow. /// The cancellation token that aborts the write request. + /// The status code returned by the server for the write. Task WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default); /// @@ -52,6 +56,7 @@ public interface IOpcUaClientService : IDisposable /// /// The node to browse, or when omitted. /// The cancellation token that aborts the browse request. + /// The child nodes discovered under . Task> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default); /// @@ -60,6 +65,7 @@ public interface IOpcUaClientService : IDisposable /// The node whose value changes should be monitored. /// The monitored-item sampling and publishing interval in milliseconds. /// The cancellation token that aborts subscription creation. + /// A task that represents the asynchronous operation. Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default); /// @@ -67,6 +73,7 @@ public interface IOpcUaClientService : IDisposable /// /// The node whose live-data subscription should be removed. /// The cancellation token that aborts the unsubscribe request. + /// A task that represents the asynchronous operation. Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default); /// @@ -75,18 +82,21 @@ public interface IOpcUaClientService : IDisposable /// The event source to monitor, or the server object when omitted. /// The publishing interval in milliseconds for the alarm subscription. /// The cancellation token that aborts alarm subscription creation. + /// A task that represents the asynchronous operation. Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default); /// /// Removes the active alarm subscription. /// /// The cancellation token that aborts alarm subscription cleanup. + /// A task that represents the asynchronous operation. Task UnsubscribeAlarmsAsync(CancellationToken ct = default); /// /// Requests retained alarm conditions again so a client can repopulate its alarm list after reconnecting. /// /// The cancellation token that aborts the condition refresh request. + /// A task that represents the asynchronous operation. Task RequestConditionRefreshAsync(CancellationToken ct = default); /// @@ -169,6 +179,7 @@ public interface IOpcUaClientService : IDisposable /// The inclusive end of the requested history range. /// The maximum number of raw values to return. /// The cancellation token that aborts the history read. + /// The raw historical samples in the requested range, up to . Task> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, int maxValues = 1000, CancellationToken ct = default); @@ -181,6 +192,7 @@ public interface IOpcUaClientService : IDisposable /// The aggregate function the operator selected for processed history. /// The processing interval, in milliseconds, for each aggregate bucket. /// The cancellation token that aborts the processed history request. + /// The aggregated historical values for each processing interval in the requested range. Task> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default); @@ -188,6 +200,7 @@ public interface IOpcUaClientService : IDisposable /// Reads redundancy status data such as redundancy mode, service level, and partner endpoint URIs. /// /// The cancellation token that aborts redundancy inspection. + /// The current redundancy status of the connected server. Task GetRedundancyInfoAsync(CancellationToken ct = default); /// diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs index a461430f..7408431c 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientService.cs @@ -24,7 +24,6 @@ public sealed class OpcUaClientService : IOpcUaClientService // Serialises SubscribeAlarmsAsync / UnsubscribeAlarmsAsync so concurrent callers // cannot both pass the _alarmSubscription == null check and create duplicate event // subscriptions. Capacity 1 = at most one waiter; async-friendly (no thread-block). - // (Client.Shared-013) private readonly SemaphoreSlim _alarmSubscribeSemaphore = new(1, 1); // Track active data subscriptions for replay after failover @@ -137,7 +136,7 @@ public sealed class OpcUaClientService : IOpcUaClientService var endpointUrl = _session?.EndpointUrl ?? _settings?.EndpointUrl ?? string.Empty; // Snapshot the subscription adapter references under the lock so a concurrent - // RunFailoverAsync that nulls _dataSubscription / _alarmSubscription (Client.Shared-012) + // RunFailoverAsync that nulls _dataSubscription / _alarmSubscription // cannot invalidate the references between the null-check and the DeleteAsync call. ISubscriptionAdapter? dataSubscription; ISubscriptionAdapter? alarmSubscription; @@ -303,7 +302,7 @@ public sealed class OpcUaClientService : IOpcUaClientService ThrowIfDisposed(); ThrowIfNotConnected(); - // Serialise check-and-create under the semaphore (Client.Shared-013): concurrent + // Serialise check-and-create under the semaphore: concurrent // callers cannot both pass the null-check and create duplicate alarm subscriptions. // The semaphore is async-friendly; the subscription I/O runs inside the critical section. await _alarmSubscribeSemaphore.WaitAsync(ct); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientServiceFactory.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientServiceFactory.cs index e5107664..152526d4 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientServiceFactory.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.Shared/OpcUaClientServiceFactory.cs @@ -5,8 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared; /// public sealed class OpcUaClientServiceFactory : IOpcUaClientServiceFactory { - /// Creates a new OPC UA client service instance with production adapters. - /// A new OpcUaClientService instance. + /// public IOpcUaClientService Create() { return new OpcUaClientService(); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/AvaloniaUiDispatcher.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/AvaloniaUiDispatcher.cs index 13275830..e74c0cb2 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/AvaloniaUiDispatcher.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/AvaloniaUiDispatcher.cs @@ -7,8 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services; /// public sealed class AvaloniaUiDispatcher : IUiDispatcher { - /// Posts an action to the Avalonia UI thread for execution. - /// The action to execute on the UI thread. + /// public void Post(Action action) { Dispatcher.UIThread.Post(action); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/ISettingsService.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/ISettingsService.cs index f95c56c0..08023ed1 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/ISettingsService.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/ISettingsService.cs @@ -6,6 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services; public interface ISettingsService { /// Loads user settings from persistent storage. + /// The loaded user settings. UserSettings Load(); /// Saves user settings to persistent storage. /// The settings to save. diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/JsonSettingsService.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/JsonSettingsService.cs index 2f793cad..28afb137 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/JsonSettingsService.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/JsonSettingsService.cs @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services; /// public sealed class JsonSettingsService : ISettingsService { - // ClientStoragePaths.GetRoot runs the one-shot legacy-folder migration so pre-#208 + // ClientStoragePaths.GetRoot runs the one-shot legacy-folder migration so pre-rename // developer boxes pick up their existing settings.json on first launch post-rename. private static readonly string SettingsDir = ClientStoragePaths.GetRoot(); @@ -19,8 +19,8 @@ public sealed class JsonSettingsService : ISettingsService WriteIndented = true }; - /// Loads user settings from the settings file. - /// The loaded user settings, or a new default instance if load fails. + /// + // Returns a new default instance when the settings file is missing or fails to load/parse. public UserSettings Load() { try @@ -37,8 +37,7 @@ public sealed class JsonSettingsService : ISettingsService } } - /// Saves user settings to the settings file. - /// The user settings to save. + /// public void Save(UserSettings settings) { try diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/SynchronousUiDispatcher.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/SynchronousUiDispatcher.cs index 06c5f6bc..7744cc3e 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/SynchronousUiDispatcher.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Services/SynchronousUiDispatcher.cs @@ -6,8 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services; /// public sealed class SynchronousUiDispatcher : IUiDispatcher { - /// Executes the action synchronously on the calling thread. - /// The action to execute. + /// public void Post(Action action) { action(); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs index ffdaf8fa..09baf3a6 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/AlarmsViewModel.cs @@ -256,6 +256,7 @@ public partial class AlarmsViewModel : ObservableObject /// /// Returns the monitored node ID for persistence, or null if not subscribed. /// + /// The monitored node ID, or null if not subscribed. public string? GetAlarmSourceNodeId() { return IsSubscribed ? MonitoredNodeIdText : null; diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/BrowseTreeViewModel.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/BrowseTreeViewModel.cs index 6ca20cbf..c3c60b8e 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/BrowseTreeViewModel.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/BrowseTreeViewModel.cs @@ -30,6 +30,7 @@ public class BrowseTreeViewModel : ObservableObject /// /// Loads root nodes by browsing with a null parent. /// + /// A task that represents the asynchronous operation. public async Task LoadRootsAsync() { var results = await _service.BrowseAsync(); diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/SubscriptionsViewModel.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/SubscriptionsViewModel.cs index 612ed752..381719a4 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/SubscriptionsViewModel.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/ViewModels/SubscriptionsViewModel.cs @@ -143,6 +143,7 @@ public partial class SubscriptionsViewModel : ObservableObject /// /// The node ID to subscribe to from the browse tree or persisted settings. /// The monitored-item interval, in milliseconds, for the subscription. + /// A task that represents the asynchronous operation. public async Task AddSubscriptionForNodeAsync(string nodeIdStr, int intervalMs = 1000) { if (!IsConnected || string.IsNullOrWhiteSpace(nodeIdStr)) return; @@ -176,6 +177,7 @@ public partial class SubscriptionsViewModel : ObservableObject /// The root node whose variables should be subscribed recursively. /// The node class of the starting node so variables can be subscribed immediately. /// The monitored-item interval, in milliseconds, used for created subscriptions. + /// A task that represents the asynchronous operation. public Task AddSubscriptionRecursiveAsync(string nodeIdStr, string nodeClass, int intervalMs = 1000) { return AddSubscriptionRecursiveAsync(nodeIdStr, nodeClass, intervalMs, maxDepth: 10, currentDepth: 0); @@ -211,6 +213,7 @@ public partial class SubscriptionsViewModel : ObservableObject /// /// Returns the node IDs of all active subscriptions for persistence. /// + /// The node IDs of all currently active subscriptions. public List GetSubscribedNodeIds() { return ActiveSubscriptions.Select(s => s.NodeId).ToList(); @@ -220,6 +223,7 @@ public partial class SubscriptionsViewModel : ObservableObject /// Restores subscriptions from a saved list of node IDs. /// /// The node IDs persisted from a prior UI session. + /// A task that represents the asynchronous operation. public async Task RestoreSubscriptionsAsync(IEnumerable nodeIds) { foreach (var nodeId in nodeIds) @@ -232,6 +236,7 @@ public partial class SubscriptionsViewModel : ObservableObject /// /// The node ID the operator wants to write. /// The raw text value entered by the operator. + /// A tuple indicating whether the write succeeded and a human-readable status message. public async Task<(bool Success, string Message)> ValidateAndWriteAsync(string nodeIdStr, string rawValue) { try diff --git a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Views/ShelveAlarmWindow.axaml.cs b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Views/ShelveAlarmWindow.axaml.cs index 8fe28063..5ee4182c 100644 --- a/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Views/ShelveAlarmWindow.axaml.cs +++ b/src/Client/ZB.MOM.WW.OtOpcUa.Client.UI/Views/ShelveAlarmWindow.axaml.cs @@ -20,6 +20,8 @@ public partial class ShelveAlarmWindow : Window } /// Creates the shelve dialog for an alarm. + /// The view model used to issue the shelve/unshelve command. + /// The alarm event being shelved. public ShelveAlarmWindow(AlarmsViewModel alarmsVm, AlarmEventViewModel alarm) { InitializeComponent(); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ClusterRoleInfo.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ClusterRoleInfo.cs index 02e42090..22feed72 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ClusterRoleInfo.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ClusterRoleInfo.cs @@ -43,20 +43,16 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable _subscriber = system.ActorOf(Props.Create(() => new SubscriberActor(this)), "clusterroleinfo-subscriber"); } - /// Gets the local cluster node identifier. + /// public CommonsNodeId LocalNode => _localNode; - /// Gets the set of roles assigned to the local node. + /// public IReadOnlySet LocalRoles => _localRoles; - /// Checks if the local node has a specific role. - /// The role name to check. - /// True if the local node has the specified role; otherwise false. + /// public bool HasRole(string role) => _localRoles.Contains(role); - /// Gets all cluster members that have a specific role. - /// The role name. - /// A read-only list of node IDs with the specified role. + /// public IReadOnlyList MembersWithRole(string role) { lock (_lock) @@ -68,9 +64,7 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable } } - /// Gets the current leader node for a specific role. - /// The role name. - /// The node ID of the current role leader, or null if no leader is elected. + /// public CommonsNodeId? RoleLeader(string role) { lock (_lock) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/RoleParser.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/RoleParser.cs index a2de2290..d417ff78 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/RoleParser.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/RoleParser.cs @@ -9,6 +9,7 @@ public static class RoleParser /// Parses a comma-separated string of role names into a validated array. /// The raw role string to parse. + /// The distinct, lower-cased, validated role names. public static string[] Parse(string? raw) { if (string.IsNullOrWhiteSpace(raw)) return Array.Empty(); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ServiceCollectionExtensions.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ServiceCollectionExtensions.cs index ad99b780..2d4fa44c 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ServiceCollectionExtensions.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Cluster/ServiceCollectionExtensions.cs @@ -20,6 +20,7 @@ public static class ServiceCollectionExtensions /// /// The service collection to configure. /// The application configuration containing cluster options. + /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaCluster(this IServiceCollection services, IConfiguration configuration) { services.AddOptions() @@ -47,6 +48,7 @@ public static class ServiceCollectionExtensions /// /// The Akka configuration builder to configure. /// The service provider for resolving cluster options. + /// The same builder, for chaining. public static AkkaConfigurationBuilder WithOtOpcUaClusterBootstrap( this AkkaConfigurationBuilder builder, IServiceProvider serviceProvider) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/IBrowseSession.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/IBrowseSession.cs index a470b7e2..cb146c78 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/IBrowseSession.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/IBrowseSession.cs @@ -16,14 +16,22 @@ public interface IBrowseSession : IAsyncDisposable DateTime LastUsedUtc { get; } /// Returns the top-level browse nodes. + /// Cancellation token for the operation. + /// The top-level browse nodes. Task> RootAsync(CancellationToken cancellationToken); /// Returns the direct children of the node identified by /// . + /// The node whose direct children are returned. + /// Cancellation token for the operation. + /// The direct children of the node. Task> ExpandAsync(string nodeId, CancellationToken cancellationToken); /// Returns the attributes of the node identified by . /// Empty for drivers whose tree is uniform (OPC UA Client). Galaxy uses this to populate /// the attribute side-panel after the user selects an object. + /// The node whose attributes are returned. + /// Cancellation token for the operation. + /// The attributes of the node. Task> AttributesAsync(string nodeId, CancellationToken cancellationToken); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/IDriverBrowser.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/IDriverBrowser.cs index 80f80375..b6a1570c 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/IDriverBrowser.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Browsing/IDriverBrowser.cs @@ -15,5 +15,6 @@ public interface IDriverBrowser /// Driver options serialized as JSON; same shape the runtime /// driver would consume. /// Cancellation for the connect phase only. + /// The opened browse session. Task OpenAsync(string configJson, CancellationToken cancellationToken); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Engines/IVirtualTagEvaluator.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Engines/IVirtualTagEvaluator.cs index 023b48cf..78d5ff4a 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Engines/IVirtualTagEvaluator.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Engines/IVirtualTagEvaluator.cs @@ -43,11 +43,7 @@ public sealed class NullVirtualTagEvaluator : IVirtualTagEvaluator { public static readonly NullVirtualTagEvaluator Instance = new(); private NullVirtualTagEvaluator() { } - /// Returns for every evaluation. - /// The virtual tag identifier (ignored). - /// The expression string (ignored). - /// The variable dependencies (ignored). - /// Always returns . + /// public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary dependencies) => VirtualTagEvalResult.NoChange; } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Interfaces/IAdminOperationsClient.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Interfaces/IAdminOperationsClient.cs index a32c5b45..de7fed79 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Interfaces/IAdminOperationsClient.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Interfaces/IAdminOperationsClient.cs @@ -53,5 +53,6 @@ public interface IAdminOperationsClient /// Expected reply type. /// The message to send. /// Cancellation token (caller-controlled timeout). + /// The reply of type received from the admin singleton. Task AskAsync(object message, CancellationToken ct); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Interfaces/IFleetDiagnosticsClient.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Interfaces/IFleetDiagnosticsClient.cs index 262cf795..213d68de 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Interfaces/IFleetDiagnosticsClient.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Interfaces/IFleetDiagnosticsClient.cs @@ -11,5 +11,6 @@ public interface IFleetDiagnosticsClient /// Gets diagnostics for the specified node. /// The node ID to retrieve diagnostics for. /// The cancellation token. + /// The diagnostics snapshot for the requested node. Task GetDiagnosticsAsync(NodeId nodeId, CancellationToken ct); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Observability/OtOpcUaTelemetry.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Observability/OtOpcUaTelemetry.cs index 33553180..668521fc 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Observability/OtOpcUaTelemetry.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Observability/OtOpcUaTelemetry.cs @@ -69,6 +69,7 @@ public static class OtOpcUaTelemetry /// null when no listener is attached so the call site stays cheap on undecorated builds. /// /// The deployment identifier to tag the span with. + /// The started activity, or null when no listener is attached. public static Activity? StartDeployApplySpan(string deploymentId) { var activity = ActivitySource.StartActivity("otopcua.deploy.apply", ActivityKind.Internal); @@ -77,6 +78,7 @@ public static class OtOpcUaTelemetry } /// Span wrapping a full OPC UA address-space rebuild (AddressSpace plan → apply). + /// The started activity, or null when no listener is attached. public static Activity? StartAddressSpaceRebuildSpan() => ActivitySource.StartActivity("otopcua.opcua.address_space_rebuild", ActivityKind.Internal); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs index 9d5664e9..701f8a55 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs @@ -22,85 +22,49 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical public void SetSink(IOpcUaAddressSpaceSink? sink) => _inner = sink ?? NullOpcUaAddressSpaceSink.Instance; - /// Writes a value to the OPC UA address space through the inner sink. - /// The node ID of the variable. - /// The value to write. - /// The OPC UA quality value. - /// The source timestamp in UTC. + /// public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) => _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc); - /// Writes a full alarm-condition state through the inner sink. - /// The node ID of the alarm condition. - /// The full condition state to project onto the node. - /// The source timestamp in UTC. + /// public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) => _inner.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc); - /// Materialises a real Part 9 alarm-condition node through the inner sink. - /// The alarm node ID (== ScriptedAlarmId). - /// The equipment folder node ID the condition parents under. - /// The human-readable condition name. - /// The domain alarm type. - /// The domain severity. - /// True for a driver-fed (native) equipment-tag alarm; false (default) for a scripted alarm. + /// public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) => _inner.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); - /// Ensures a folder exists in the address space through the inner sink. - /// The node ID of the folder. - /// The node ID of the parent folder, or null for root. - /// The display name of the folder. + /// public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) => _inner.EnsureFolder(folderNodeId, parentNodeId, displayName); - /// Ensures a variable exists in the address space through the inner sink. - /// The node ID of the variable. - /// The node ID of the parent folder, or null for root. - /// The display name of the variable. - /// The OPC UA data type of the variable. - /// When true the node is created read/write; otherwise read-only. - /// null ⇒ not historized; non-null ⇒ create Historizing with the - /// HistoryRead access bit and register the historian tagname. - /// When true the node is created as a 1-D array; when false (default) scalar. - /// The declared length of the 1-D array when is true. + /// public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) => _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength); - /// Rebuilds the address space through the inner sink. + /// public void RebuildAddressSpace() => _inner.RebuildAddressSpace(); - /// Announces a runtime NodeAdded model-change (discovered-node injection) through the inner sink. - /// The node under which discovered nodes were added. + /// public void RaiseNodesAddedModelChange(string affectedNodeId) => _inner.RaiseNodesAddedModelChange(affectedNodeId); - /// Forwards an in-place tag-attribute update (F10b) to the inner sink when it supports the - /// surgical capability. Returns false otherwise — before the real SdkAddressSpaceSink is - /// swapped in (inner is still the null sink), or any inner sink that isn't surgical — so the caller - /// (AddressSpaceApplier) falls back to a full rebuild. Without this forward the surgical optimization is - /// inert on every driver-role host, because actors inject THIS wrapper, not the inner sink. ALL six args - /// (including the FB-7 DataType/array-shape ones) MUST be forwarded — a partial forward silently drops the - /// shape update on every driver-role host. - /// The node ID of the variable to update in place. - /// Whether the node should be read/write. - /// null ⇒ not historized; non-null ⇒ Historizing + historian binding. - /// The OPC UA built-in data type name to apply in place. - /// When true the node becomes a 1-D array; when false scalar. - /// The declared length of the 1-D array when is true. - /// True when the inner sink applied the update; false when it lacks the capability or the node is missing. + /// + // Forwards to the inner sink when it supports the surgical capability; returns false otherwise — + // before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any inner + // sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild. + // Without this forward the surgical optimization is inert on every driver-role host, because + // actors inject THIS wrapper, not the inner sink. ALL six args (including the DataType/array-shape + // ones) MUST be forwarded — a partial forward silently drops the shape update on every driver-role host. public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength) => _inner is ISurgicalAddressSpaceSink surgical && surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength); - /// Forwards an in-place folder-display-name update (OpcUaServer-001 — UNS Area / Line - /// rename) to the inner sink when it supports the surgical capability. Returns false otherwise — - /// before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any - /// inner sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild. - /// Without this forward the rename-refresh optimization is inert on every driver-role host, because - /// actors inject THIS wrapper, not the inner sink. - /// The folder node id whose display name to update in place. - /// The new display name to apply. - /// True when the inner sink applied the update; false when it lacks the capability or the folder is missing. + /// + // Forwards to the inner sink when it supports the surgical capability; returns false otherwise — + // before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any inner + // sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild. + // Without this forward the rename-refresh optimization is inert on every driver-role host, because + // actors inject THIS wrapper, not the inner sink. public bool UpdateFolderDisplayName(string folderNodeId, string displayName) => _inner is ISurgicalAddressSpaceSink surgical && surgical.UpdateFolderDisplayName(folderNodeId, displayName); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredServiceLevelPublisher.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredServiceLevelPublisher.cs index eb227979..f567d9ec 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredServiceLevelPublisher.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredServiceLevelPublisher.cs @@ -16,7 +16,6 @@ public sealed class DeferredServiceLevelPublisher : IServiceLevelPublisher public void SetInner(IServiceLevelPublisher? inner) => _inner = inner ?? NullServiceLevelPublisher.Instance; - /// Publishes a service level value to the inner publisher. - /// The service level to publish. + /// public void Publish(byte serviceLevel) => _inner.Publish(serviceLevel); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IServiceLevelPublisher.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IServiceLevelPublisher.cs index c464670a..59530579 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IServiceLevelPublisher.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/IServiceLevelPublisher.cs @@ -23,7 +23,6 @@ public sealed class NullServiceLevelPublisher : IServiceLevelPublisher /// Gets the last published service level value. public byte LastPublished { get; private set; } - /// Records the service level value without publishing. - /// The service level value (0-255). + /// public void Publish(byte serviceLevel) => LastPublished = serviceLevel; } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/CorrelationId.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/CorrelationId.cs index fbf9dc06..8964350b 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/CorrelationId.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/CorrelationId.cs @@ -3,15 +3,18 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Types; public readonly record struct CorrelationId(Guid Value) { /// Creates a new CorrelationId with a randomly generated GUID. + /// A new, randomly generated . public static CorrelationId NewId() => new(Guid.NewGuid()); /// public override string ToString() => Value.ToString("N"); /// Parses a lowercase hex string without hyphens into a CorrelationId. /// The string to parse. + /// The parsed . public static CorrelationId Parse(string s) => new(Guid.ParseExact(s, "N")); /// Attempts to parse a lowercase hex string without hyphens into a CorrelationId. /// The string to parse, or null. /// The resulting CorrelationId if parsing succeeds. + /// true if was successfully parsed. public static bool TryParse(string? s, out CorrelationId id) { if (Guid.TryParseExact(s, "N", out var g)) { id = new CorrelationId(g); return true; } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs index e0cd6f2d..d29894ca 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Commons/Types/EquipmentScriptPaths.cs @@ -41,6 +41,7 @@ public static class EquipmentScriptPaths /// True when the source uses the {{equip}} token anywhere. /// The script source to scan. + /// True if contains the {{equip}} token; otherwise false. public static bool ContainsEquipToken(string? source) => !string.IsNullOrEmpty(source) && source.Contains(EquipToken, StringComparison.Ordinal); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs index c7368c93..91a9bf02 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNode.cs @@ -20,7 +20,7 @@ public sealed class ClusterNode /// /// OPC UA ApplicationUri — MUST be unique per node per OPC UA spec. Clients pin trust here. - /// Fleet-wide unique index enforces no two nodes share a value (decision #86). + /// Fleet-wide unique index enforces no two nodes share a value. /// Stored explicitly, NOT derived from at runtime — silent rewrite on /// hostname change would break all client trust. /// @@ -31,7 +31,7 @@ public sealed class ClusterNode /// /// Per-node override JSON keyed by DriverInstanceId, merged onto cluster-level DriverConfig - /// at apply time. Minimal by intent (decision #81). Nullable when no overrides exist. + /// at apply time. Minimal by intent. Nullable when no overrides exist. /// public string? DriverConfigOverridesJson { get; set; } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNodeCredential.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNodeCredential.cs index 6a767e9a..26265d2e 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNodeCredential.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ClusterNodeCredential.cs @@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// /// Authenticates a to the central config DB. -/// Per decision #83 — credentials bind to NodeId, not ClusterId. +/// Credentials bind to NodeId, not ClusterId. /// public sealed class ClusterNodeCredential { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverHostStatus.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverHostStatus.cs index 7b1eb167..a8719af0 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverHostStatus.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverHostStatus.cs @@ -11,8 +11,8 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// /// /// -/// Closes the data-layer piece of LMX follow-up #7 (per-AppEngine Admin dashboard -/// drill-down). The publisher hosted service on the Server side subscribes to every +/// Supports the per-AppEngine Admin dashboard drill-down. The publisher hosted +/// service on the Server side subscribes to every /// registered driver's OnHostStatusChanged and upserts rows on transitions + /// periodic liveness heartbeats. advances on every /// heartbeat so the Admin UI can flag stale rows from a crashed Server. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs index 39db08a4..77fb4284 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/DriverInstance.cs @@ -14,7 +14,7 @@ public sealed class DriverInstance /// /// Logical FK to . Same-cluster binding enforced by - /// sp_ValidateDraft per decision #122: Namespace.ClusterId must equal DriverInstance.ClusterId. + /// sp_ValidateDraft: Namespace.ClusterId must equal DriverInstance.ClusterId. /// public required string NamespaceId { get; set; } @@ -27,12 +27,12 @@ public sealed class DriverInstance /// Gets or sets a value indicating whether this driver instance is enabled. public bool Enabled { get; set; } = true; - /// Schemaless per-driver-type JSON config. Validated against registered JSON schema at draft-publish time (decision #91). + /// Schemaless per-driver-type JSON config. Validated against registered JSON schema at draft-publish time. public required string DriverConfig { get; set; } /// /// Optional per-instance overrides for the Phase 6.1 shared Polly resilience pipeline. - /// Null = use the driver's tier defaults (decision #143). When populated, expected shape: + /// Null = use the driver's tier defaults. When populated, expected shape: /// /// { /// "bulkheadMaxConcurrent": 16, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs index 59d38caa..e6eef1c8 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Equipment.cs @@ -2,8 +2,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// /// UNS level-5 entity. Only for drivers in Equipment-kind namespaces. -/// Per decisions #109 (first-class), #116 (5-identifier model), #125 (system-generated EquipmentId), -/// #138–139 (OPC 40010 Identification fields as first-class columns). /// public sealed class Equipment { @@ -12,7 +10,7 @@ public sealed class Equipment /// /// System-generated stable internal logical ID. Format: 'EQ-' + first 12 hex chars of EquipmentUuid. - /// NEVER operator-supplied, NEVER in CSV imports, NEVER editable in Admin UI (decision #125). + /// NEVER operator-supplied, NEVER in CSV imports, NEVER editable in Admin UI. /// public required string EquipmentId { get; set; } @@ -34,7 +32,7 @@ public sealed class Equipment /// UNS level 5 segment, matches ^[a-z0-9-]{1,32}$. public required string Name { get; set; } - // Operator-facing / external-system identifiers (decision #116) + // Operator-facing / external-system identifiers /// Operator colloquial id (e.g. "machine_001"). Unique within cluster. Required. public required string MachineCode { get; set; } @@ -45,7 +43,7 @@ public sealed class Equipment /// SAP PM equipment id. Unique fleet-wide via . public string? SAPID { get; set; } - // OPC UA Companion Spec OPC 40010 Machinery Identification fields (decision #139). + // OPC UA Companion Spec OPC 40010 Machinery Identification fields. // All nullable so equipment can be added before identity is fully captured. /// Gets or sets the manufacturer name for this equipment. public string? Manufacturer { get; set; } @@ -66,7 +64,7 @@ public sealed class Equipment /// Gets or sets the device manual URI for this equipment. public string? DeviceManualUri { get; set; } - /// Nullable hook for future schemas-repo template ID (decision #112). + /// Nullable hook for future schemas-repo template ID. public string? EquipmentClassRef { get; set; } /// Gets or sets whether this equipment is enabled. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/EquipmentImportBatch.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/EquipmentImportBatch.cs index bd37b7d2..35c58cf2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/EquipmentImportBatch.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/EquipmentImportBatch.cs @@ -46,8 +46,8 @@ public sealed class EquipmentImportBatch } /// -/// One staged row under an . Mirrors the decision #117 -/// + decision #139 columns from the CSV importer's output + an +/// One staged row under an . Mirrors the required +/// + optional columns from the CSV importer's output + an /// flag + a string the preview modal /// renders. /// @@ -68,7 +68,6 @@ public sealed class EquipmentImportRow /// Gets or sets the reason this row was rejected, if applicable. public string? RejectReason { get; set; } - // Required (decision #117) /// Gets or sets the Z tag identifier. public required string ZTag { get; set; } @@ -93,7 +92,6 @@ public sealed class EquipmentImportRow /// Gets or sets the UNS line name. public required string UnsLineName { get; set; } - // Optional (decision #139 — OPC 40010 Identification) /// Gets or sets the manufacturer name. public string? Manufacturer { get; set; } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ExternalIdReservation.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ExternalIdReservation.cs index 934e9135..39164813 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ExternalIdReservation.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ExternalIdReservation.cs @@ -3,7 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// -/// Fleet-wide rollback-safe reservation of ZTag and SAPID. Per decision #124 — NOT generation-versioned. +/// Fleet-wide rollback-safe reservation of ZTag and SAPID. NOT generation-versioned. /// Exists outside generation flow specifically because old generations and disabled equipment can /// still hold the same external IDs; per-generation uniqueness indexes fail under rollback/re-enable. /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs index 4029edf9..99c87047 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/LdapGroupRoleMapping.cs @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// applies fleet-wide. /// /// -/// Per docs/v2/plan.md decisions #105 and #150 — this entity is control-plane +/// This entity is control-plane /// only. The OPC UA data-path evaluator does not read these rows; it reads /// joined directly against the session's resolved LDAP group /// memberships. Collapsing the two would let a user inherit tag permissions via an diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Namespace.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Namespace.cs index 55f53433..f7324fb2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Namespace.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Namespace.cs @@ -3,7 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// -/// OPC UA namespace served by a cluster. Generation-versioned per decision #123 — +/// OPC UA namespace served by a cluster. Generation-versioned — /// namespaces are content (affect what consumers see at the endpoint), not topology. /// public sealed class Namespace diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs index 537dfa4b..18a8ab60 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/NodeAcl.cs @@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// /// One ACL grant: an LDAP group gets a set of at a specific scope. -/// Generation-versioned per decision #130. See acl-design.md for evaluation algorithm. +/// Generation-versioned per decision. See acl-design.md for evaluation algorithm. /// public sealed class NodeAcl { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs index 17fa3ad9..7a1cda14 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Script.cs @@ -1,7 +1,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// -/// Per Phase 7 plan decision #8 — user-authored C# script source, referenced by +/// User-authored C# script source, referenced by /// and . One row per script, /// per generation. SourceHash is the compile-cache key. /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs index b53aab4d..f170c264 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarm.cs @@ -1,15 +1,15 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// -/// Per Phase 7 plan decisions #5, #13, #15 — a scripted OPC UA Part 9 alarm whose +/// A scripted OPC UA Part 9 alarm whose /// condition is the predicate referenced by /// . Materialized by Core.ScriptedAlarms as a /// concrete AlarmConditionType subtype per . /// /// /// -/// Message tokens ({TagPath}) resolved at emission time per plan decision #13. -/// (plan decision #15) gates whether transitions +/// Message tokens ({TagPath}) resolved at emission time. +/// gates whether transitions /// route through the Core.AlarmHistorian SQLite queue + Galaxy.Host to the Aveva /// Historian alarm schema. /// @@ -41,7 +41,7 @@ public sealed class ScriptedAlarm public required string PredicateScriptId { get; set; } /// - /// Plan decision #15 — when true, transitions route through the SQLite store-and-forward + /// When true, transitions route through the SQLite store-and-forward /// queue to the Aveva Historian. Defaults on for scripted alarms because they are the /// primary motivation for the historian sink; operator can disable per alarm. /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarmState.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarmState.cs index c199ac63..49a60311 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarmState.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ScriptedAlarmState.cs @@ -1,8 +1,8 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// -/// Per Phase 7 plan decision #14 — persistent runtime state for each scripted alarm. -/// Survives process restart so operators don't re-ack and ack history survives for +/// Persistent runtime state for each scripted alarm. Survives process restart so +/// operators don't re-ack and ack history survives for /// GxP / 21 CFR Part 11 compliance. Keyed on ScriptedAlarmId logically (not /// per-generation) because ack state follows the alarm's stable identity across /// generations — a Modified alarm keeps its ack history. @@ -24,7 +24,7 @@ public sealed class ScriptedAlarmState /// Logical FK — matches . One row per alarm identity. public required string ScriptedAlarmId { get; set; } - /// Enabled/Disabled. Persists across restart per plan decision #14. + /// Enabled/Disabled. Persists across restart. public required string EnabledState { get; set; } = "Enabled"; /// Unacknowledged / Acknowledged. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ServerCluster.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ServerCluster.cs index c05e5b36..192964f4 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ServerCluster.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/ServerCluster.cs @@ -14,7 +14,7 @@ public sealed class ServerCluster /// Gets or sets the display name for the server cluster. public required string Name { get; set; } - /// UNS level 1. Canonical org value: "zb" per decision #140. + /// UNS level 1. Canonical org value: "zb". public required string Enterprise { get; set; } /// UNS level 2, e.g. "warsaw-west". diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs index 7a84ae12..32fefab5 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/Tag.cs @@ -51,7 +51,7 @@ public sealed class Tag /// public required TagAccessLevel AccessLevel { get; set; } - /// Per decisions #44–45 — opt-in for write retry eligibility. + /// Opt-in for write retry eligibility. public bool WriteIdempotent { get; set; } /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsArea.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsArea.cs index 889da2d9..cb0ed18d 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsArea.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsArea.cs @@ -1,6 +1,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; -/// UNS level-3 segment. Generation-versioned per decision #115. +/// UNS level-3 segment. Generation-versioned. public sealed class UnsArea { /// Gets or sets the unique row identifier. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsLine.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsLine.cs index ee990fbf..3133ab57 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsLine.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/UnsLine.cs @@ -1,6 +1,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; -/// UNS level-4 segment. Generation-versioned per decision #115. +/// UNS level-4 segment. Generation-versioned. public sealed class UnsLine { /// Gets or sets the unique row identifier for this UNS line. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs index 5957dba9..9b8416fa 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Entities/VirtualTag.cs @@ -1,20 +1,19 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities; /// -/// Per Phase 7 plan decision #2 — a virtual (calculated) tag that lives in the -/// Equipment tree alongside driver tags. Value is produced by the -/// referenced by . +/// A virtual (calculated) tag that lives in the Equipment tree alongside driver tags. +/// Value is produced by the referenced by . /// /// /// /// is mandatory — virtual tags are always scoped to an -/// Equipment node per plan decision #2 (unified Equipment tree, not a separate +/// Equipment node (unified Equipment tree, not a separate /// /Virtual namespace). matches the shape used by /// Tag.DataType. /// /// /// and together realize -/// plan decision #3 (change + timer). At least one must produce evaluations; the +/// change + timer evaluation. At least one must produce evaluations; the /// Core.VirtualTags engine rejects an all-disabled tag at load time. /// /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs index affe524b..0210dd3f 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/AdminRole.cs @@ -8,14 +8,14 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums; /// /// /// -/// Per docs/v2/plan.md decision #150 the two concerns share zero runtime code path: +/// Per docs/v2/plan.md the two concerns share zero runtime code path: /// the control plane (Admin UI) consumes ; the /// data plane consumes rows directly. Having them in one /// table would collapse the distinction + let a user inherit tag permissions via their /// admin-role claim path. /// /// -/// Task 1.7 standardized the member names on the canonical control-plane role vocabulary +/// The member names were standardized on the canonical control-plane role vocabulary /// (ZB.MOM.WW.Auth CanonicalRole): ConfigViewer → Viewer, /// ConfigEditor → Designer, FleetAdmin → Administrator. The appsettings-only /// DriverOperator string role likewise became Operator. These members persist diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/CredentialKind.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/CredentialKind.cs index df5369e0..b139e1e4 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/CredentialKind.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/CredentialKind.cs @@ -1,6 +1,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums; -/// Credential kind for . Per decision #83. +/// Credential kind for . public enum CredentialKind { SqlLogin, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NamespaceKind.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NamespaceKind.cs index cc0f37b9..41969599 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NamespaceKind.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NamespaceKind.cs @@ -1,6 +1,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums; -/// OPC UA namespace kind per decision #107. One of each kind per cluster per generation. +/// OPC UA namespace kind. One of each kind per cluster per generation. public enum NamespaceKind { /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/ReservationKind.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/ReservationKind.cs index 936a17a1..88f8b0ff 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/ReservationKind.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/ReservationKind.cs @@ -1,6 +1,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums; -/// External-ID reservation kind. Per decision #124. +/// External-ID reservation kind. public enum ReservationKind { ZTag, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs index e152bfa5..220b9f9b 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/GenerationSealedCache.cs @@ -3,7 +3,7 @@ using LiteDB; namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache; /// -/// Generation-sealed LiteDB cache per docs/v2/plan.md decision #148 and Phase 6.1 +/// Generation-sealed LiteDB cache per docs/v2/plan.md and Phase 6.1 /// Stream D.1. Each published generation writes one read-only LiteDB file under /// <cache-root>/<clusterId>/<generationId>.db. A per-cluster /// CURRENT text file holds the currently-active generation id; it is updated @@ -32,7 +32,7 @@ public sealed class GenerationSealedCache // BsonMapper.Global is a process-wide singleton whose lazy per-type member registration is // not thread-safe across concurrently-constructed LiteDatabase instances; a seal racing a // read (or this cache racing LiteDbConfigCache) corrupts the global mapper, surfacing as - // "Member … not found on BsonMapper" or a bogus duplicate-_id insert — Configuration-012. + // "Member … not found on BsonMapper" or a bogus duplicate-_id insert. private static BsonMapper BuildMapper() { var mapper = new BsonMapper(); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ILocalConfigCache.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ILocalConfigCache.cs index fc92d9ec..9483de8d 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ILocalConfigCache.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ILocalConfigCache.cs @@ -2,7 +2,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache; /// /// Per-node local cache of the most-recently-applied generation(s). Used to bootstrap the -/// address space when the central DB is unreachable (decision #79 — degraded-but-running). +/// address space when the central DB is unreachable (degraded-but-running). /// /// /// Concurrency contract: implementations must serialize writes — specifically, @@ -21,10 +21,12 @@ public interface ILocalConfigCache /// Stores a generation snapshot in the local cache. /// The generation snapshot to store. /// The cancellation token. + /// A task that represents the asynchronous operation. Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default); /// Removes old generations, keeping only the most recent N. /// The cluster identifier. /// The number of latest generations to keep. /// The cancellation token. + /// A task that represents the asynchronous operation. Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/LiteDbConfigCache.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/LiteDbConfigCache.cs index 75b144b0..4a597b12 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/LiteDbConfigCache.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/LiteDbConfigCache.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache; /// LiteDB-backed . One file per node (default /// config_cache.db), one collection per snapshot. Corruption surfaces as /// on construction or read — callers should -/// delete and re-fetch from the central DB (decision #80). +/// delete and re-fetch from the central DB. /// public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable { @@ -17,7 +17,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable // instances. When several caches (this one + GenerationSealedCache) initialise in parallel // the global mapper races, surfacing as "Member ClusterId not found on BsonMapper" or a // bogus "duplicate key _id = 0" (the int auto-id mapping was lost so Insert writes a literal - // 0 twice) — Configuration-012. Give each database a private, pre-registered mapper so member + // 0 twice). Give each database a private, pre-registered mapper so member // resolution happens once, single-threaded, at construction and never touches the global. private static BsonMapper BuildMapper() { @@ -30,7 +30,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable private readonly ILiteCollection _col; // PutAsync is a find-then-insert/update; without serialization, two concurrent puts for the // same (ClusterId, GenerationId) can both observe `existing is null` and both Insert, - // producing duplicate rows (Configuration-005). Serialize writes through this semaphore so + // producing duplicate rows. Serialize writes through this semaphore so // the read-modify-write block is atomic for a given instance. LiteDB itself only locks the // page-level write, not the find-then-insert window. private readonly SemaphoreSlim _writeGate = new(initialCount: 1, maxCount: 1); @@ -60,9 +60,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable } } - /// Gets the most recent snapshot for the specified cluster. - /// The cluster ID. - /// Cancellation token. + /// public Task GetMostRecentAsync(string clusterId, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); @@ -73,15 +71,13 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable return Task.FromResult(snapshot); } - /// Stores a snapshot in the cache. - /// The snapshot to store. - /// Cancellation token. + /// public async Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); // Serialize the find-then-insert/update so concurrent callers do not observe a stale - // `existing is null` and both Insert (Configuration-005). LiteDB's per-call lock is - // not enough — the read and the write are independent calls. + // `existing is null` and both Insert. LiteDB's per-call lock is not enough — the + // read and the write are independent calls. await _writeGate.WaitAsync(ct).ConfigureAwait(false); try { @@ -104,10 +100,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable } } - /// Removes old generation snapshots, keeping only the latest ones. - /// The cluster ID. - /// Number of latest generations to keep. - /// Cancellation token. + /// public Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default) { ct.ThrowIfCancellationRequested(); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ResilientConfigReader.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ResilientConfigReader.cs index d6c72bf1..2c55b199 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ResilientConfigReader.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/LocalCache/ResilientConfigReader.cs @@ -69,7 +69,7 @@ public sealed class ResilientConfigReader } /// - /// Configuration-010: redact connection-string fragments (Password, User Id, Pwd, etc.) + /// Redacts connection-string fragments (Password, User Id, Pwd, etc.) /// that a caller's exception message could carry. Conservative regex pass — anything /// matching Key=Value with a known credential key gets its value replaced. /// @@ -120,7 +120,7 @@ public sealed class ResilientConfigReader // that case, not propagate. Only rethrow if the caller actually requested cancellation. catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested) { - // Configuration-010: do NOT pass the raw exception object — it carries the stack + // Do NOT pass the raw exception object — it carries the stack // and inner-exception chain, and SqlException/wrapping delegates can surface // connection-string fragments (Password=…, User Id=…) embedded in messages. // Log only the exception type and a scrubbed message so secrets stay out of logs. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs index 44524e97..dbc97b3e 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs @@ -72,8 +72,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptionsGets the DbSet of data protection keys. public DbSet DataProtectionKeys => Set(); - /// Configures the entity model when the context is first created. - /// The model builder used to configure the context. /// protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -151,13 +149,12 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions x.ClusterId) .OnDelete(DeleteBehavior.Restrict); - // Fleet-wide unique per decision #86 + // Fleet-wide unique. e.HasIndex(x => x.ApplicationUri).IsUnique().HasDatabaseName("UX_ClusterNode_ApplicationUri"); e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_ClusterNode_ClusterId"); // v2: the "one Primary per cluster" filtered unique index (and the RedundancyRole // column it filtered on) are gone. Akka cluster leader-of-driver-role is the - // authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator, - // Task 35). + // authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator). }); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/ILdapGroupRoleMappingService.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/ILdapGroupRoleMappingService.cs index 7b7348e5..5f0193dd 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/ILdapGroupRoleMappingService.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/ILdapGroupRoleMappingService.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Services; /// /// CRUD surface for — the control-plane mapping from /// LDAP groups to Admin UI roles. Consumed only by Admin UI code paths; the OPC UA -/// data-path evaluator MUST NOT depend on this interface (see decision #150 and the +/// data-path evaluator MUST NOT depend on this interface (see the /// Phase 6.2 compliance check on control/data-plane separation). /// /// @@ -28,11 +28,13 @@ public interface ILdapGroupRoleMappingService /// /// The LDAP groups to search for. /// The cancellation token. + /// The mappings whose LDAP group matches one of . Task> GetByGroupsAsync( IEnumerable ldapGroups, CancellationToken cancellationToken); /// Enumerate every mapping; Admin UI listing only. /// The cancellation token. + /// Every LDAP group role mapping. Task> ListAllAsync(CancellationToken cancellationToken); /// Create a new grant. @@ -43,11 +45,13 @@ public interface ILdapGroupRoleMappingService /// /// The LDAP group role mapping to create. /// The cancellation token. + /// The created mapping row. Task CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken); /// Delete a mapping by its surrogate key. /// The unique identifier of the mapping to delete. /// The cancellation token. + /// A task that represents the asynchronous operation. Task DeleteAsync(Guid id, CancellationToken cancellationToken); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs index 283fadbc..d09545f4 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Services/LdapGroupRoleMappingService.cs @@ -10,10 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Services; /// public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILdapGroupRoleMappingService { - /// Gets LDAP group role mappings for the specified groups. - /// The LDAP group names to query. - /// The cancellation token. - /// The matching role mappings. + /// public async Task> GetByGroupsAsync( IEnumerable ldapGroups, CancellationToken cancellationToken) { @@ -28,9 +25,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd .ConfigureAwait(false); } - /// Lists all LDAP group role mappings. - /// The cancellation token. - /// All role mappings ordered by group and cluster ID. + /// public async Task> ListAllAsync(CancellationToken cancellationToken) => await db.LdapGroupRoleMappings .AsNoTracking() @@ -39,10 +34,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd .ToListAsync(cancellationToken) .ConfigureAwait(false); - /// Creates a new LDAP group role mapping. - /// The mapping to create. - /// The cancellation token. - /// The created mapping with generated ID and timestamp. + /// public async Task CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(row); @@ -56,10 +48,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd return row; } - /// Deletes an LDAP group role mapping. - /// The mapping identifier. - /// The cancellation token. - /// A task that completes when the deletion is done. + /// public async Task DeleteAsync(Guid id, CancellationToken cancellationToken) { var existing = await db.LdapGroupRoleMappings.FindAsync([id], cancellationToken).ConfigureAwait(false); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs index 76bdb3f3..b5ba029d 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs @@ -5,7 +5,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.Configuration.Validation; /// -/// Managed-code pre-publish validator per decision #91. Complements the structural checks in +/// Managed-code pre-publish validator. Complements the structural checks in /// sp_ValidateDraft — this layer owns schema validation for JSON columns, UNS segment /// regex, EquipmentId derivation, cross-cluster checks, and anything else that's uncomfortable /// to express in T-SQL. Returns every failing rule in one pass (decision: surface all errors, @@ -21,6 +21,7 @@ public static class DraftValidator /// Validates a draft snapshot and returns all validation errors found in a single pass. /// /// The draft snapshot to validate. + /// Every validation error found; empty when the draft is valid. public static IReadOnlyList Validate(DraftSnapshot draft) { var errors = new List(); @@ -204,8 +205,9 @@ public static class DraftValidator } } - /// Decision #125: EquipmentId = 'EQ-' + lowercase first 12 hex chars of the UUID. + /// EquipmentId = 'EQ-' + lowercase first 12 hex chars of the UUID. /// The equipment UUID to derive the ID from. + /// The derived EQ--prefixed EquipmentId. public static string DeriveEquipmentId(Guid uuid) => "EQ-" + uuid.ToString("N")[..12].ToLowerInvariant(); @@ -222,7 +224,7 @@ public static class DraftValidator } /// - /// Phase 6.3 Stream A.2 + task #148 part 2 — managed pre-publish guard for cluster + /// Managed pre-publish guard for cluster /// topology vs. . The SQL /// CK_ServerCluster_RedundancyMode_NodeCount CHECK already enforces the /// (NodeCount, RedundancyMode) pair on the row itself, but it cannot see the @@ -240,6 +242,8 @@ public static class DraftValidator /// /// The server cluster to validate. /// The cluster nodes to validate against the cluster configuration. + /// Every failing topology rule found; empty when the cluster's declared and enabled-node + /// topology is consistent with its . public static IReadOnlyList ValidateClusterTopology( ServerCluster cluster, IReadOnlyList clusterNodes) @@ -273,7 +277,7 @@ public static class DraftValidator cluster.ClusterId)); // v2: the v1 "exactly one Primary per cluster" invariant is gone. RedundancyRole was - // dropped in Task 14d; in v2 the Akka cluster's role-leader-of-"driver" elects the + // dropped; in v2 the Akka cluster's role-leader-of-"driver" elects the // primary at runtime, so there is no static configuration to validate here. return errors; diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DataValueSnapshot.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DataValueSnapshot.cs index e247e905..9da53372 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DataValueSnapshot.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DataValueSnapshot.cs @@ -6,10 +6,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// shape so the node-manager can pass through quality, source timestamp, and /// server timestamp without translation. /// -/// -/// Per docs/v2/plan.md decision #13 — every driver maps to the same -/// OPC UA StatusCode space; this DTO is the universal carrier. -/// /// The raw value; null when indicates Bad. /// OPC UA status code (numeric value matches the OPC UA spec). /// Driver-side timestamp when the value was sampled at the source. Null if unavailable. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs index dc9a7fb0..c3b2085f 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverAttributeInfo.cs @@ -27,14 +27,14 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// /// True when a timed-out or failed write to this attribute is safe to replay. Per -/// docs/v2/plan.md decisions #44, #45, #143 — writes are NOT auto-retried by default +/// docs/v2/plan.md, writes are NOT auto-retried by default /// because replaying a pulse / alarm-ack / counter-increment / recipe-step advance can /// duplicate field actions. Drivers flag only tags whose semantics make retry safe /// (holding registers with level-set values, set-point writes to analog tags) — the /// capability invoker respects this flag when deciding whether to apply Polly retry. /// /// -/// Per ADR-002 — discriminates which runtime subsystem owns this node's dispatch. +/// Discriminates which runtime subsystem owns this node's dispatch. /// Defaults to so existing callers are unchanged. /// /// @@ -59,7 +59,7 @@ public sealed record DriverAttributeInfo( string? ScriptedAlarmId = null); /// -/// Per ADR-002 — discriminates which runtime subsystem owns this node's Read/Write/ +/// Discriminates which runtime subsystem owns this node's Read/Write/ /// Subscribe dispatch. Driver = a real IDriver capability surface; /// Virtual = a Phase 7 .VirtualTagId'd tag /// computed by the VirtualTagEngine; ScriptedAlarm = a scripted Part 9 alarm diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverCapability.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverCapability.cs index 79dfeac7..b6f9ea0d 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverCapability.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverCapability.cs @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// , ). /// /// -/// Per docs/v2/plan.md decision #143 (per-capability retry policy): Read / HistoryRead / +/// Per the per-capability retry policy in docs/v2/plan.md: Read / HistoryRead / /// Discover / Probe / AlarmSubscribe auto-retry; does NOT retry unless the /// tag-definition carries . Alarm-acknowledge is treated /// as a write for retry semantics (an alarm-ack is not idempotent at the plant-floor acknowledgement @@ -34,7 +34,7 @@ public enum DriverCapability /// . Retries by default. AlarmSubscribe, - /// . Does NOT retry — ack is a write-shaped operation (decision #143). + /// . Does NOT retry — ack is a write-shaped operation. AlarmAcknowledge, /// reads (Raw/Processed/AtTime/Events). Retries by default. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverHealth.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverHealth.cs index 7d6eb77d..f76d163f 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverHealth.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverHealth.cs @@ -13,7 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// failure (useful for diagnostics), and / /// / states may all /// carry a non-null message. Callers must not key behaviour on the LastError-null ↔ Healthy -/// pairing (Core.Abstractions-008). +/// pairing. /// public sealed record DriverHealth( DriverState State, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTier.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTier.cs index 92d72da2..33c678a5 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTier.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTier.cs @@ -6,8 +6,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// supervision with process-level recycle is in play. /// /// -/// Per docs/v2/driver-stability.md §2-4 and docs/v2/plan.md decisions #63-74. -/// /// /// A — managed, known-good SDK; low blast radius. In-process. Fast retries. /// Examples: OPC UA Client (OPCFoundation stack), S7 (S7NetPlus). @@ -18,7 +16,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// /// Process-kill protections (MemoryRecycle, ScheduledRecycleScheduler) are -/// Tier C only per decisions #73-74 and #145 — killing an in-process Tier A/B driver also kills +/// Tier C only — killing an in-process Tier A/B driver also kills /// every OPC UA session and every co-hosted driver, blast-radius worse than the leak. /// public enum DriverTier diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs index 3b0e7313..9d622322 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs @@ -6,9 +6,9 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// the registry to validate DriverInstance.DriverType values from the central config DB. /// /// -/// Per docs/v2/plan.md decisions #91 (JSON content validation in Admin app, not SQL CLR) -/// and #111 (driver type → namespace kind mapping enforced by sp_ValidateDraft). -/// The registry is the source of truth for both checks. +/// Per docs/v2/plan.md decisions on JSON content validation happening in the Admin app +/// (not SQL CLR), and on driver type → namespace kind mapping being enforced by +/// sp_ValidateDraft. The registry is the source of truth for both checks. /// /// Thread-safety: registration is typically single-threaded at startup; lookups happen on /// every config-apply (multi-threaded). The check-then-act inside is @@ -28,7 +28,7 @@ public sealed class DriverTypeRegistry /// /// The check-then-act (duplicate check → copy-on-write rebuild → swap) is performed under /// so concurrent calls cannot silently - /// discard each other's registrations — see Core.Abstractions-004. + /// discard each other's registrations. /// /// The driver type metadata to register. public void Register(DriverTypeMetadata metadata) @@ -55,6 +55,7 @@ public sealed class DriverTypeRegistry /// Look up a driver type by name. Throws if unknown. /// The driver type name to look up. + /// The registered metadata for the driver type. public DriverTypeMetadata Get(string driverType) { ArgumentException.ThrowIfNullOrWhiteSpace(driverType); @@ -69,6 +70,7 @@ public sealed class DriverTypeRegistry /// Try to look up a driver type by name. Returns null if unknown (no exception). /// The driver type name to look up. + /// The registered metadata, or if the type is not registered. public DriverTypeMetadata? TryGet(string driverType) { ArgumentException.ThrowIfNullOrWhiteSpace(driverType); @@ -76,6 +78,7 @@ public sealed class DriverTypeRegistry } /// Snapshot of all registered driver types. + /// A snapshot collection of all registered driver type metadata. public IReadOnlyCollection All() => _types.Values.ToList(); } @@ -86,8 +89,8 @@ public sealed class DriverTypeRegistry /// JSON Schema for DeviceConfig (multi-device drivers); null if the driver has no device layer. /// JSON Schema for TagConfig; required for every driver since every driver has tags. /// -/// Stability tier per docs/v2/driver-stability.md §2-4 and docs/v2/plan.md -/// decisions #63-74. Drives the shared resilience pipeline defaults +/// Stability tier per docs/v2/driver-stability.md §2-4 and the tiering decisions in +/// docs/v2/plan.md. Drives the shared resilience pipeline defaults /// ( × capability → CapabilityPolicy), the MemoryTracking /// hybrid-formula constants, and whether process-level MemoryRecycle / scheduled- /// recycle protections apply (Tier C only). Every registered driver type must declare one. @@ -100,7 +103,7 @@ public sealed record DriverTypeMetadata( string TagConfigJsonSchema, DriverTier Tier); -/// Bitmask of namespace kinds a driver type may populate. Per decision #111. +/// Bitmask of namespace kinds a driver type may populate. [Flags] public enum NamespaceKindCompatibility { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs index 4d6af673..d95bafb7 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/EquipmentTagRefResolver.cs @@ -16,6 +16,7 @@ public sealed class EquipmentTagRefResolver where TDef : class private readonly Func _parseRef; private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); + /// Initializes a new instance of the class. /// Authored tag-table lookup (returns null on miss). /// Parses an equipment-tag reference (TagConfig JSON) into a transient def, or null. public EquipmentTagRefResolver(Func byName, Func parseRef) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs index aa80b1c5..7443f6be 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorianDataSource.cs @@ -28,6 +28,7 @@ public interface IHistorianDataSource : IDisposable /// The end of the time range in UTC. /// The maximum number of values to return per node. /// A cancellation token that can be used to cancel the operation. + /// The raw historical samples for the tag over the requested range. Task ReadRawAsync( string fullReference, DateTime startUtc, @@ -46,6 +47,7 @@ public interface IHistorianDataSource : IDisposable /// The interval for bucketing samples. /// The aggregation function to apply to each bucket. /// A cancellation token that can be used to cancel the operation. + /// One aggregated sample per interval bucket over the requested range. Task ReadProcessedAsync( string fullReference, DateTime startUtc, @@ -63,6 +65,7 @@ public interface IHistorianDataSource : IDisposable /// The full reference of the tag to read. /// The list of timestamps to read values at. /// A cancellation token that can be used to cancel the operation. + /// One sample per requested timestamp, in the same order as . Task ReadAtTimeAsync( string fullReference, IReadOnlyList timestampsUtc, @@ -77,7 +80,7 @@ public interface IHistorianDataSource : IDisposable /// Note on parameter types — is (not /// ) so callers can pass 0 or a negative value as a "use the /// backend's default cap" sentinel; see WonderwareHistorianClient / - /// HistorianDataSource and Core.Abstractions-006 for the rationale. The sibling + /// HistorianDataSource for the rationale. The sibling /// / use /// uint maxValuesPerNode because their OPC UA HistoryRead surface has no /// equivalent "use default" sentinel. @@ -85,8 +88,7 @@ public interface IHistorianDataSource : IDisposable /// This surface declares and /// as required members — a server-side historian owns the full read surface, unlike /// where the same two methods are optional default-impl - /// methods so legacy drivers can stay raw-only. The asymmetry is intentional - /// (Core.Abstractions-008). + /// methods so legacy drivers can stay raw-only. The asymmetry is intentional. /// /// The source name to filter events, or null to return events from all sources. /// The start of the time range in UTC. @@ -96,9 +98,10 @@ public interface IHistorianDataSource : IDisposable /// default cap. When the backend cap truncates the result, implementations MUST set /// to a non-null token so /// callers can detect truncation and page — a null continuation point means all - /// matching events were returned. (Core.Abstractions-009.) + /// matching events were returned. /// /// A cancellation token that can be used to cancel the operation. + /// The matching historical event records, with a continuation point if the result was truncated. Task ReadEventsAsync( string? sourceName, DateTime startUtc, @@ -110,5 +113,6 @@ public interface IHistorianDataSource : IDisposable /// Point-in-time health snapshot for diagnostics and dashboards. Pure /// observation; never blocks on backend I/O. /// + /// The current health snapshot for the historian backend. HistorianHealthSnapshot GetHealthSnapshot(); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorizationOutbox.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorizationOutbox.cs index 5f44ce27..70aea123 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorizationOutbox.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/IHistorizationOutbox.cs @@ -16,6 +16,7 @@ public interface IHistorizationOutbox : IDisposable /// Appends to the tail of the durable buffer. /// The value record to buffer. /// Cancellation token. + /// A task that represents the asynchronous operation. ValueTask AppendAsync(HistorizationOutboxEntry entry, CancellationToken ct); /// @@ -24,6 +25,7 @@ public interface IHistorizationOutbox : IDisposable /// /// Maximum number of entries to return; must be positive. /// Cancellation token. + /// Up to oldest un-acked entries in FIFO order. ValueTask> PeekBatchAsync(int max, CancellationToken ct); /// @@ -32,9 +34,11 @@ public interface IHistorizationOutbox : IDisposable /// /// The to ack. /// Cancellation token. + /// A task that represents the asynchronous operation. ValueTask RemoveAsync(Guid id, CancellationToken ct); /// Current number of un-acked entries held in the buffer. /// Cancellation token. + /// The current number of un-acked entries in the buffer. ValueTask CountAsync(CancellationToken ct); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs index 582bb53c..e7d2f90b 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/Historian/NullHistorianDataSource.cs @@ -9,7 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// A process-wide singleton via (private ctor): it carries no state /// and is immutable, so one shared instance is safe to assign as the node-manager's -/// HistorianDataSource default until the Host wires a real source post-start (Task 5). +/// HistorianDataSource default until the Host wires a real source post-start. /// /// public sealed class NullHistorianDataSource : IHistorianDataSource @@ -55,10 +55,7 @@ public sealed class NullHistorianDataSource : IHistorianDataSource int maxEvents, CancellationToken cancellationToken) => Task.FromResult(EmptyEvents); - /// - /// Returns a fully-disabled snapshot — no connections open, every counter zero, every nullable - /// field null, no cluster nodes. Pure; never blocks. - /// + /// public HistorianHealthSnapshot GetHealthSnapshot() => new( TotalQueries: 0, TotalSuccesses: 0, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs index 85118c95..11689b00 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAddressSpaceBuilder.cs @@ -6,9 +6,9 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// as it discovers nodes — no buffering of the whole tree. /// /// -/// Per docs/v2/plan.md decision #52 — drivers register nodes via this builder -/// rather than returning a tree object. Supports incremental / large address spaces -/// without forcing the driver to buffer the whole tree. +/// Drivers register nodes via this builder rather than returning a tree object, +/// supporting incremental / large address spaces without forcing the driver to +/// buffer the whole tree. /// public interface IAddressSpaceBuilder { @@ -18,6 +18,7 @@ public interface IAddressSpaceBuilder /// /// OPC UA browse name (the segment of the path under the parent). /// Human-readable display name. May equal . + /// A child builder scoped to inside the new folder. IAddressSpaceBuilder Folder(string browseName, string displayName); /// @@ -27,6 +28,7 @@ public interface IAddressSpaceBuilder /// OPC UA browse name (the segment of the path under the parent folder). /// Human-readable display name. May equal . /// Driver-side metadata for the variable. + /// A handle for the registered variable, used by Core for subscription routing. IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo); /// @@ -56,6 +58,7 @@ public interface IVariableHandle /// Acknowledge, Deactivate). /// /// The alarm condition information. + /// The sink that receives lifecycle transitions for this alarm condition. IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs index ef967a28..1d763c7c 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs @@ -13,6 +13,7 @@ public interface IAlarmSource /// /// The driver node IDs to subscribe to. /// Cancellation token for the operation. + /// A handle identifying the created subscription. Task SubscribeAlarmsAsync( IReadOnlyList sourceNodeIds, CancellationToken cancellationToken); @@ -20,11 +21,13 @@ public interface IAlarmSource /// Cancel an alarm subscription returned by . /// The subscription handle returned from . /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken); /// Acknowledge one or more active alarms by source node ID + condition ID. /// The batch of alarm acknowledgement requests. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task AcknowledgeAsync( IReadOnlyList acknowledgements, CancellationToken cancellationToken); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs index 968d133a..a3904797 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriver.cs @@ -8,10 +8,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// ) are composable — a driver implements only what its /// backend actually supports. /// -/// -/// Per docs/v2/plan.md decisions #4 (composable capability interfaces) and #53 -/// (capability discovery via is checks — no redundant flag enum). -/// public interface IDriver { /// Stable logical ID of this driver instance, sourced from the central config DB. @@ -23,6 +19,7 @@ public interface IDriver /// Initialize the driver from its DriverConfig JSON; open connections; prepare for first use. /// The driver configuration as JSON. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken); /// @@ -37,13 +34,16 @@ public interface IDriver /// /// The driver configuration as JSON. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken); /// Stop the driver, close connections, release resources. Called on shutdown or driver removal. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task ShutdownAsync(CancellationToken cancellationToken); /// Current health snapshot, polled by Core for the status dashboard and ServiceLevel. + /// The driver's current health snapshot. DriverHealth GetHealth(); /// @@ -56,6 +56,7 @@ public interface IDriver /// allocation tracking". Tier C drivers (process-isolated) report through the same /// interface but the cache-flush is internal to their host. /// + /// The approximate memory footprint in bytes. long GetMemoryFootprint(); /// @@ -63,5 +64,6 @@ public interface IDriver /// Required-for-correctness state must NOT be flushed. /// /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task FlushOptionalCachesAsync(CancellationToken cancellationToken); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverConfigEditor.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverConfigEditor.cs index c47f0dba..3c9ef166 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverConfigEditor.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverConfigEditor.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// JSON editor with schema-driven validation against the registered JSON schema. /// /// -/// Per docs/v2/plan.md decision #27 — driver-specific config editors are deferred +/// Driver-specific config editors are deferred /// to each driver's implementation phase; v2.0 ships with the generic JSON editor as the /// default. This interface is the future plug-point so phase-specific editors can land /// incrementally. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverFactory.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverFactory.cs index ab154e30..e27fd6a9 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverFactory.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverFactory.cs @@ -34,12 +34,8 @@ public sealed class NullDriverFactory : IDriverFactory public static readonly NullDriverFactory Instance = new(); private NullDriverFactory() { } - /// Creates a driver (always returns null in this null implementation). - /// The driver type name. - /// The driver instance identifier. - /// The driver configuration as a JSON string. - /// Always returns null. + /// public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson) => null; - /// Gets the collection of supported driver types (empty in this null implementation). + /// public IReadOnlyCollection SupportedTypes { get; } = Array.Empty(); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverHealthPublisher.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverHealthPublisher.cs index fa2516db..32851fcb 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverHealthPublisher.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverHealthPublisher.cs @@ -11,6 +11,10 @@ public interface IDriverHealthPublisher /// Publishes a health snapshot for one driver instance. Implementations must be /// non-blocking and tolerant of being called from any thread. /// + /// The cluster the driver instance belongs to. + /// The driver instance the snapshot describes. + /// The current health snapshot. + /// The number of errors observed in the trailing 5-minute window. void Publish( string clusterId, string driverInstanceId, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverProbe.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverProbe.cs index 2043f3cf..2fa4dfc6 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverProbe.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverProbe.cs @@ -17,6 +17,10 @@ public interface IDriverProbe /// timeout cancellation. Never throw on connection failure; instead return a result /// with Ok = false + a message. /// + /// The driver-specific configuration JSON to probe with. + /// The maximum time to allow the probe to run. + /// Cancellation token for the operation. + /// The probe outcome, including success/failure and latency. Task ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverSupervisor.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverSupervisor.cs index 0e072712..6e169bd0 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverSupervisor.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IDriverSupervisor.cs @@ -6,10 +6,10 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// hard fault is detected (memory breach, wedge, scheduled recycle window). /// /// -/// Per docs/v2/plan.md decisions #68, #73-74, and #145. Tier A/B drivers do NOT have -/// a supervisor because they run in-process — recycling would kill every OPC UA session and -/// every co-hosted driver. The Core.Stability layer only invokes this interface for Tier C -/// instances after asserting the tier via . +/// Tier A/B drivers do NOT have a supervisor because they run in-process — recycling would +/// kill every OPC UA session and every co-hosted driver. The Core.Stability layer only invokes +/// this interface for Tier C instances after asserting the tier via +/// . /// public interface IDriverSupervisor { @@ -22,5 +22,6 @@ public interface IDriverSupervisor /// /// Human-readable reason — flows into the supervisor's logs. /// Cancels the recycle request; an in-flight restart is not interrupted. + /// A task that represents the asynchronous operation. Task RecycleAsync(string reason, CancellationToken cancellationToken); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs index 51ffb318..684b8b7c 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHistoryProvider.cs @@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// a raw-only driver compiles without forcing it to provide at-time / event surfaces it /// has no backend for. The sibling server-side surface, , /// declares both methods as required because a registered historian owns the full read -/// surface; the asymmetry is intentional (Core.Abstractions-008). +/// surface; the asymmetry is intentional. /// public interface IHistoryProvider { @@ -91,7 +91,7 @@ public interface IHistoryProvider /// reads use for maxValuesPerNode) because callers and downstream historian /// adapters historically treat maxEvents <= 0 as a sentinel meaning /// "use the backend's default cap" (see WonderwareHistorianClient / - /// HistorianDataSource). The asymmetry is intentional — Core.Abstractions-006. + /// HistorianDataSource). The asymmetry is intentional. /// /// Continuation contract when using the sentinel: When maxEvents <= 0 /// the backend applies its own cap. If the backend's cap truncates the result @@ -99,9 +99,9 @@ public interface IHistoryProvider /// to a non-null token so /// callers can detect truncation and page. A null ContinuationPoint means all /// matching events were returned — callers rely on this to decide whether to page. - /// (Core.Abstractions-009.) /// /// Request cancellation. + /// A task that returns the historical events result containing matching event records and optional continuation point. /// /// Default implementation throws. Drivers whose backend can serve historical events /// override: Galaxy (Wonderware Alarm & Events log) and the OPC UA Client driver diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHostConnectivityProbe.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHostConnectivityProbe.cs index 3a446b1d..d0f31fa5 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHostConnectivityProbe.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IHostConnectivityProbe.cs @@ -16,6 +16,7 @@ public interface IHostConnectivityProbe /// Snapshot of host-level connectivity. The Core uses this to drive Bad-quality /// fan-out scoped to the affected host's subtree (not the whole driver namespace). /// + /// The current connectivity status of each known host. IReadOnlyList GetHostStatuses(); /// Fired when a host transitions Running ↔ Stopped (or similar lifecycle change). diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IPerCallHostResolver.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IPerCallHostResolver.cs index 327cc166..8a52b4fc 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IPerCallHostResolver.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IPerCallHostResolver.cs @@ -9,8 +9,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// /// Multi-host drivers (Modbus with N PLCs, hypothetical AB CIP across a rack, etc.) -/// implement this so the Phase 6.1 resilience pipeline can be keyed on -/// (DriverInstanceId, ResolvedHostName, DriverCapability) per decision #144. One +/// implement this so the resilience pipeline can be keyed on +/// (DriverInstanceId, ResolvedHostName, DriverCapability). One /// dead PLC behind a multi-device Modbus driver then trips only its own breaker; healthy /// siblings keep serving. /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs index ab378056..0c605ff0 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IReadable.cs @@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// /// Reads are idempotent — Polly retry pipelines can safely retry on transient failures -/// (per docs/v2/plan.md decisions #34 and #44). +/// (per docs/v2/plan.md). /// public interface IReadable { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs index 39e8c3e3..5d98e132 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IRediscoverable.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// symbol-version-changed) implement this to tell Core when to re-run discovery. /// /// -/// Per docs/v2/plan.md decision #54 — static drivers (Modbus, S7, etc. whose tags +/// Per docs/v2/plan.md — static drivers (Modbus, S7, etc. whose tags /// only change via a published config generation) don't implement IRediscoverable. /// The Core just sees absence of the interface and skips change-detection wiring for that driver. /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs index e393cac4..c63b5268 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/ITagDiscovery.cs @@ -15,7 +15,7 @@ public enum DiscoveryRediscoverPolicy /// /// Driver capability for discovering tags and hierarchy from the backend. /// Streams discovered nodes into rather than -/// buffering the entire tree (decision #52 — supports incremental / large address spaces). +/// buffering the entire tree (supports incremental / large address spaces). /// public interface ITagDiscovery { @@ -25,6 +25,7 @@ public interface ITagDiscovery /// /// The address space builder to stream discovered nodes into. /// A cancellation token for the discovery operation. + /// A task that represents the asynchronous operation. Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken); /// Post-connect re-discovery policy. Default preserves the original retry-until-stable behavior. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IWritable.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IWritable.cs index 4abf60e4..257ad4e4 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IWritable.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IWritable.cs @@ -5,7 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// historian-only adapter, for example) can omit this. /// /// -/// Per docs/v2/plan.md decisions #44 + #45 — writes are NOT auto-retried by default. +/// Per docs/v2/plan.md, writes are NOT auto-retried by default. /// A timeout may fire after the device already accepted the command; replaying non-idempotent /// field actions (pulses, alarm acks, recipe steps, counter increments) can cause duplicate /// operations. Per-tag opt-in via Tag.WriteIdempotent = true in the central config DB @@ -19,6 +19,7 @@ public interface IWritable /// /// Pairs of full reference + value to write. /// Cancellation token; the driver should abort the batch if cancelled. + /// One per requested write, in the same order. Task> WriteAsync( IReadOnlyList writes, CancellationToken cancellationToken); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/OpcUaOperation.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/OpcUaOperation.cs index c8db1172..a8add4b0 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/OpcUaOperation.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/OpcUaOperation.cs @@ -2,8 +2,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// /// Every OPC UA operation surface the Phase 6.2 authorization evaluator gates, per -/// docs/v2/implementation/phase-6-2-authorization-runtime.md §Stream C and -/// decision #143. The evaluator maps each operation onto the corresponding +/// docs/v2/implementation/phase-6-2-authorization-runtime.md §Stream C. +/// The evaluator maps each operation onto the corresponding /// NodePermissions bit(s) to decide whether the calling session is allowed. /// /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/PollGroupEngine.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/PollGroupEngine.cs index f8c54378..deb33e9a 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/PollGroupEngine.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/PollGroupEngine.cs @@ -41,6 +41,7 @@ public sealed class PollGroupEngine : IAsyncDisposable /// Default floor for publishing intervals — matches the Modbus 100 ms cap. public static readonly TimeSpan DefaultMinInterval = TimeSpan.FromMilliseconds(100); + /// Constructs the engine with the driver-supplied reader, change callback, and optional interval floor / error sink. /// Driver-supplied batch reader; snapshots MUST be returned in the same /// order as the input references. /// Callback invoked per changed tag — the driver forwards to its own @@ -49,7 +50,7 @@ public sealed class PollGroupEngine : IAsyncDisposable /// per . /// Optional error sink — invoked once per caught reader exception (or /// internal contract-violation throw) so the owning driver can route the failure to its - /// health surface (Core.Abstractions-005). Defensive: an onError handler that + /// health surface. Defensive: an onError handler that /// itself throws is silently absorbed so a buggy forwarder cannot crash the poll loop. public PollGroupEngine( Func, CancellationToken, Task>> reader, @@ -68,6 +69,7 @@ public sealed class PollGroupEngine : IAsyncDisposable /// Register a new polled subscription and start its background loop. /// The list of tag references to poll. /// The desired polling interval; will be clamped to the configured minimum. + /// A handle identifying the new subscription, for later use with . public ISubscriptionHandle Subscribe(IReadOnlyList fullReferences, TimeSpan publishingInterval) { ArgumentNullException.ThrowIfNull(fullReferences); @@ -138,7 +140,7 @@ public sealed class PollGroupEngine : IAsyncDisposable catch (Exception ex) when (!IsFatal(ex)) { // transient poll error — loop continues, driver health surface logs it - // via the supplied onError callback (Core.Abstractions-005). + // via the supplied onError callback. ReportError(ex); } } @@ -170,7 +172,7 @@ public sealed class PollGroupEngine : IAsyncDisposable { var snapshots = await _reader(state.TagReferences, ct).ConfigureAwait(false); - // Core.Abstractions-002: validate the reader contract before indexing. A reader that + // Validate the reader contract before indexing. A reader that // returns fewer snapshots than references would silently stall the subscription; surface // the violation immediately with a descriptive exception instead. if (snapshots.Count != state.TagReferences.Count) @@ -207,6 +209,7 @@ public sealed class PollGroupEngine : IAsyncDisposable } /// Cancel every active subscription and await all loop tasks. Idempotent. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { // Cancel all loops first so they can all start winding down in parallel. @@ -253,7 +256,7 @@ public sealed class PollGroupEngine : IAsyncDisposable private sealed record PollSubscriptionHandle(long Id) : ISubscriptionHandle { - /// Gets a diagnostic identifier for this subscription. + /// public string DiagnosticId => $"poll-sub-{Id}"; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/WriteIdempotentAttribute.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/WriteIdempotentAttribute.cs index 1c0c0e64..900e83c7 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/WriteIdempotentAttribute.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/WriteIdempotentAttribute.cs @@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions; /// recipe-step advances can duplicate irreversible field actions. /// /// -/// Per docs/v2/plan.md decisions #44, #45, and #143. Applied to tag-definition POCOs +/// Applied to tag-definition POCOs /// (e.g. ModbusTagDefinition, S7TagDefinition, OPC UA client tag rows) at the /// property or record level. The CapabilityInvoker in ZB.MOM.WW.OtOpcUa.Core.Resilience /// reads this attribute via reflection once at driver-init time and caches the result; no diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/AlarmHistorianEvent.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/AlarmHistorianEvent.cs index 2bf8f5c8..45be63d6 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/AlarmHistorianEvent.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/AlarmHistorianEvent.cs @@ -4,8 +4,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; /// /// The event shape the historian sink consumes — source-agnostic across scripted -/// alarms + Galaxy-native + AB CIP ALMD + any future IAlarmSource per Phase 7 plan -/// decision #15 (sink scope = all alarm sources, not just scripted). A per-alarm +/// alarms + Galaxy-native + AB CIP ALMD + any future IAlarmSource (sink scope = all +/// alarm sources, not just scripted). A per-alarm /// HistorizeToAveva toggle on the producer side gates which events flow. /// /// Stable condition identity. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs index cf74b82a..7efaf3c9 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/IAlarmHistorianSink.cs @@ -11,8 +11,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; /// is fire-and-forget from the engine's perspective — /// the sink MUST NOT block the emitting thread. Production implementations /// () persist to a local SQLite queue -/// first, then drain asynchronously to the actual historian. Per Phase 7 plan -/// decision #16, failed downstream writes replay with exponential backoff; +/// first, then drain asynchronously to the actual historian. Per the Phase 7 plan, +/// failed downstream writes replay with exponential backoff; /// operator actions are never blocked waiting on the historian. /// /// @@ -25,9 +25,11 @@ public interface IAlarmHistorianSink /// Durably enqueue the event. Returns as soon as the queue row is committed. /// The alarm historian event to enqueue. /// A cancellation token for async operations. + /// A task that represents the asynchronous operation. Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken); /// Snapshot of current queue depth + drain health. + /// The current queue depth and drain health snapshot. HistorianSinkStatus GetStatus(); } @@ -96,6 +98,7 @@ public interface IAlarmHistorianWriter /// Push a batch of events to the historian. Returns one outcome per event, same order. /// The batch of alarm historian events to write. /// A cancellation token for async operations. + /// One per event, in the same order as . Task> WriteBatchAsync( IReadOnlyList batch, CancellationToken cancellationToken); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs index f9a59c45..a76bd1ee 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian/SqliteStoreAndForwardSink.cs @@ -5,11 +5,10 @@ using Serilog; namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; /// -/// Phase 7 plan decisions #16–#17 implementation: durable SQLite queue on the node -/// absorbs every qualifying alarm event, a drain worker batches rows to the -/// Wonderware historian sidecar via on an -/// exponential-backoff cadence, and operator acks never block on the historian -/// being reachable. +/// Durable SQLite queue on the node absorbs every qualifying alarm event, a drain +/// worker batches rows to the Wonderware historian sidecar via +/// on an exponential-backoff cadence, and +/// operator acks never block on the historian being reachable. /// /// /// @@ -27,7 +26,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian; /// ); /// /// Dead-lettered rows stay in place for the configured retention window (default -/// 30 days per Phase 7 plan decision #21) so operators can inspect + manually +/// 30 days) so operators can inspect + manually /// retry before the sweeper purges them. Regular queue capacity is bounded — /// overflow evicts the oldest non-dead-lettered rows with a WARN log. The /// durability guarantee is therefore bounded by : @@ -77,29 +76,16 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable private TimeSpan _tickInterval; private volatile int _backoffIndex; // volatile: read by EnqueueAsync, DrainOnceAsync, RescheduleDrain, and StartDrainLoop - // from different threads; Dispose() writes it from the owner's thread (Core.AlarmHistorian-013). + // from different threads; Dispose() writes it from the owner's thread. private volatile bool _disposed; - // Core.AlarmHistorian-005: status fields written by the drain timer thread and - // read concurrently by GetStatus() / health-check threads. Guard all reads and - // writes with this lock so the Admin UI never observes a torn or stale value. private readonly object _statusLock = new(); private DateTime? _lastDrainUtc; private DateTime? _lastSuccessUtc; private string? _lastError; private HistorianDrainState _drainState = HistorianDrainState.Idle; - // Core.AlarmHistorian-009: lifetime counter of rows evicted due to capacity overflow. - // Surfaces in HistorianSinkStatus so operators can see data-loss events without - // having to scrape the WARN log. private long _evictedCount; - // Core.AlarmHistorian-008: keep an approximate in-memory count of non-dead-lettered - // rows so EnqueueAsync does not need to run a SELECT COUNT(*) on every call. The - // counter is seeded from storage at construction, kept current by every mutation - // (Enqueue, Drain, RetryDeadLettered, PurgeAgedDeadLetters, EnforceCapacity), and - // periodically re-synced from storage as a safety net against drift. - // Mutations cross threads (EnqueueAsync is called from the emitting thread, drain - // runs on the timer / drain thread) so it is updated via Interlocked. private long _queuedRowCount; // Probe counter — incremented every time we actually issue a real COUNT(*) for // capacity enforcement. Public for test instrumentation only. @@ -145,7 +131,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable // DefaultTimeout gives ADO.NET command-level retry; the PRAGMA busy_timeout // applied in OpenConnection backs it with SQLite's own busy-handler so an // enqueue/drain collision waits out the file lock instead of throwing - // SQLITE_BUSY immediately (Core.AlarmHistorian-004). + // SQLITE_BUSY immediately. _connectionString = new SqliteConnectionStringBuilder { DataSource = databasePath, @@ -153,8 +139,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable }.ToString(); InitializeSchema(); - // Core.AlarmHistorian-008: seed the in-memory counter from storage so the - // perf-optimised EnqueueAsync path starts in sync with what's on disk. _queuedRowCount = ProbeQueuedRowCount(); } @@ -195,11 +179,11 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable /// /// The worker is a self-rescheduling one-shot : after each /// drain it sets its next due-time to max(tickInterval, CurrentBackoff) - /// so a historian outage actually slows the cadence down the backoff ladder - /// (Core.AlarmHistorian-002). The callback body is fully guarded — a fault in + /// so a historian outage actually slows the cadence down the backoff ladder. + /// The callback body is fully guarded — a fault in /// is logged and recorded into /// rather than being lost as an unobserved task - /// exception (Core.AlarmHistorian-006). + /// exception. /// /// The base interval between drain attempts. public void StartDrainLoop(TimeSpan tickInterval) @@ -251,19 +235,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable catch (ObjectDisposedException) { /* raced with Dispose — nothing to re-arm */ } } - /// - /// Enqueues an alarm historian event asynchronously for forwarding to the historian. - /// Respects the queue capacity and enforces eviction of oldest rows when full. - /// - /// - /// Core.AlarmHistorian-003: use async SQLite APIs so the emitting thread is not - /// blocked waiting for a file-lock or disk write; honor the cancellationToken - /// throughout. Microsoft.Data.Sqlite's async surface (OpenAsync / - /// ExecuteNonQueryAsync) is a thin wrapper over the synchronous path, so the - /// blocking still happens — but on a thread-pool thread, not the caller's thread. - /// - /// The alarm historian event to enqueue. - /// Cancellation token for the operation. + /// public async Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken) { if (evt is null) throw new ArgumentNullException(nameof(evt)); @@ -273,10 +245,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable await conn.OpenAsync(cancellationToken).ConfigureAwait(false); await ApplyPragmasAsync(conn, cancellationToken).ConfigureAwait(false); - // Core.AlarmHistorian-008: use the in-memory counter to short-circuit the - // capacity check on every enqueue. The bare hot path is now one INSERT — no - // SELECT COUNT(*). We fall back to a real probe only when the cached counter - // says we're at or above capacity, or periodically to defend against drift. await EnforceCapacityFastPathAsync(conn, cancellationToken).ConfigureAwait(false); using var cmd = conn.CreateCommand(); @@ -347,13 +315,8 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable /// on RetryPlease. Safe to call from multiple threads; the semaphore enforces /// serial execution. /// - /// - /// Core.AlarmHistorian-008: every per-tick SQLite operation runs through a - /// single shared connection (purge, read, corrupt-row dead-letter, and the - /// outcome-applying transaction). Pre-fix the drain opened three independent - /// connections per tick, each paying the open + PRAGMA cost. - /// /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. public async Task DrainOnceAsync(CancellationToken ct) { if (_disposed) return; @@ -391,8 +354,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable foreach (var rowId in corruptRowIds) DeadLetterRow(conn, corruptTx, rowId, $"corrupt payload at {_clock():O}"); corruptTx.Commit(); - // Each corrupt row leaves the non-dead-lettered queue — bookkeeping for - // the in-memory counter (Core.AlarmHistorian-008). Interlocked.Add(ref _queuedRowCount, -corruptRowIds.Count); _logger.Warning( "Dead-lettered {Count} historian queue row(s) with un-deserializable payload", @@ -413,9 +374,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable } catch (OperationCanceledException) { - // Core.AlarmHistorian-012: reset _drainState so the status surface does - // not stay stuck at Draining after the cancellation unwinds the method. - // The row stays queued; the next drain tick will retry it. lock (_statusLock) { _drainState = HistorianDrainState.BackingOff; } throw; } @@ -432,13 +390,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable return; } - // Core.AlarmHistorian-007: a cardinality mismatch is a writer contract - // violation — potentially the events were already persisted. Rather than - // throwing (which, pre -006 fix, was swallowed and left _drainState - // stale), treat it as a transient batch failure so the rows stay queued - // and the backoff surface becomes visible to the operator. A deterministic - // mismatch will stall the row until an operator intervenes or the writer - // is fixed — far safer than re-throwing into a fire-and-forget timer. if (outcomes.Count != events.Count) { var msg = $"Writer returned {outcomes.Count} outcomes for {events.Count} events — expected 1:1; treating as batch retry"; @@ -491,7 +442,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable } // Ack-deleted + PermanentFail-dead-lettered rows both leave the // non-dead-lettered queue, as do RetryPlease rows that hit the max-attempts - // cap (finding 002) — keep the counter aligned (Core.AlarmHistorian-008). + // cap (finding 002) — keep the counter aligned. if (rowsLeavingQueue > 0) Interlocked.Add(ref _queuedRowCount, -rowsLeavingQueue); @@ -517,14 +468,10 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable } } - /// Gets the current status of the historian sink including queue depth and drain state. + /// public HistorianSinkStatus GetStatus() { if (_disposed) throw new ObjectDisposedException(nameof(SqliteStoreAndForwardSink)); - // Core.AlarmHistorian-008: read the non-dead-lettered count from the in-memory - // counter so a busy Admin UI / health probe does not hammer the DB. Dead-letter - // depth is rare-path only (it lives in the queue until retention) so a real - // COUNT(*) on a single combined connection is fine. var queued = Interlocked.Read(ref _queuedRowCount); if (queued < 0) queued = 0; @@ -536,8 +483,6 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable deadlettered = (long)(cmd.ExecuteScalar() ?? 0L); } - // Core.AlarmHistorian-005: snapshot status fields atomically under the lock - // so the Admin UI never sees a torn DateTime? or stale DrainState. DateTime? lastDrain, lastSuccess; string? lastError; HistorianDrainState drainState; @@ -562,6 +507,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable } /// Operator action from Admin UI — retry every dead-lettered row. Non-cascading: they rejoin the regular queue + get a fresh backoff. + /// The number of rows revived from the dead-letter state. public int RetryDeadLettered() { if (_disposed) throw new ObjectDisposedException(nameof(SqliteStoreAndForwardSink)); @@ -570,7 +516,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable cmd.CommandText = "UPDATE Queue SET DeadLettered = 0, AttemptCount = 0, LastError = NULL WHERE DeadLettered = 1"; var revived = cmd.ExecuteNonQuery(); // Dead-lettered rows rejoin the non-dead-lettered queue — keep the in-memory - // counter aligned (Core.AlarmHistorian-008). + // counter aligned. if (revived > 0) Interlocked.Add(ref _queuedRowCount, revived); return revived; } @@ -652,10 +598,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable cmd.ExecuteNonQuery(); } - // Async variant used by EnqueueAsync (Core.AlarmHistorian-003). - // Core.AlarmHistorian-008: the precise path — runs COUNT(*) to compute the exact - // number of rows to evict. Reached only from the fast-path fallback when the - // in-memory counter says we are at or above capacity. + // Async variant used by EnqueueAsync. private async Task EnforceCapacityAsync(SqliteConnection conn, CancellationToken ct) { Interlocked.Increment(ref _capacityProbeCount); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs index 103f234c..8b1cc7f9 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/AlarmConditionState.cs @@ -5,12 +5,12 @@ namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; /// /// Persistent per-alarm state tracked by the Part 9 state machine. Every field /// carried here either participates in the state machine or contributes to the -/// audit trail required by Phase 7 plan decision #14 (GxP / 21 CFR Part 11). +/// audit trail required for GxP / 21 CFR Part 11 compliance. /// /// /// -/// is re-derived from the predicate at startup per Phase 7 -/// decision #14 — the engine runs every alarm's predicate against current tag +/// is re-derived from the predicate at startup — the engine +/// runs every alarm's predicate against current tag /// values at Load, overriding whatever Active state is in the store. /// Every other state field persists verbatim across server restarts so /// operators don't re-ack active alarms after an outage + shelved alarms stay @@ -22,7 +22,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; /// entries. The runtime type is so /// each append is O(log n) rather than the O(n) copy a plain /// IReadOnlyList<AlarmComment> would force on every audit-producing -/// transition. (Core.ScriptedAlarms-008) +/// transition. /// /// public sealed record AlarmConditionState( diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs index c5ae66e4..59b6fac2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/IAlarmStateStore.cs @@ -3,8 +3,8 @@ using System.Collections.Concurrent; namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; /// -/// Persistence for across server restarts. Phase 7 -/// plan decision #14: operator-supplied state (EnabledState / AckedState / +/// Persistence for across server restarts. +/// Operator-supplied state (EnabledState / AckedState / /// ConfirmedState / ShelvingState + audit trail) persists; ActiveState is /// recomputed from the live predicate on startup so operators never re-ack. /// diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs index 241676a7..ec32da73 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/MessageTemplate.cs @@ -4,8 +4,8 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; /// -/// Per Phase 7 plan decision #13, alarm messages are static-with-substitution -/// templates. The engine resolves {TagPath} tokens at event emission time +/// Alarm messages are static-with-substitution templates. The engine resolves +/// {TagPath} tokens at event emission time /// against current tag values; unresolvable tokens become {?} so the event /// still fires but the operator sees where the reference broke. /// @@ -41,7 +41,6 @@ public static class MessageTemplate /// inspect, but the operator-facing message must make doubt explicit rather /// than substituting a value an operator might act on. See the /// "Input-quality policy" section in docs/ScriptedAlarms.md. - /// (Core.ScriptedAlarms-010) /// /// The template string with {path} tokens. /// A function to resolve tag values by path. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs index 4745d50f..23643061 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/Part9StateMachine.cs @@ -340,7 +340,6 @@ public static class Part9StateMachine /// no operator intent recorded — e.g. a predicate re-evaluation that /// confirms the existing active state) leave /// null because there is nothing to surface to an operator. -/// (Core.ScriptedAlarms-011) /// /// public sealed record TransitionResult(AlarmConditionState State, EmissionKind Emission, string? NoOpReason = null) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs index 05f10e7c..d3a3cebd 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmDefinition.cs @@ -17,15 +17,15 @@ namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms; /// /// Human-readable alarm name — used in the browse tree + Admin UI. /// Concrete OPC UA Part 9 subtype the alarm materializes as. -/// Static severity per Phase 7 plan decision #13; not currently computed by the predicate. +/// Static severity; not currently computed by the predicate. /// -/// Message text with {TagPath} tokens resolved at event-emission time per -/// Phase 7 plan decision #13. Unresolvable tokens emit {?} + a structured +/// Message text with {TagPath} tokens resolved at event-emission time. +/// Unresolvable tokens emit {?} + a structured /// error so operators can spot stale references. /// /// /// Roslyn C# script returning bool. true = alarm condition currently holds (active); -/// false = condition has cleared. Same sandbox rules as virtual tags per Phase 7 decision #6. +/// false = condition has cleared. Same sandbox rules as virtual tags. /// /// /// When true, every transition emission of this alarm flows to the Historian alarm diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs index 9b14507d..1dac6ac2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmEngine.cs @@ -46,7 +46,7 @@ public sealed class ScriptedAlarmEngine : IDisposable // A plain Dictionary read concurrent with a writer's entry reassignment can // throw or return torn state; ConcurrentDictionary makes entry assignment and // snapshot enumeration safe. The only write shapes are indexer-set and Clear, - // both of which ConcurrentDictionary supports atomically. (Core.ScriptedAlarms-001) + // both of which ConcurrentDictionary supports atomically. private readonly ConcurrentDictionary _alarms = new(StringComparer.Ordinal); /// @@ -58,7 +58,6 @@ public sealed class ScriptedAlarmEngine : IDisposable /// runs under , which serialises every evaluation: /// two threads can never observe the same scratch in a half-refilled state. /// Cleared in alongside . - /// (Core.ScriptedAlarms-009) /// private readonly ConcurrentDictionary _scratchByAlarmId = new(StringComparer.Ordinal); @@ -67,11 +66,11 @@ public sealed class ScriptedAlarmEngine : IDisposable /// Compile cache for every alarm predicate. Routes 's /// calls through the /// cache so the collectible - /// each compile produces is actually disposed on the publish-replace path - /// (Core.Scripting-016): the cache's + /// each compile produces is actually disposed on the publish-replace path: + /// the cache's /// disposes every materialised evaluator before dropping its dictionary entry, /// so a config-publish releases the prior generation's ALCs and the per-publish - /// accretion the Core.Scripting-008 fix targeted is actually freed in production. + /// accretion the prior fix targeted is actually freed in production. /// Pre-fix the engine called ScriptEvaluator.Compile directly, which left /// the ALCs rooted until the process exited — defeating -008 on the real path. /// @@ -79,9 +78,9 @@ public sealed class ScriptedAlarmEngine : IDisposable /// /// Test-only diagnostic: returns the per-alarm scratch read-cache dictionary - /// if one has been allocated, else null. Used by Core.ScriptedAlarms-009 - /// regression tests to assert the scratch is reused across evaluations - /// (two reads return the same instance). + /// if one has been allocated, else null. Used by regression tests to assert + /// the scratch is reused across evaluations (two reads return the same + /// instance). /// /// /// Synchronization: the returned @@ -94,9 +93,10 @@ public sealed class ScriptedAlarmEngine : IDisposable /// comparisons (e.g. asserting the same instance is reused across calls), /// and single-key reads against an engine that has quiesced after a /// deterministic upstream push. Anything more involved should snapshot a - /// copy under the gate. (Core.ScriptedAlarms-013.) + /// copy under the gate. /// /// The alarm identifier to look up. + /// The alarm's read-cache dictionary if one has been allocated; otherwise null. internal IReadOnlyDictionary? TryGetScratchReadCacheForTest(string alarmId) => _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.ReadCache : null; @@ -110,9 +110,9 @@ public sealed class ScriptedAlarmEngine : IDisposable /// read-cache as — the same /// "don't iterate during an in-flight evaluation" caveat applies. Safe /// for reference-identity assertions on a quiesced engine. - /// (Core.ScriptedAlarms-013.) /// /// The alarm identifier to look up. + /// The alarm's predicate context if one has been allocated; otherwise null. internal AlarmPredicateContext? TryGetScratchContextForTest(string alarmId) => _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.Context : null; private readonly ConcurrentDictionary _valueCache @@ -131,7 +131,7 @@ public sealed class ScriptedAlarmEngine : IDisposable // these so a re-evaluation in flight when shutdown begins finishes its // SaveAsync before the engine returns control to the caller. The HashSet is // accessed under its own lock — never under _evalGate — so registration / - // unregistration cannot deadlock against the gate. (Core.ScriptedAlarms-006) + // unregistration cannot deadlock against the gate. private readonly HashSet _inFlight = []; private readonly object _inFlightLock = new(); @@ -171,10 +171,11 @@ public sealed class ScriptedAlarmEngine : IDisposable /// compile failures into one , subscribes /// to upstream input tags, seeds the value cache, loads persisted state from /// the store (falling back to Fresh for first-load alarms), and recomputes - /// ActiveState per Phase 7 plan decision #14 (startup recovery). + /// ActiveState from the current predicate (startup recovery). /// /// The alarm definitions to load. /// The cancellation token. + /// A task that completes once every definition has been loaded and state restored. public async Task LoadAsync(IReadOnlyList definitions, CancellationToken ct) { if (_disposed) throw new ObjectDisposedException(nameof(ScriptedAlarmEngine)); @@ -191,7 +192,7 @@ public sealed class ScriptedAlarmEngine : IDisposable // assignment at the bottom of the try block. Without this, the old timer // keeps firing against the partially-cleared _alarms until Dispose() is // eventually called — not a permanent leak, but an unexpected side effect - // during the window. (Core.ScriptedAlarms-015) + // during the window. _shelvingTimer?.Dispose(); _shelvingTimer = null; @@ -200,11 +201,11 @@ public sealed class ScriptedAlarmEngine : IDisposable _alarmsReferencing.Clear(); // Drop the prior generation's per-alarm scratch buffers — definitions may // have changed (different Inputs, different Logger), so any reuse would be - // unsafe. (Core.ScriptedAlarms-009) + // unsafe. _scratchByAlarmId.Clear(); // Dispose every compiled-predicate ALC from the prior generation BEFORE we - // recompile this one. Skipping this is what made Core.Scripting-008 a - // no-op in production. (Core.Scripting-016) + // recompile this one. Skipping this is what made the earlier fix a + // no-op in production. _compileCache.Clear(); var compileFailures = new List(); @@ -222,7 +223,6 @@ public sealed class ScriptedAlarmEngine : IDisposable // Route through CompiledScriptCache so the emitted assembly's // collectible ALC participates in publish-replace cleanup. - // (Core.Scripting-016) var evaluator = _compileCache.GetOrCompile(def.PredicateScriptSource); var timed = new TimedScriptEvaluator(evaluator, _scriptTimeout); var logger = _loggerFactory.Create(def.AlarmId); @@ -256,21 +256,20 @@ public sealed class ScriptedAlarmEngine : IDisposable // Seed the value cache with current tag values before subscribing. The // ReadTag calls happen first so that the initial predicate evaluation below - // (startup recovery, decision #14) uses a consistent snapshot. + // (startup recovery) uses a consistent snapshot. // Subscriptions are established AFTER _loaded = true so that any synchronous // initial-push an ITagUpstreamSource delivers from inside SubscribeTag arrives // when _alarms is fully initialised. Before _loaded = true, a synchronous push // would race the in-progress state restore and could overwrite the carefully // seeded cache with a push that has no defined ordering relative to ReadTag. - // (Core.ScriptedAlarms-004) foreach (var path in _alarmsReferencing.Keys) _valueCache[path] = _upstream.ReadTag(path); // Restore persisted state, falling back to Fresh where nothing was saved, - // then re-derive ActiveState from the current predicate per decision #14. + // then re-derive ActiveState from the current predicate. // Any predicate emissions queue into `pending` and fire after the gate // is released — so a startup-recovery activation event can call back into - // the engine without deadlocking. (Core.ScriptedAlarms-003) + // the engine without deadlocking. foreach (var (alarmId, state) in _alarms) { var persisted = await _store.LoadAsync(alarmId, ct).ConfigureAwait(false); @@ -294,8 +293,8 @@ public sealed class ScriptedAlarmEngine : IDisposable // Start the shelving-check timer — ticks every 5s, expires any timed shelves // that have passed their UnshelveAtUtc. The prior timer was already disposed - // at the START of this try block (Core.ScriptedAlarms-015), so _shelvingTimer - // is null here; no double-dispose risk. (Core.ScriptedAlarms-002, -015) + // at the START of this try block, so _shelvingTimer + // is null here; no double-dispose risk. _shelvingTimer = new Timer(_ => RunShelvingCheck(), null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); } @@ -305,7 +304,7 @@ public sealed class ScriptedAlarmEngine : IDisposable } // Fire any emissions collected during startup recovery OUTSIDE the gate so - // subscribers can re-enter the engine safely. (Core.ScriptedAlarms-003) + // subscribers can re-enter the engine safely. foreach (var evt in pending) FireEvent(evt); } @@ -314,10 +313,12 @@ public sealed class ScriptedAlarmEngine : IDisposable /// unknown alarm. Mainly used for diagnostics + the Admin UI status page. /// /// The alarm identifier. + /// The alarm's current condition state, or null if the alarm id is unknown. public AlarmConditionState? GetState(string alarmId) => _alarms.TryGetValue(alarmId, out var s) ? s.Condition : null; /// Gets the current persisted state for all loaded alarms. + /// The current condition state of every loaded alarm. public IReadOnlyCollection GetAllStates() => _alarms.Values.Select(a => a.Condition).ToArray(); @@ -326,6 +327,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// The user performing the acknowledgment. /// An optional comment to attach to the acknowledgment. /// The cancellation token. + /// A task that completes once the acknowledgment has been applied and persisted. public Task AcknowledgeAsync(string alarmId, string user, string? comment, CancellationToken ct) => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAcknowledge(cur, user, comment, _clock())); @@ -334,6 +336,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// The user performing the confirmation. /// An optional comment to attach to the confirmation. /// The cancellation token. + /// A task that completes once the confirmation has been applied and persisted. public Task ConfirmAsync(string alarmId, string user, string? comment, CancellationToken ct) => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyConfirm(cur, user, comment, _clock())); @@ -341,6 +344,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// The alarm identifier. /// The user performing the shelve operation. /// The cancellation token. + /// A task that completes once the shelve has been applied and persisted. public Task OneShotShelveAsync(string alarmId, string user, CancellationToken ct) => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyOneShotShelve(cur, user, _clock())); @@ -349,6 +353,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// The user performing the shelve operation. /// The UTC time at which the shelve will automatically expire. /// The cancellation token. + /// A task that completes once the timed shelve has been applied and persisted. public Task TimedShelveAsync(string alarmId, string user, DateTime unshelveAtUtc, CancellationToken ct) => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyTimedShelve(cur, user, unshelveAtUtc, _clock())); @@ -356,6 +361,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// The alarm identifier. /// The user performing the unshelve operation. /// The cancellation token. + /// A task that completes once the unshelve has been applied and persisted. public Task UnshelveAsync(string alarmId, string user, CancellationToken ct) => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyUnshelve(cur, user, _clock())); @@ -363,6 +369,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// The alarm identifier. /// The user performing the enable operation. /// The cancellation token. + /// A task that completes once the enable has been applied and persisted. public Task EnableAsync(string alarmId, string user, CancellationToken ct) => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyEnable(cur, user, _clock())); @@ -370,6 +377,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// The alarm identifier. /// The user performing the disable operation. /// The cancellation token. + /// A task that completes once the disable has been applied and persisted. public Task DisableAsync(string alarmId, string user, CancellationToken ct) => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyDisable(cur, user, _clock())); @@ -378,6 +386,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// The user adding the comment. /// The comment text. /// The cancellation token. + /// A task that completes once the comment has been recorded and persisted. public Task AddCommentAsync(string alarmId, string user, string text, CancellationToken ct) => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAddComment(cur, user, text, _clock())); @@ -395,14 +404,13 @@ public sealed class ScriptedAlarmEngine : IDisposable // Persist BEFORE updating in-memory so a store failure leaves both // in-memory and persisted at the prior state rather than diverging. // If SaveAsync throws the in-memory _alarms entry stays unchanged and - // the exception propagates to the caller. (Core.ScriptedAlarms-007) + // the exception propagates to the caller. await _store.SaveAsync(result.State, ct).ConfigureAwait(false); _alarms[alarmId] = state with { Condition = result.State }; // Build the emission event under the gate (it captures a coherent // snapshot of state + message-template values) but defer the actual // OnEvent dispatch until after Release() so a slow subscriber or a // subscriber that re-enters the engine doesn't block / deadlock. - // (Core.ScriptedAlarms-003) if (result.Emission != EmissionKind.None) pending = BuildEmission(state, result.State, result.Emission); else if (result.NoOpReason is { } reason) @@ -411,7 +419,7 @@ public sealed class ScriptedAlarmEngine : IDisposable // disabled-alarm no-ops + idempotent ack/confirm/shelve/unshelve // calls. We surface them at debug so they're available when // investigating "why didn't my ack take effect?" without spamming - // the main info log. (Core.ScriptedAlarms-011) + // the main info log. state.Logger.Debug("Alarm {AlarmId} no-op transition: {Reason}", alarmId, reason); } } @@ -427,7 +435,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// Upstream-change callback. Updates the value cache + enqueues predicate /// re-evaluation for every alarm referencing the changed path. Fire-and-forget /// so driver-side dispatch isn't blocked; the background task is tracked so - /// can drain it. (Core.ScriptedAlarms-006) + /// can drain it. /// /// The upstream tag path that changed. /// The new data value snapshot. @@ -452,7 +460,6 @@ public sealed class ScriptedAlarmEngine : IDisposable // Re-check after acquiring the gate: a Dispose() call may have // completed between our _evalGate.WaitAsync and here. Writing to a // disposing store or mutating _alarms after clear is unsafe. - // (Core.ScriptedAlarms-005) if (_disposed) return; foreach (var id in alarmIds) @@ -463,7 +470,7 @@ public sealed class ScriptedAlarmEngine : IDisposable if (!ReferenceEquals(newState, state.Condition)) { // Persist before updating in-memory so a store failure leaves - // both sides at the prior state. (Core.ScriptedAlarms-007) + // both sides at the prior state. await _store.SaveAsync(newState, ct).ConfigureAwait(false); _alarms[id] = state with { Condition = newState }; } @@ -477,7 +484,7 @@ public sealed class ScriptedAlarmEngine : IDisposable return; } // Fire emissions OUTSIDE _evalGate so subscriber callbacks can re-enter - // the engine without deadlocking. (Core.ScriptedAlarms-003) + // the engine without deadlocking. foreach (var evt in pending) FireEvent(evt); } @@ -486,14 +493,13 @@ public sealed class ScriptedAlarmEngine : IDisposable /// Returns the new condition state. If the transition produces an emission, /// appends it to so the caller can fire /// them after releasing _evalGate — keeping subscriber callbacks - /// outside the gate. (Core.ScriptedAlarms-003) + /// outside the gate. /// /// /// Every caller (LoadAsync and ReevaluateAsync) owns a pending list and /// passes it here; emissions are always deferred to after the gate is released. /// The parameter is required (non-nullable) to make this contract explicit and /// prevent a future caller from accidentally firing events under the gate. - /// (Core.ScriptedAlarms-014) /// private async Task EvaluatePredicateToStateAsync( AlarmState state, AlarmConditionState seed, DateTime nowUtc, CancellationToken ct, @@ -501,7 +507,7 @@ public sealed class ScriptedAlarmEngine : IDisposable { // Look up (or lazily allocate) the per-alarm scratch and refill its read cache // in place. The dictionary + context survive across evaluations so the hot path - // no longer allocates per upstream tag change. (Core.ScriptedAlarms-009) + // no longer allocates per upstream tag change. var scratch = _scratchByAlarmId.GetOrAdd( state.Definition.AlarmId, _ => new AlarmScratch(state.Inputs, state.Logger, _clock)); @@ -552,7 +558,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// first upstream push hasn't arrived yet. The dictionary is cleared and /// repopulated under _evalGate so no concurrent reader can observe /// a partial state. Replaces the old BuildReadCache which allocated a - /// fresh dictionary every call (Core.ScriptedAlarms-009). + /// fresh dictionary every call. /// private void RefillReadCache( Dictionary cache, IReadOnlySet inputs) @@ -591,7 +597,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// _evalGate so the message-template resolution uses a coherent /// value-cache snapshot. The actual dispatch is /// done by AFTER the gate is - /// released. (Core.ScriptedAlarms-003) + /// released. /// private ScriptedAlarmEvent? BuildEmission(AlarmState state, AlarmConditionState condition, EmissionKind kind) { @@ -632,7 +638,6 @@ public sealed class ScriptedAlarmEngine : IDisposable /// block the gate for every other engine operation, and a subscriber /// that re-enters the engine (e.g. calls AcknowledgeAsync) would /// deadlock against the non-reentrant SemaphoreSlim. - /// (Core.ScriptedAlarms-003) /// private void FireEvent(ScriptedAlarmEvent evt) { @@ -656,7 +661,6 @@ public sealed class ScriptedAlarmEngine : IDisposable /// /// Register a fire-and-forget task so can await it. /// The task removes itself from the set on completion via a continuation. - /// (Core.ScriptedAlarms-006) /// private void TrackBackgroundTask(Task task) { @@ -673,7 +677,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// /// Test hook — triggers a shelving check synchronously without waiting for /// the 5-second timer. Allows tests that inject a controllable clock to advance - /// time and immediately drive timed-shelve expiry. (Core.ScriptedAlarms-012) + /// time and immediately drive timed-shelve expiry. /// internal void RunShelvingCheckForTest() => RunShelvingCheck(); @@ -689,7 +693,6 @@ public sealed class ScriptedAlarmEngine : IDisposable // running callbacks, so a shelving-check callback that passed the _disposed // check in RunShelvingCheck can arrive here after Dispose() has returned. // Mutating _alarms or saving to a disposed store here is unsafe. - // (Core.ScriptedAlarms-005) if (_disposed) return; var now = _clock(); @@ -700,7 +703,7 @@ public sealed class ScriptedAlarmEngine : IDisposable if (!ReferenceEquals(result.State, state.Condition)) { // Persist before updating in-memory so a store failure leaves - // both sides at the prior state. (Core.ScriptedAlarms-007) + // both sides at the prior state. await _store.SaveAsync(result.State, ct).ConfigureAwait(false); _alarms[id] = state with { Condition = result.State }; if (result.Emission != EmissionKind.None) @@ -718,7 +721,7 @@ public sealed class ScriptedAlarmEngine : IDisposable _engineLogger.Warning(ex, "ScriptedAlarmEngine shelving-check failed"); return; } - // Fire emissions OUTSIDE _evalGate. (Core.ScriptedAlarms-003) + // Fire emissions OUTSIDE _evalGate. foreach (var evt in pending) FireEvent(evt); } @@ -751,7 +754,6 @@ public sealed class ScriptedAlarmEngine : IDisposable // flight can outlive the engine and write to a (possibly disposed) store // after Dispose() has returned. The tasks re-check _disposed after // acquiring the gate and bail out, but the await still has to complete. - // (Core.ScriptedAlarms-006) Task[] toAwait; lock (_inFlightLock) { toAwait = [.. _inFlight]; } if (toAwait.Length > 0) @@ -769,20 +771,19 @@ public sealed class ScriptedAlarmEngine : IDisposable // Safe to clear here: the Task.WhenAll drain above guaranteed no // ReevaluateAsync / ShelvingCheckAsync is mid-flight, and _disposed=true // prevents new background work from being queued (OnUpstreamChange bails on - // line 334). Pre-Core.Scripting-016 the comment said "Do NOT clear _alarms", + // line 334). Previously the comment said "Do NOT clear _alarms", // but that was when the engine called ScriptEvaluator.Compile directly and // held the script ALCs through _alarms→AlarmState→TimedScriptEvaluator - // forever — leaving them rooted defeated the -008 collectible-ALC unload. + // forever — leaving them rooted defeated the collectible-ALC unload. // Clearing now drops the delegate references so the cache's Dispose call - // below can actually unload the emitted assemblies. (Core.ScriptedAlarms-005 - // re-evaluated under -016.) + // below can actually unload the emitted assemblies. _alarms.Clear(); _alarmsReferencing.Clear(); _scratchByAlarmId.Clear(); // Dispose every compiled-predicate ALC so the engine's shutdown actually // releases the emitted assemblies. The drain above ensures no evaluator is // mid-call; CompiledScriptCache.Dispose internally guards against use-after- - // dispose. (Core.Scripting-016) + // dispose. _compileCache.Dispose(); } @@ -800,7 +801,7 @@ public sealed class ScriptedAlarmEngine : IDisposable /// cleared and refilled in on /// each call. wraps that dictionary by reference, so a /// refilled is what the predicate's - /// ctx.GetTag(path) calls observe. (Core.ScriptedAlarms-009) + /// ctx.GetTag(path) calls observe. /// /// /// Reuse is safe because serialises every diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs index eabf3411..3e59119b 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs @@ -124,7 +124,7 @@ public sealed class ScriptedAlarmSource : IAlarmSource, IDisposable /// Initializes a new instance of the class. /// The diagnostic ID for this subscription handle. public SubscriptionHandle(string id) { DiagnosticId = id; } - /// Gets the diagnostic ID that uniquely identifies this subscription. + /// public string DiagnosticId { get; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/AlarmPredicateContext.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/AlarmPredicateContext.cs index f57cff3b..f5349bdf 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/AlarmPredicateContext.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/AlarmPredicateContext.cs @@ -36,8 +36,6 @@ public sealed class AlarmPredicateContext : ScriptContext _clock = clock ?? (() => DateTime.UtcNow); } - /// Gets a tag value from the read cache. - /// The tag path to retrieve. /// public override DataValueSnapshot GetTag(string path) { @@ -48,9 +46,6 @@ public sealed class AlarmPredicateContext : ScriptContext : new DataValueSnapshot(null, 0x80340000u /* BadNodeIdUnknown */, null, _clock()); } - /// Rejects virtual tag writes for pure predicate semantics. - /// The virtual tag path. - /// The value to write. /// public override void SetVirtualTag(string path, object? value) { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/ScriptContext.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/ScriptContext.cs index e0f389b9..6a73c622 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/ScriptContext.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting.Abstractions/ScriptContext.cs @@ -6,8 +6,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// The API user scripts see as the global ctx. Abstract — concrete subclasses /// (e.g. VirtualTagScriptContext, AlarmScriptContext) plug in the -/// actual tag-backend + logger + virtual-tag writer for each evaluation. Plan decision -/// #6 (see docs/v2/plan.md): scripts can read any tag, write only to virtual +/// actual tag-backend + logger + virtual-tag writer for each evaluation. By design +/// (see docs/v2/plan.md), scripts can read any tag, write only to virtual /// tags, and have no other .NET reach — no HttpClient, no File, no Process, no /// reflection. /// @@ -47,12 +47,13 @@ public abstract class ScriptContext /// /// /// The literal tag path to read. + /// A snapshot of the tag's current value, quality, and source timestamp. public abstract DataValueSnapshot GetTag(string path); /// /// Write a value to a virtual tag. Operator scripts cannot write to driver-sourced /// tags — the OPC UA dispatch in DriverNodeManager rejects that separately - /// per ADR-002 with BadUserAccessDenied. This method is the only write path + /// with BadUserAccessDenied. This method is the only write path /// virtual tags have. /// /// @@ -89,6 +90,8 @@ public abstract class ScriptContext /// The previous value to compare against. /// The minimum change magnitude to detect (must be >= 0). /// Negative values cause the function to always return true; NaN always returns false. + /// true when differs from + /// by more than ; otherwise false. public static bool Deadband(double current, double previous, double tolerance) => Math.Abs(current - previous) > tolerance; } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs index 70b52bb1..b287e84b 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/CompiledScriptCache.cs @@ -32,8 +32,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// /// Lifecycle: compiled scripts hold a collectible -/// per evaluator -/// (Core.Scripting-008 fix). disposes every materialised +/// per evaluator. +/// disposes every materialised /// evaluator before dropping its dictionary entry so the emitted assemblies are /// eligible for GC immediately after a publish. drops the /// cache itself for graceful server shutdown. @@ -75,7 +75,7 @@ public sealed class CompiledScriptCache : IDisposable // value reference, so if two threads race the same bad source both observe // the same faulted Lazy and both reach this catch, and a concurrent retry // re-added a fresh Lazy under the same key between the two removals, the - // second removal does NOT evict the in-flight retry. (Core.Scripting-006.) + // second removal does NOT evict the in-flight retry. _cache.TryRemove(new KeyValuePair>>(key, lazy)); throw; } @@ -88,28 +88,28 @@ public sealed class CompiledScriptCache : IDisposable /// Drop every cached compile. Used on config generation publish + tests. /// Disposes each materialised evaluator before removing it so its collectible /// unloads and the - /// emitted script assembly becomes eligible for GC (Core.Scripting-008). + /// emitted script assembly becomes eligible for GC. /// /// /// Safe to call after — the operation is idempotent. /// sets _disposed = true before invoking this /// method (so callers see the post-Dispose guard on ), /// but this method itself MUST run to completion so the Dispose-triggered - /// drain actually unloads every materialised evaluator's ALC. (Core.Scripting-016 - /// uncovered this — a previous Clear-aborts-when-disposed guard silently - /// skipped the entire drain on Dispose, leaving emitted assemblies rooted.) + /// drain actually unloads every materialised evaluator's ALC (a previous + /// Clear-aborts-when-disposed guard silently skipped the entire drain on + /// Dispose, leaving emitted assemblies rooted). /// public void Clear() { // Snapshot (key, value) pairs and remove with the value-scoped // TryRemove(KeyValuePair<,>) overload — same shape as the - // Core.Scripting-006 fix in GetOrCompile's catch block. A concurrent + // fix in GetOrCompile's catch block. A concurrent // GetOrCompile re-add that hashes to the same key between our snapshot // and the TryRemove inserts a *different* Lazy reference; the value- // scoped removal sees the mismatch and leaves the fresh entry intact // (instead of evicting + disposing it while the concurrent caller // still holds it). The fresh evaluator and its ALC stay live for the - // concurrent caller. (Core.Scripting-014.) + // concurrent caller. foreach (var entry in _cache.ToArray()) { if (_cache.TryRemove(entry)) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs index 819c0651..2f9f5290 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/DependencyExtractor.cs @@ -8,8 +8,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// Parses a script's source text + extracts every ctx.GetTag("literal") and /// ctx.SetVirtualTag("literal", ...) call. Outputs the static dependency set -/// the virtual-tag engine uses to build its change-trigger subscription graph (Phase -/// 7 plan decision #7 — AST inference, operator doesn't maintain a separate list). +/// the virtual-tag engine uses to build its change-trigger subscription graph (AST +/// inference, operator doesn't maintain a separate list). /// /// /// @@ -29,7 +29,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// extractor while still having a working script. Calls with the same method name on /// a different receiver (other.GetTag("X")) are explicitly ignored so that /// scripts defining local helper types with matching names do not produce spurious -/// dependencies. (Core.Scripting-004.) +/// dependencies. /// /// public static class DependencyExtractor @@ -39,6 +39,7 @@ public static class DependencyExtractor /// paths, or a list of rejection messages if non-literal paths were used. /// /// The script source code to analyze. + /// The inferred read/write tag dependencies, plus any non-literal-path rejections. public static DependencyExtractionResult Extract(string scriptSource) { if (string.IsNullOrWhiteSpace(scriptSource)) @@ -81,7 +82,6 @@ public static class DependencyExtractor // field). Calls with the same method name on a different receiver (e.g. // someHelper.GetTag("X")) are ignored — not picking them up avoids spurious // dependencies when scripts define local types with matching method names. - // (Core.Scripting-004.) if (node.Expression is MemberAccessExpressionSyntax member && member.Expression is IdentifierNameSyntax receiver && receiver.Identifier.ValueText == "ctx") @@ -113,7 +113,7 @@ public static class DependencyExtractor // rather than StringLiteralToken. Checking the expression kind // (StringLiteralExpression) covers all token kinds Roslyn assigns to literal // strings, so a """raw""" path is harvested rather than mis-rejected as a - // dynamic path. (Core.Scripting-005.) + // dynamic path. if (pathArg is not LiteralExpressionSyntax literal || !literal.IsKind(SyntaxKind.StringLiteralExpression)) { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs index 71392f37..005734b6 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ForbiddenTypeAnalyzer.cs @@ -17,13 +17,13 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// /// -/// Deny-list is the authoritative Phase 7 plan decision #6 set: +/// Deny-list is the authoritative Phase 7 plan set: /// System.IO, System.Net, System.Diagnostics, /// System.Reflection, System.Threading.Thread, /// System.Threading.Tasks (scripts are synchronous predicates — no /// legitimate need to start background tasks; a Task.Run fan-out outlives /// the evaluation timeout entirely), System.Runtime.InteropServices, -/// Microsoft.Win32. (Core.Scripting-003.) +/// Microsoft.Win32. /// /// /// Deny-list prefix match. System.Net catches System.Net.Http, @@ -45,7 +45,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// Activator.CreateInstance is a reflection-equivalent escape; GC /// and AppDomain expose process-wide control. Legitimate System /// types (Math, String, Convert, DateTime, …) are not -/// on the list and stay usable. (Core.Scripting-001.) +/// on the list and stay usable. /// /// public static class ForbiddenTypeAnalyzer @@ -69,14 +69,13 @@ public static class ForbiddenTypeAnalyzer // ForbiddenFullTypeNames instead so the check actually fires. "System.Threading.Tasks", // Task.Run / Parallel — scripts are synchronous predicates // and have no legitimate need to start background work; - // a Task fan-out outlives the evaluation timeout entirely - // (Core.Scripting-003). + // a Task fan-out outlives the evaluation timeout entirely. "System.Runtime.InteropServices", "System.Runtime.Loader", // AssemblyLoadContext + AssemblyDependencyResolver — - // arbitrary DLL load into the host process - // (Core.Scripting-012). Namespace-prefix rather than - // type-granular so future BCL additions to this - // namespace are denied by default. + // arbitrary DLL load into the host process. + // Namespace-prefix rather than type-granular so + // future BCL additions to this namespace are + // denied by default. "Microsoft.Win32", // registry ]; @@ -104,15 +103,14 @@ public static class ForbiddenTypeAnalyzer /// per-evaluation timeout; denied type-granularly because its containing /// namespace is System.Threading (shared with allowed types like /// CancellationToken), so a namespace-prefix rule cannot reach it - /// without blocking unrelated types. (Core.Scripting-010.) + /// without blocking unrelated types. /// System.Threading.ThreadPool / System.Threading.Timer — /// background-work vectors that outlive the per-evaluation timeout; same - /// threat as Task.Run that Core.Scripting-003 closed. (Core.Scripting-012.) + /// threat as Task.Run. /// System.Runtime.CompilerServices.Unsafe — exposes raw type- /// reinterpretation (As<TFrom,TTo>, As<T>(object)) /// that bypasses the CLR type system without requiring an unsafe context at - /// the call site; enables type-confusion and managed heap corruption. - /// (Core.Scripting-017.) + /// the call site; enables type-confusion and managed heap corruption. /// /// public static readonly IReadOnlyList ForbiddenFullTypeNames = @@ -124,18 +122,15 @@ public static class ForbiddenTypeAnalyzer // System.Threading.Thread lives in the System.Threading namespace (shared with // CancellationToken, SemaphoreSlim, etc.), so a namespace-prefix deny-list cannot // target it without blocking those legitimate types. Denied type-granularly here. - // (Core.Scripting-010.) "System.Threading.Thread", - // Core.Scripting-012 — broadening the references list to the BCL trusted-platform- - // assemblies set (Core.Scripting-008 follow-up) re-exposed two background-work - // vectors the original deny-list missed. Both live in System.Threading (shared - // with allowed sync primitives like CancellationToken / SemaphoreSlim), so they - // must be denied type-granularly: + // Broadening the references list to the BCL trusted-platform-assemblies set + // re-exposed two background-work vectors the original deny-list missed. Both live + // in System.Threading (shared with allowed sync primitives like CancellationToken / + // SemaphoreSlim), so they must be denied type-granularly: // // System.Threading.ThreadPool — QueueUserWorkItem / UnsafeQueueUserWorkItem // re-introduce the background-fanout threat - // Core.Scripting-003 closed against - // System.Threading.Tasks. + // closed against System.Threading.Tasks. // System.Threading.Timer — Timer(callback, …) schedules unbounded work // that outlives the per-evaluation timeout. // @@ -144,14 +139,14 @@ public static class ForbiddenTypeAnalyzer // namespace are denied by default. "System.Threading.ThreadPool", "System.Threading.Timer", - // Core.Scripting-017 — System.Runtime.CompilerServices.Unsafe exposes raw - // type-reinterpretation (Unsafe.As, Unsafe.As(object)) that - // bypasses the CLR type system without requiring an 'unsafe' compilation context - // at the call site. In a script, calling Unsafe.As(someObject) can corrupt - // managed heap references and cause type-confusion in downstream virtual-tag - // consumers. Type-granular (not namespace-prefix) because System.Runtime.CompilerServices - // also contains harmless compile-time-only types (CallerMemberNameAttribute, - // MethodImplAttribute, MethodImplOptions) that scripts may legitimately reference. + // System.Runtime.CompilerServices.Unsafe exposes raw type-reinterpretation + // (Unsafe.As, Unsafe.As(object)) that bypasses the CLR type system + // without requiring an 'unsafe' compilation context at the call site. In a script, + // calling Unsafe.As(someObject) can corrupt managed heap references and + // cause type-confusion in downstream virtual-tag consumers. Type-granular (not + // namespace-prefix) because System.Runtime.CompilerServices also contains harmless + // compile-time-only types (CallerMemberNameAttribute, MethodImplAttribute, + // MethodImplOptions) that scripts may legitimately reference. "System.Runtime.CompilerServices.Unsafe", ]; @@ -176,7 +171,7 @@ public static class ForbiddenTypeAnalyzer /// declares it, even though the receiver type System.Type is allowed). /// /// - /// Pass (2) — the Core.Scripting-002 fix — resolves the type of every + /// Pass (2) resolves the type of every /// TypeSyntax node via GetTypeInfo. The old walker only inspected /// the four node kinds above, so a forbidden type named through /// typeof(System.IO.File), a generic argument @@ -223,7 +218,7 @@ public static class ForbiddenTypeAnalyzer break; } - // Pass (2) — type-reference surface (Core.Scripting-002). Every TypeSyntax + // Pass (2) — type-reference surface. Every TypeSyntax // resolves to the type it names, regardless of the syntactic form that // introduced it (typeof operand, cast type, generic argument, default(T) // operand, array element type, is/as pattern type, declared local type). @@ -275,10 +270,10 @@ public static class ForbiddenTypeAnalyzer var typeName = typeSymbol.ToDisplayString(); - // The broadened walk (Core.Scripting-002) resolves both GetSymbolInfo and - // GetTypeInfo on every node, so the same forbidden reference can be hit several - // times. Dedupe on span + type so the operator sees one rejection per offending - // reference, not a noisy pile of identical messages. + // The broadened walk resolves both GetSymbolInfo and GetTypeInfo on every node, + // so the same forbidden reference can be hit several times. Dedupe on span + type + // so the operator sees one rejection per offending reference, not a noisy pile of + // identical messages. if (rejections.Any(r => r.Span == span && r.TypeName == typeName)) return; @@ -298,9 +293,9 @@ public static class ForbiddenTypeAnalyzer } // Type-granular deny-list — dangerous types that live in the allow-listed - // System namespace and so cannot be caught by ForbiddenNamespacePrefixes - // (Core.Scripting-001). Matched on the full type name; OriginalDefinition - // unwraps any generic construction before naming. + // System namespace and so cannot be caught by ForbiddenNamespacePrefixes. + // Matched on the full type name; OriginalDefinition unwraps any generic + // construction before naming. var fullTypeName = typeSymbol.OriginalDefinition.ToDisplayString( SymbolDisplayFormat.FullyQualifiedFormat.WithGlobalNamespaceStyle( SymbolDisplayGlobalNamespaceStyle.Omitted)); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs index de482ede..c7ac8280 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptEvaluator.cs @@ -18,9 +18,9 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// Scripts are wrapped in a synthesized CompiledScript.Run(globals) method /// and compiled via into a regular .NET assembly /// that is loaded into a collectible -/// . The collectible ALC is the fix for -/// Core.Scripting-008: per-publish recompile accretion was previously unbounded -/// because Roslyn's CSharpScript.CreateDelegate emits into the default ALC +/// . The collectible ALC fixes unbounded +/// per-publish recompile accretion: previously +/// Roslyn's CSharpScript.CreateDelegate emitted into the default ALC /// (non-collectible); now unloads the entire ALC and the /// emitted assembly becomes eligible for GC. /// @@ -38,8 +38,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// /// Runtime exceptions thrown from user code propagate unwrapped. The virtual-tag -/// engine catches them per-tag and maps to BadInternalError quality -/// per Phase 7 decision #11; this layer doesn't swallow anything so tests can +/// engine catches them per-tag and maps to BadInternalError quality; +/// this layer doesn't swallow anything so tests can /// assert on the original exception type. /// /// @@ -66,6 +66,7 @@ public sealed class ScriptEvaluator : IDisposable /// Compiles user script source into an evaluator. /// The user script source code to compile. + /// A ready-to-run wrapping the compiled script. public static ScriptEvaluator Compile(string scriptSource) { if (scriptSource is null) throw new ArgumentNullException(nameof(scriptSource)); @@ -78,7 +79,7 @@ public sealed class ScriptEvaluator : IDisposable var wrapperSource = BuildWrapperSource(scriptSource, sandbox.Imports); var syntaxTree = CSharpSyntaxTree.ParseText(wrapperSource); - // Step 1a — defend against wrapper-source injection (Core.Scripting-013). + // Step 1a — defend against wrapper-source injection. // A script body of `return 0; } public static int Evil() { return 0;` would // close the synthesized `Run` method early, declare a sibling `Evil` method // inside the synthesized `CompiledScript` class, and leave the wrapper's @@ -173,6 +174,7 @@ public sealed class ScriptEvaluator : IDisposable /// Runs the script against an already-constructed context. /// The script context. /// Cancellation token for the operation. + /// A task carrying the script's result. public Task RunAsync(TContext context, CancellationToken ct = default) { if (_disposed) throw new ObjectDisposedException(nameof(ScriptEvaluator)); @@ -215,7 +217,7 @@ public sealed class ScriptEvaluator : IDisposable /// exactly one member — the Run method. Anything else (a sibling /// method, nested class, additional class in the namespace, free-floating /// top-level statement) means the user source closed the synthesized braces - /// early and injected its own declarations. (Core.Scripting-013.) + /// early and injected its own declarations. /// private static void EnforceSingleRunMember(SyntaxTree syntaxTree) { @@ -344,7 +346,7 @@ public sealed class ScriptEvaluator : IDisposable // emits as `global::Ns.Outer.Inner` — valid C#. The pre-fix code // used `IndexOf('`')` to find the FIRST backtick and truncated the // entire name there, silently dropping the rest of the nested-generic - // closed args. (Core.Scripting-015.) + // closed args. var rawName = t.GetGenericTypeDefinition().FullName!.Replace('+', '.'); var allArgs = t.GetGenericArguments(); var segments = rawName.Split('.'); @@ -403,7 +405,7 @@ internal sealed class ScriptAssemblyLoadContext : AssemblyLoadContext /// reports compile-time errors against the wrapper source. Mirrors the /// Microsoft.CodeAnalysis.Scripting.CompilationErrorException from the legacy /// CSharpScript path so callers (engines + the Admin test-harness) keep the -/// same catch site after the Core.Scripting-008 rewrite. +/// same catch site after the rewrite. /// public sealed class CompilationErrorException : Exception { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs index 514633ff..42cf1790 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/ScriptSandbox.cs @@ -5,7 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// /// Factory for the compile-time sandbox every user script is built against. -/// Implements Phase 7 plan decision #6 (read-only sandbox) by whitelisting only the +/// Enforces a read-only sandbox by whitelisting only the /// assemblies + namespaces the script API needs; no System.IO, no /// System.Net, no System.Diagnostics.Process, no /// System.Reflection. Attempts to reference those types in a script fail at @@ -18,8 +18,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Scripting; /// mscorlib / System.Runtime — this class overrides that with an /// explicit minimal allow-list. The list is the same regardless of whether /// uses the legacy -/// CSharpScript path or the collectible-AssemblyLoadContext path -/// (Core.Scripting-008): both go through . +/// CSharpScript path or the collectible-AssemblyLoadContext path: +/// both go through . /// /// /// Namespaces pre-imported so scripts don't have to write using clauses: @@ -43,6 +43,7 @@ public static class ScriptSandbox /// to resolve ctx.GetTag(...) calls. /// /// The concrete script context type to use for compilation. + /// The sandbox configuration (assembly references + default imports) to compile the script against. public static SandboxConfig Build(Type contextType) { if (contextType is null) throw new ArgumentNullException(nameof(contextType)); @@ -74,8 +75,8 @@ public static class ScriptSandbox // type by FQN — including the ones we forbid (HttpClient, File, Process, // Registry, etc.). Letting those types resolve at compile is intentional: the // hard security gate is ForbiddenTypeAnalyzer in step 3 of the compile pipeline - // (Core.Scripting-001 / -002 established the analyzer must be the sole gate - // because type forwarding makes any references-list-only restriction porous). + // (the analyzer must be the sole gate because type forwarding makes any + // references-list-only restriction porous). // The references list now serves only as scoping hygiene — out-of-band BCL // surface (operator-authored hosting helpers, third-party packages, app code) // is not on the list and stays unreachable. @@ -118,7 +119,7 @@ public static class ScriptSandbox string.Equals(name, "mscorlib.dll", StringComparison.Ordinal) || // Microsoft.Win32.Registry isn't a System.* DLL but the analyzer's // Microsoft.Win32 deny-list relies on the type being resolvable so it - // can identify + reject it (Core.Scripting-001 / -002). Add the one + // can identify + reject it. Add the one // DLL we need rather than broadening to Microsoft.* (which would also // pull in compilers, build tooling, etc.). string.Equals(name, "Microsoft.Win32.Registry.dll", StringComparison.Ordinal)) diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs index f3bda39a..4adeccab 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.Scripting/TimedScriptEvaluator.cs @@ -90,7 +90,7 @@ public sealed class TimedScriptEvaluator // When both fire at nearly the same time, WaitAsync observes them in // non-deterministic order, so a cancel that arrives a few µs after the // timeout still reaches here as TimeoutException. Re-check the token so - // the guarantee holds regardless of race ordering. (Core.Scripting-007.) + // the guarantee holds regardless of race ordering. if (ct.IsCancellationRequested) throw new OperationCanceledException(ct); throw new ScriptTimeoutException(Timeout); @@ -101,7 +101,7 @@ public sealed class TimedScriptEvaluator /// /// Thrown when a script evaluation exceeds its configured timeout. The virtual-tag /// engine (Stream B) catches this + maps the owning tag's quality to -/// BadInternalError per Phase 7 plan decision #11, logging a structured +/// BadInternalError per the Phase 7 plan, logging a structured /// warning with the offending script name so operators can locate + fix it. /// public sealed class ScriptTimeoutException : Exception diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs index e655196e..aeee6423 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/DependencyGraph.cs @@ -156,6 +156,7 @@ public sealed class DependencyGraph /// dependencies. Throws if any cycle /// exists. Implemented via Kahn's algorithm. /// + /// The nodes in dependency-respecting evaluation order. public IReadOnlyList TopologicalSort() { // Kahn's framing: edge u -> v means "u must come before v". For dependencies, @@ -205,6 +206,7 @@ public sealed class DependencyGraph /// Empty list means the graph is a DAG. Useful for surfacing every cycle in one /// rejection pass so operators see all of them, not just one at a time. /// + /// Every strongly-connected component of size greater than 1, plus every self-loop; empty when the graph is a DAG. public IReadOnlyList> DetectCycles() { // Iterative Tarjan's SCC. Avoids recursion so deep graphs don't StackOverflow. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs index 73b3ba03..0b5e97b1 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/IHistoryWriter.cs @@ -25,8 +25,6 @@ public sealed class NullHistoryWriter : IHistoryWriter { public static readonly NullHistoryWriter Instance = new(); - /// Records a data value snapshot (no-op implementation). - /// The virtual tag path (unused). - /// The data value snapshot (unused). + /// public void Record(string path, DataValueSnapshot value) { } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs index b6ba7612..3616abe0 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/ITagUpstreamSource.cs @@ -30,6 +30,7 @@ public interface ITagUpstreamSource /// when the path isn't configured. /// /// The tag path to read. + /// The last-known value + quality snapshot for . DataValueSnapshot ReadTag(string path); /// @@ -40,5 +41,6 @@ public interface ITagUpstreamSource /// /// The tag path to subscribe to. /// The callback to invoke when the value changes. + /// An that cancels the subscription when disposed. IDisposable SubscribeTag(string path, Action observer); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs index 9b67a296..7009684c 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagEngine.cs @@ -11,21 +11,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags; /// , builds the dependency graph, subscribes to every /// referenced upstream tag, and schedules re-evaluations on change + on timer. /// -/// -/// -/// Evaluation order is topological per ADR-001 / Phase 7 plan decision #19 — -/// serial for the v1 rollout, parallel promoted to a follow-up. When upstream -/// tag X changes, the engine computes the transitive dependent closure of X in -/// topological rank and evaluates each in turn, so a cascade through multiple -/// levels of virtual tags settles within one change-trigger pass. -/// -/// -/// Per-tag error isolation per Phase 7 plan decision #11 — a script exception -/// (or timeout) fails that tag's latest value with BadInternalError or -/// BadTypeMismatch quality and logs a structured error; every other tag -/// keeps evaluating. The engine itself never faults from a user script. -/// -/// public sealed class VirtualTagEngine : IDisposable { private readonly ITagUpstreamSource _upstream; @@ -42,13 +27,13 @@ public sealed class VirtualTagEngine : IDisposable /// Compile cache for every virtual-tag script. Routes 's /// calls through the /// cache so the collectible - /// each compile produces is actually disposed on the publish-replace path - /// (Core.Scripting-016): the cache's - /// disposes every materialised evaluator before dropping its dictionary entry, - /// so a config-publish releases the prior generation's ALCs and the per-publish - /// accretion the Core.Scripting-008 fix targeted is actually freed in production. - /// Pre-fix the engine called ScriptEvaluator.Compile directly, which left - /// the ALCs rooted until the process exited — defeating -008 on the real path. + /// each compile produces is actually disposed on the publish-replace path: the + /// cache's disposes + /// every materialised evaluator before dropping its dictionary entry, so a + /// config-publish releases the prior generation's ALCs and the per-publish + /// accretion is actually freed in production. Pre-fix, the engine called + /// ScriptEvaluator.Compile directly, which left the ALCs rooted until + /// the process exited. /// private readonly CompiledScriptCache _compileCache = new(); @@ -99,9 +84,6 @@ public sealed class VirtualTagEngine : IDisposable UnsubscribeFromUpstream(); _tags.Clear(); _graph.Clear(); - // Dispose every compiled-script ALC from the prior generation BEFORE we - // recompile this one. Skipping this is what made Core.Scripting-008 a - // no-op in production (Core.Scripting-016). _compileCache.Clear(); var compileFailures = new List(); @@ -131,8 +113,6 @@ public sealed class VirtualTagEngine : IDisposable continue; } - // Route through CompiledScriptCache so the emitted assembly's collectible - // ALC participates in publish-replace cleanup. (Core.Scripting-016) var evaluator = _compileCache.GetOrCompile(def.ScriptSource); var timed = new TimedScriptEvaluator(evaluator, _scriptTimeout); var scriptLogger = _loggerFactory.Create(def.Path); @@ -198,6 +178,7 @@ public sealed class VirtualTagEngine : IDisposable /// default. Also called after a config reload. /// /// Cancellation token to stop evaluation. + /// A task that represents the asynchronous operation. public async Task EvaluateAllAsync(CancellationToken ct = default) { EnsureLoaded(); @@ -212,6 +193,7 @@ public sealed class VirtualTagEngine : IDisposable /// Evaluate a single tag — used by the timer trigger + test hooks. /// Path of the virtual tag to evaluate. /// Cancellation token to stop evaluation. + /// A task that represents the asynchronous operation. public Task EvaluateOneAsync(string path, CancellationToken ct = default) { EnsureLoaded(); @@ -226,6 +208,7 @@ public sealed class VirtualTagEngine : IDisposable /// evaluation result. /// /// Path of the tag to read. + /// The tag's most recently evaluated value, or a Bad-quality snapshot if the path is unrecognized. public DataValueSnapshot Read(string path) { if (string.IsNullOrWhiteSpace(path)) @@ -242,6 +225,7 @@ public sealed class VirtualTagEngine : IDisposable /// /// Path of the tag to subscribe to. /// Callback invoked with the tag path and new value on each evaluation. + /// An that removes the observer when disposed. public IDisposable Subscribe(string path, Action observer) { // Race-safe pattern paired with Unsub.Dispose: if Unsub.Dispose removed the @@ -308,11 +292,6 @@ public sealed class VirtualTagEngine : IDisposable { if (!_tags.TryGetValue(path, out var state)) return; - // Serial evaluation across all tags. Phase 7 plan decision #19 — parallel is a - // follow-up. The semaphore bounds the evaluation graph so two cascades don't - // interleave, which would break the "earlier nodes computed first" invariant. - // SemaphoreSlim.WaitAsync is async-safe where Monitor.Enter is not (Monitor - // ownership is thread-local and lost across await). await _evalGate.WaitAsync(ct).ConfigureAwait(false); try { @@ -397,7 +376,7 @@ public sealed class VirtualTagEngine : IDisposable /// BadInternalError — the correct sentinel for "script ran but faulted" /// rather than "inputs not yet available". Previously the null-value check /// caused a Good-quality null upstream to permanently block all its dependents at - /// BadWaitingForInitialData (Core.VirtualTags-014). + /// BadWaitingForInitialData. /// private static bool AreInputsReady(IReadOnlyDictionary cache) { @@ -529,8 +508,6 @@ public sealed class VirtualTagEngine : IDisposable UnsubscribeFromUpstream(); _tags.Clear(); _graph.Clear(); - // Dispose every compiled-script ALC so the engine's shutdown actually - // releases the emitted assemblies. (Core.Scripting-016) _compileCache.Dispose(); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs index 86aab2d7..e12ec270 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core.VirtualTags/VirtualTagSource.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags; /// /// Implements the driver-agnostic capability surface the /// DriverNodeManager dispatches to when a node resolves to -/// NodeSource.Virtual per ADR-002. Reads return the engine's last-known +/// NodeSource.Virtual. Reads return the engine's last-known /// evaluation result; subscriptions forward engine-emitted change events as /// events. /// @@ -14,7 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.VirtualTags; /// /// is deliberately not implemented — OPC UA client /// writes to virtual tags are rejected in DriverNodeManager before they -/// reach here per Phase 7 decision #6. Scripts are the only write path, routed +/// reach here. Scripts are the only write path, routed /// through ctx.SetVirtualTag. /// /// @@ -37,12 +37,7 @@ public sealed class VirtualTagSource : IReadable, ISubscribable /// public event EventHandler? OnDataChange; - /// - /// Reads data values asynchronously. - /// - /// The full references to read. - /// The cancellation token. - /// A list of data value snapshots. + /// public Task> ReadAsync( IReadOnlyList fullReferences, CancellationToken cancellationToken) { @@ -53,13 +48,7 @@ public sealed class VirtualTagSource : IReadable, ISubscribable return Task.FromResult>(results); } - /// - /// Subscribes to data changes asynchronously. - /// - /// The full references to subscribe to. - /// The publishing interval. - /// The cancellation token. - /// A subscription handle. + /// public Task SubscribeAsync( IReadOnlyList fullReferences, TimeSpan publishingInterval, @@ -87,12 +76,7 @@ public sealed class VirtualTagSource : IReadable, ISubscribable return Task.FromResult(handle); } - /// - /// Unsubscribes from data changes asynchronously. - /// - /// The subscription handle. - /// The cancellation token. - /// A task representing the asynchronous operation. + /// public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) { if (handle is null) throw new ArgumentNullException(nameof(handle)); @@ -114,9 +98,7 @@ public sealed class VirtualTagSource : IReadable, ISubscribable /// The subscription identifier. public SubscriptionHandle(string id) { DiagnosticId = id; } - /// - /// Gets the diagnostic identifier. - /// + /// public string DiagnosticId { get; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs index 7050f75c..dfd43d26 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/AuthorizationDecision.cs @@ -4,8 +4,8 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums; namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// -/// Tri-state result of an call, per decision -/// #149. Phase 6.2 only produces and +/// Tri-state result of an call. +/// Phase 6.2 only produces and /// ; the /// variant exists in the model so v2.1 Explicit Deny lands without an API break. Provenance /// carries the matched grants (or empty when not granted) for audit + the Admin UI "Probe @@ -19,6 +19,7 @@ public sealed record AuthorizationDecision( public bool IsAllowed => Verdict == AuthorizationVerdict.Allow; /// Convenience constructor for the common "no grants matched" outcome. + /// An authorization decision with the verdict. public static AuthorizationDecision NotGranted() => new(AuthorizationVerdict.NotGranted, []); /// Allow with the list of grants that matched. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs index ea96aeba..27156f4b 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/IPermissionEvaluator.cs @@ -9,7 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// /// Data-plane only. Reads NodeAcl rows joined against the session's resolved LDAP /// groups (via ). Must not depend on the control-plane -/// admin-role mapping table per decision #150 — the two concerns share zero runtime code. +/// admin-role mapping table — the two concerns share zero runtime code. /// public interface IPermissionEvaluator { @@ -22,5 +22,6 @@ public interface IPermissionEvaluator /// The user session containing resolved LDAP groups and roles. /// The OPC UA operation being requested. /// The node address scope being accessed. + /// An authorization decision indicating whether the operation is allowed. AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope); } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/NodeScope.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/NodeScope.cs index 353f677f..285b8835 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/NodeScope.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/NodeScope.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// to which walks the matching trie path. /// /// -/// Per decision #129 and the Phase 6.2 Stream B plan the hierarchy is +/// Per the Phase 6.2 Stream B plan the hierarchy is /// Cluster → Namespace → UnsArea → UnsLine → Equipment → Tag for all /// (Equipment-kind) namespaces. Galaxy is a standard Equipment-kind driver, so Galaxy /// points are ordinary equipment tags that resolve through this same walk. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs index df1ec481..cc2a83e6 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrie.cs @@ -10,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// requested . /// /// -/// Per decision #129 (additive grants, no explicit Deny in v2.0) the walk is pure union: +/// The walk is pure union (additive grants; no explicit Deny in v2.0): /// encountering a grant at any level contributes its flags, never revokes them. A grant at /// the Cluster root therefore cascades to every tag below it; a grant at a deep equipment /// leaf is visible only on that equipment subtree. @@ -33,6 +33,7 @@ public sealed class PermissionTrie /// /// The node scope to match permissions for. /// The user's LDAP group memberships. + /// The matched grants accumulated across every trie level visited for the scope. public IReadOnlyList CollectMatches(NodeScope scope, IEnumerable ldapGroups) { ArgumentNullException.ThrowIfNull(scope); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs index c9f0548a..bccdf49d 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs @@ -37,10 +37,11 @@ public static class PermissionTrieBuilder /// /// Optional callback invoked when a sub-cluster row's ScopeId cannot be located /// in . Production callers should wire a logger here so - /// orphaned grants surface — silently dropping them under the wrong trie level was the - /// Core-011 production hazard. The callback fires only when + /// orphaned grants surface — silently dropping them under the wrong trie level was a + /// production hazard. The callback fires only when /// is non-null (a null lookup is the explicit deterministic-test fallback mode). /// + /// The built for the cluster + generation. public static PermissionTrie Build( string clusterId, long generationId, @@ -128,8 +129,7 @@ public sealed record NodeAclPath(IReadOnlyList Segments); /// /// Diagnostic emitted by when a row could not be /// placed at its structurally-correct trie node. Production callers should log these so -/// orphaned grants surface instead of being silently dropped under an unreachable node -/// (Core-011). +/// orphaned grants surface instead of being silently dropped under an unreachable node. /// /// The offending row's logical id. /// The row's . diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs index 47161e0a..47366ee3 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieCache.cs @@ -10,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// . /// /// -/// Per decision #148 and Phase 6.2 Stream B.4 the cache is generation-sealed: once a +/// Per Phase 6.2 Stream B.4 the cache is generation-sealed: once a /// trie is installed for (ClusterId, GenerationId) the entry is immutable. When a /// new generation publishes, the caller calls with the new trie /// + the cache atomically updates its "current generation" pointer for that cluster. @@ -34,6 +34,7 @@ public sealed class PermissionTrieCache /// Get the current-generation trie for a cluster; null when nothing installed. /// The cluster identifier. + /// The current trie for the cluster, or null when nothing is installed. public PermissionTrie? GetTrie(string clusterId) { ArgumentException.ThrowIfNullOrWhiteSpace(clusterId); @@ -43,6 +44,7 @@ public sealed class PermissionTrieCache /// Get a specific (cluster, generation) trie; null if that pair isn't cached. /// The cluster identifier. /// The generation identifier. + /// The trie for that cluster and generation, or null if that pair isn't cached. public PermissionTrie? GetTrie(string clusterId, long generationId) { if (!_byCluster.TryGetValue(clusterId, out var entry)) return null; @@ -51,6 +53,7 @@ public sealed class PermissionTrieCache /// The generation id the shortcut currently serves for a cluster. /// The cluster identifier. + /// The current generation id for the cluster, or null if nothing is cached. public long? CurrentGenerationId(string clusterId) => _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null; @@ -111,11 +114,13 @@ public sealed class PermissionTrieCache /// Creates a cluster entry from a single trie. /// The permission trie to create the entry from. + /// A new cluster entry whose current trie and generation map both hold only . public static ClusterEntry FromSingle(PermissionTrie trie) => new(trie, new Dictionary { [trie.GenerationId] = trie }); /// Creates a new entry with an additional trie, updating current if it's newer. /// The new permission trie to add. + /// A new cluster entry with the trie merged in and the current pointer updated if needed. public ClusterEntry WithAdditional(PermissionTrie trie) { var next = new Dictionary(Tries) { [trie.GenerationId] = trie }; diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs index a5881c3a..b9ecead1 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs @@ -24,18 +24,12 @@ public sealed class TriePermissionEvaluator : IPermissionEvaluator _timeProvider = timeProvider ?? TimeProvider.System; } - /// Authorizes an operation against the user's session and node scope. - /// The user's authorization session. - /// The OPC UA operation to authorize. - /// The target node scope. - /// An authorization decision indicating whether the operation is allowed. + /// public AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope) { ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(scope); - // Decision #152 — beyond the staleness ceiling every call fails closed regardless of - // cache warmth elsewhere in the process. if (session.IsStale(_timeProvider.GetUtcNow().UtcDateTime)) return AuthorizationDecision.NotGranted(); @@ -45,12 +39,6 @@ public sealed class TriePermissionEvaluator : IPermissionEvaluator var trie = _cache.GetTrie(scope.ClusterId); if (trie is null) return AuthorizationDecision.NotGranted(); - // Decision #153 / Phase 6.2 adversarial-review item #3 (redundancy-safe invalidation): - // the GetTrie shortcut returns whatever generation the cache currently holds, which may - // have advanced past the generation this session was bound to (another node published). - // Evaluate against the session's *bound* generation so a grant added or removed in a - // newer generation cannot silently take effect mid-session, and so the provenance in the - // AuthorizationDecision reports the generation that actually produced the verdict. if (trie.GenerationId != session.AuthGenerationId) { trie = _cache.GetTrie(scope.ClusterId, session.AuthGenerationId); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs index e811109c..53b5069d 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/UserAuthorizationState.cs @@ -6,12 +6,12 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Authorization; /// the membership was resolved against, and the bounded freshness window. /// /// -/// Per decision #151 the membership is bounded by +/// The membership is bounded by /// (default 5 min). After that, the next hot-path authz call re-resolves LDAP group /// memberships; failure to re-resolve (LDAP unreachable) flips the session to fail-closed /// until a refresh succeeds. /// -/// Per decision #152 (default 15 min) is separate from +/// (default 15 min) is separate from /// Phase 6.1's availability-oriented 24h cache — beyond this window the evaluator returns /// regardless of config-cache warmth. /// @@ -42,13 +42,13 @@ public sealed record UserAuthorizationState /// Trie generation the session is currently bound to. When /// moves to a new generation, the session's /// (AuthGenerationId, MembershipVersion) stamp no longer matches its - /// MonitoredItems and they re-evaluate on next publish (decision #153). + /// MonitoredItems and they re-evaluate on next publish. /// public required long AuthGenerationId { get; init; } /// /// Monotonic counter incremented every time membership is re-resolved. Combined with - /// into the subscription stamp per decision #153. + /// into the subscription stamp. /// public required long MembershipVersion { get; init; } @@ -64,6 +64,7 @@ public sealed record UserAuthorizationState /// whenever this is true. /// /// The current UTC time. + /// if the cached membership has exceeded the staleness ceiling. public bool IsStale(DateTime utcNow) => utcNow - MembershipResolvedUtc > AuthCacheMaxStaleness; /// @@ -72,6 +73,7 @@ public sealed record UserAuthorizationState /// call still evaluates against the cached memberships. /// /// The current UTC time. + /// if membership is past its freshness interval but not yet stale, signaling an async refresh is due. public bool NeedsRefresh(DateTime utcNow) => !IsStale(utcNow) && utcNow - MembershipResolvedUtc > MembershipFreshnessInterval; } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistry.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistry.cs index bbade74e..97a5bf4e 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistry.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistry.cs @@ -11,7 +11,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Hosting; /// DriverInstanceId + DriverConfig JSON. /// /// -/// Closes the gap surfaced by task #240 live smoke — DriverInstance rows in +/// Closes a gap where DriverInstance rows in /// the central config DB had no path to materialise as registered /// instances. The factory registry is the seam. /// @@ -63,6 +63,7 @@ public sealed class DriverFactoryRegistry /// missing-assembly deployment doesn't take down the whole server. /// /// The driver type to look up. + /// The registered factory, or if none is registered for . public Func? TryGet(string driverType) { ArgumentException.ThrowIfNullOrWhiteSpace(driverType); @@ -75,6 +76,7 @@ public sealed class DriverFactoryRegistry /// case upstream; we don't double-surface that failure here. /// /// The driver type to look up. + /// The recorded stability tier, or if unknown. public DriverTier GetTier(string driverType) { ArgumentException.ThrowIfNullOrWhiteSpace(driverType); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistryAdapter.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistryAdapter.cs index de24430e..fd44bb96 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistryAdapter.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverFactoryRegistryAdapter.cs @@ -20,16 +20,13 @@ public sealed class DriverFactoryRegistryAdapter : IDriverFactory _registry = registry; } - /// Attempts to create a driver instance by type and configuration. - /// The driver type name. - /// The driver instance identifier. - /// The driver configuration as a JSON string. + /// public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson) { var factory = _registry.TryGet(driverType); return factory?.Invoke(driverInstanceId, driverConfigJson); } - /// Gets the collection of supported driver type names. + /// public IReadOnlyCollection SupportedTypes => _registry.RegisteredTypes; } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs index 9c8dbd86..160512b9 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Hosting/DriverHost.cs @@ -3,8 +3,8 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Hosting; /// -/// Process-local registry + lifecycle manager for loaded instances -/// (decision #65). Phase 1 scaffold — per-process isolation for Tier C drivers (Galaxy, FOCAS) +/// Process-local registry + lifecycle manager for loaded instances. +/// Phase 1 scaffold — per-process isolation for Tier C drivers (Galaxy, FOCAS) /// is implemented in Phase 2 via named-pipe RPC; this class handles in-process drivers today /// and exposes the same registration interface so the Tier C wrapper can slot in later. /// @@ -21,6 +21,7 @@ public sealed class DriverHost : IAsyncDisposable /// Gets the health status of a registered driver. /// The driver instance identifier to query. + /// The driver's health status, or null when not registered. public DriverHealth? GetHealth(string driverInstanceId) { lock (_lock) @@ -33,6 +34,7 @@ public sealed class DriverHost : IAsyncDisposable /// startup. Returns null when the driver is not registered. /// /// The driver instance identifier to look up. + /// The registered driver, or null when not registered. public IDriver? GetDriver(string driverInstanceId) { lock (_lock) @@ -47,6 +49,7 @@ public sealed class DriverHost : IAsyncDisposable /// The driver instance to register. /// The configuration JSON for the driver. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. public async Task RegisterAsync(IDriver driver, string driverConfigJson, CancellationToken ct) { ArgumentNullException.ThrowIfNull(driver); @@ -70,6 +73,7 @@ public sealed class DriverHost : IAsyncDisposable /// Unregisters a driver and calls shutdown. /// The driver instance identifier to unregister. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. public async Task UnregisterAsync(string driverInstanceId, CancellationToken ct) { IDriver? driver; @@ -84,6 +88,7 @@ public sealed class DriverHost : IAsyncDisposable } /// Disposes the driver host and all registered drivers. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { List snapshot; diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs index bd4f0961..b1eb70d1 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/DriverHealthReport.cs @@ -27,6 +27,7 @@ public static class DriverHealthReport { /// Compute the fleet-wide readiness verdict from per-driver states. /// The list of per-driver health snapshots to aggregate. + /// The fleet-wide readiness verdict. public static ReadinessVerdict Aggregate(IReadOnlyList drivers) { ArgumentNullException.ThrowIfNull(drivers); @@ -54,6 +55,7 @@ public static class DriverHealthReport /// return per the Stream C.1 state matrix. /// /// The readiness verdict to map to HTTP status. + /// The HTTP status code the /readyz endpoint should return. public static int HttpStatus(ReadinessVerdict verdict) => verdict switch { ReadinessVerdict.Healthy => 200, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/LogContextEnricher.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/LogContextEnricher.cs index 97f34a83..2e6bc76f 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/LogContextEnricher.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Observability/LogContextEnricher.cs @@ -11,7 +11,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Observability; /// automatically via the Serilog enricher chain. /// /// -/// Per docs/v2/implementation/phase-6-1-resilience-and-observability.md §Stream C.2. /// The correlation ID should be the OPC UA RequestHeader.RequestHandle when in-flight; /// otherwise a short random GUID. Callers supply whichever is available. /// @@ -22,6 +21,7 @@ public static class LogContextEnricher /// The driver type name. /// The driver capability being invoked. /// The correlation ID for tracing the call. + /// A disposable scope that pops the pushed properties when disposed. public static IDisposable Push(string driverInstanceId, string driverType, DriverCapability capability, string correlationId) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); @@ -40,6 +40,7 @@ public static class LogContextEnricher /// 12-hex-char slice of a GUID — long enough for log correlation, short enough to /// scan visually. /// + /// A 12-hex-character correlation ID. public static string NewCorrelationId() => Guid.NewGuid().ToString("N")[..12]; private sealed class CompositeScope : IDisposable diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs index 58ea16e8..217cca88 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/EquipmentNodeWalker.cs @@ -10,12 +10,12 @@ namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa; /// UnsArea / UnsLine / Equipment / Tag rows. Runs during /// address-space build per whose /// Namespace.Kind = Equipment; non-Equipment namespaces are -/// exempt per decision #120 and reach this walker only indirectly through +/// exempt and reach this walker only indirectly through /// . /// /// /// -/// Composition strategy. ADR-001 (2026-04-20) accepted Option A — Config +/// Composition strategy. Accepted Option A — Config /// primary. The walker treats the supplied /// snapshot as the authoritative published surface. Every Equipment row becomes a /// folder node at the UNS level-5 segment; every bound to an @@ -26,12 +26,12 @@ namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa; /// /// /// -/// Under each Equipment node. Five identifier properties per decision #121 +/// Under each Equipment node. Five identifier properties /// (EquipmentId, EquipmentUuid, MachineCode, ZTag, /// SAPID) are added as OPC UA properties — external systems (ERP, SAP PM) /// resolve equipment by whichever identifier they natively use without a sidecar. /// materializes the OPC 40010 -/// Identification sub-folder with the nine decision-#139 fields when at least one +/// Identification sub-folder with the nine fields when at least one /// is non-null; when all nine are null the sub-folder is omitted rather than /// appearing empty. /// @@ -131,7 +131,7 @@ public static class EquipmentNodeWalker } /// - /// Adds the five operator-facing identifiers from decision #121 as OPC UA properties + /// Adds the five operator-facing identifiers as OPC UA properties /// on the Equipment node. EquipmentId + EquipmentUuid are always populated; /// MachineCode is required per ; ZTag + SAPID are nullable in /// the data model so they're skipped when null to avoid empty-string noise in the @@ -152,7 +152,7 @@ public static class EquipmentNodeWalker /// Emit a single Tag row as an . The driver /// full reference lives in Tag.TagConfig (wire-level address, driver-specific /// JSON blob); the variable node's data type derives from Tag.DataType. - /// Unreachable-address behavior per ADR-001 Option A: the variable is created; the + /// Unreachable-address behavior: the variable is created; the /// driver's natural Read failure surfaces an OPC UA Bad status at runtime. /// private static void AddTagVariable(IAddressSpaceBuilder equipmentBuilder, Tag tag) @@ -189,6 +189,7 @@ public static class EquipmentNodeWalker /// wants an opaque non-JSON reference. /// /// The tag configuration JSON or string. + /// The extracted FullName value, or the raw if it can't be extracted. internal static string ExtractFullName(string tagConfig) { if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig; @@ -208,7 +209,7 @@ public static class EquipmentNodeWalker /// /// Parse (stored as the enum - /// name string, decision #138) into the enum value. Unknown names fall back to + /// name string) into the enum value. Unknown names fall back to /// so a one-off driver-specific type doesn't /// abort the whole walk; the underlying driver still sees the original TagConfig /// address + can surface its own typed value via the OPC UA variant at read time. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs index 6399075d..6b9aaf89 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/GenericDriverNodeManager.cs @@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa; /// against CustomNodeManager2. /// /// -/// Per docs/v2/plan.md decision #52 + #62 — Core owns the node tree, drivers stream +/// Per docs/v2/plan.md — Core owns the node tree, drivers stream /// Folder/Variable calls, alarm-bearing variables are annotated via /// and subsequent /// payloads route to the sink the builder returned. @@ -41,7 +41,7 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable /// If called a second time (e.g. Galaxy redeploy via IRediscoverable.OnRediscoveryNeeded) /// the previous alarm subscription is torn down and the sink registry is cleared before /// re-walking, preventing double delivery of alarm transitions. - /// Exception isolation (per decision #12 — marking the driver's subtree Faulted while other + /// Exception isolation (marking the driver's subtree Faulted while other /// drivers stay available) is the caller's responsibility; exceptions from /// propagate unhandled to the caller. The Server /// project's OpcUaApplicationHost.PopulateAddressSpaces wraps this call in a per-driver @@ -49,6 +49,7 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable /// /// The address space builder to populate. /// The cancellation token. + /// A task that represents the asynchronous operation. public async Task BuildAddressSpaceAsync(IAddressSpaceBuilder builder, CancellationToken ct) { ArgumentNullException.ThrowIfNull(builder); @@ -111,23 +112,15 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable IAddressSpaceBuilder inner, ConcurrentDictionary sinks) : IAddressSpaceBuilder { - /// Adds a folder to the address space. - /// The browse name of the folder node. - /// The display name of the folder node. + /// public IAddressSpaceBuilder Folder(string browseName, string displayName) => new CapturingBuilder(inner.Folder(browseName, displayName), sinks); - /// Adds a variable to the address space. - /// The browse name of the variable node. - /// The display name of the variable node. - /// Metadata describing the variable's data type and properties. + /// public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo) => new CapturingHandle(inner.Variable(browseName, displayName, attributeInfo), sinks); - /// Adds a property to the address space. - /// The browse name of the property node. - /// The OPC UA data type of the property. - /// The initial value of the property, or null. + /// public void AddProperty(string browseName, DriverDataType dataType, object? value) => inner.AddProperty(browseName, dataType, value); } @@ -136,11 +129,10 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable IVariableHandle inner, ConcurrentDictionary sinks) : IVariableHandle { - /// Gets the full reference for the variable. + /// public string FullReference => inner.FullReference; - /// Marks the variable as an alarm condition and registers its sink. - /// Configuration for the alarm condition. + /// public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) { var sink = inner.MarkAsAlarmCondition(info); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs index 982d26d9..c84a10c3 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/OpcUa/IdentificationFolderBuilder.cs @@ -5,7 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa; /// /// Phase 6.4 Stream D: materializes the OPC 40010 Machinery companion-spec Identification -/// sub-folder under an Equipment node. Reads the nine decision-#139 columns off the +/// sub-folder under an Equipment node. Reads the nine documented columns off the /// row and emits one property per non-null field. /// /// @@ -19,7 +19,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.OpcUa; /// grant gets BadUserAccessDenied on both the Equipment node + its Identification variables. /// See docs/v2/acl-design.md §Identification cross-reference. /// -/// The nine fields per decision #139 are exposed exactly when they carry a non-null +/// The nine fields are exposed exactly when they carry a non-null /// value. A row with all nine null produces no Identification sub-folder at all — the /// caller can use to skip the Folder call entirely /// and avoid a pointless empty folder appearing in browse trees. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs index c2e2c24a..193e5844 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/AlarmSurfaceInvoker.cs @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Resilience; /// ) through so the /// Phase 6.1 resilience pipeline runs — retry semantics match /// (retries by default) and -/// (does NOT retry per decision #143). +/// (does NOT retry). /// /// /// Multi-host dispatch: when the driver implements , @@ -59,6 +59,7 @@ public sealed class AlarmSurfaceInvoker /// /// The source node IDs to subscribe to. /// The cancellation token. + /// One subscription handle per resolved host. public async Task> SubscribeAsync( IReadOnlyList sourceNodeIds, CancellationToken cancellationToken) @@ -89,6 +90,7 @@ public sealed class AlarmSurfaceInvoker /// /// The subscription handle to unsubscribe. /// The cancellation token. + /// A task that represents the asynchronous operation. public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(handle); @@ -105,11 +107,12 @@ public sealed class AlarmSurfaceInvoker /// /// Acknowledge alarms. Fans out by resolved host; each host's batch runs through the - /// AlarmAcknowledge pipeline (no-retry per decision #143 — an alarm-ack is not idempotent + /// AlarmAcknowledge pipeline (no-retry — an alarm-ack is not idempotent /// at the plant-floor acknowledgement level even if the OPC UA spec permits re-issue). /// /// The alarm acknowledgement requests. /// The cancellation token. + /// A task that represents the asynchronous operation. public async Task AcknowledgeAsync( IReadOnlyList acknowledgements, CancellationToken cancellationToken) @@ -161,7 +164,7 @@ public sealed class AlarmSurfaceInvoker public IAlarmSubscriptionHandle Inner { get; } = inner; /// Gets the resolved host name. public string Host { get; } = host; - /// Gets the diagnostic ID from the inner handle. + /// public string DiagnosticId => Inner.DiagnosticId; } } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs index 42fc65fe..e4e42922 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs @@ -10,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Resilience; /// is process-singleton so all invokers share its cache. /// /// -/// Per docs/v2/plan.md decisions #143-144 and Phase 6.1 Stream A.3. The server's dispatch +/// The server's dispatch /// layer routes every capability call (IReadable.ReadAsync, IWritable.WriteAsync, /// ITagDiscovery.DiscoverAsync, ISubscribable.SubscribeAsync/UnsubscribeAsync, /// IHostConnectivityProbe probe loop, IAlarmSource.SubscribeAlarmsAsync/AcknowledgeAsync, @@ -58,6 +58,7 @@ public sealed class CapabilityInvoker /// The host name for logging and status tracking. /// The async function to execute. /// Cancellation token for the operation. + /// The result of the underlying driver call. public async ValueTask ExecuteAsync( DriverCapability capability, string hostName, @@ -86,6 +87,7 @@ public sealed class CapabilityInvoker /// The host name for logging and status tracking. /// The async function to execute. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. public async ValueTask ExecuteAsync( DriverCapability capability, string hostName, @@ -112,8 +114,8 @@ public sealed class CapabilityInvoker /// /// Execute a call honoring /// semantics — if is false, retries are disabled regardless - /// of the tag-level configuration (the pipeline for a non-idempotent write never retries per - /// decisions #44-45). If true, the call runs through the capability's pipeline which may + /// of the tag-level configuration (the pipeline for a non-idempotent write never retries). + /// If true, the call runs through the capability's pipeline which may /// retry when the tier configuration permits. /// /// Return type of the underlying driver call. @@ -121,6 +123,7 @@ public sealed class CapabilityInvoker /// Whether the write operation is idempotent. /// The async function to execute. /// Cancellation token for the operation. + /// The result of the underlying driver write call. public async ValueTask ExecuteWriteAsync( string hostName, bool isIdempotent, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceOptions.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceOptions.cs index af6afccc..ad97bca9 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceOptions.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceOptions.cs @@ -5,7 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Resilience; /// /// Per-tier × per-capability resilience policy configuration for a driver instance. /// Bound from DriverInstance.ResilienceConfig JSON (nullable column; null = tier defaults). -/// Per docs/v2/plan.md decisions #143 and #144. +/// Per docs/v2/plan.md. /// public sealed record DriverResilienceOptions { @@ -34,7 +34,7 @@ public sealed record DriverResilienceOptions /// indefinitely unless a memory breach or operator action triggers a recycle. Only /// respected for ; Tier A/B recycle would tear down every /// OPC UA session, so the loader ignores non-null values for those tiers + logs a - /// warning (per decisions #74 / #145). + /// warning. /// public int? RecycleIntervalSeconds { get; init; } @@ -68,7 +68,7 @@ public sealed record DriverResilienceOptions } /// - /// Per-tier per-capability default policy table, per decisions #143-144 and the Phase 6.1 + /// Per-tier per-capability default policy table, per the Phase 6.1 /// Stream A.2 specification. Retries skipped on and /// regardless of tier. /// @@ -119,6 +119,6 @@ public sealed record DriverResilienceOptions /// Number of retry attempts after the first failure; zero = no retry. /// /// Consecutive-failure count that opens the circuit breaker; zero = no breaker -/// (Tier C uses the supervisor's process-level breaker instead, per decision #68). +/// (Tier C uses the supervisor's process-level breaker instead). /// public sealed record CapabilityPolicy(int TimeoutSeconds, int RetryCount, int BreakerFailureThreshold); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceOptionsParser.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceOptionsParser.cs index 168d6677..fa9690b2 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceOptionsParser.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceOptionsParser.cs @@ -50,6 +50,7 @@ public static class DriverResilienceOptionsParser /// The driver tier for default resilience options. /// The optional JSON configuration string to parse. /// An out parameter containing diagnostic information if parsing fails. + /// The effective , layering any parsed overrides on top of the tier defaults. public static DriverResilienceOptions ParseOrDefaults( DriverTier tier, string? resilienceConfigJson, @@ -96,7 +97,7 @@ public static class DriverResilienceOptionsParser // Scheduled recycle is Tier C only — reject a configured interval on Tier A/B as a // misconfiguration surface rather than silently honouring it (recycling an in-process - // driver would kill every OPC UA session + every co-hosted driver, per decision #74). + // driver would kill every OPC UA session + every co-hosted driver). int? recycleIntervalSeconds = null; if (shape.RecycleIntervalSeconds is int secs) { diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs index 99fbcde7..03f36ef3 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResiliencePipelineBuilder.cs @@ -13,7 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Resilience; /// driver cannot open the circuit breaker for healthy sibling hosts. /// /// -/// Per docs/v2/plan.md decision #144 (per-device isolation). Composition from outside-in: +/// Per-device isolation. Composition from outside-in: /// Timeout → Retry (when capability permits) → Circuit Breaker (when tier permits) → Bulkhead. /// /// Pipeline resolution is lock-free on the hot path: the inner @@ -54,6 +54,7 @@ public sealed class DriverResiliencePipelineBuilder /// /// Which capability surface is being called. /// Per-driver-instance options (tier + per-capability overrides). + /// The cached or newly built resilience pipeline for the key. public ResiliencePipeline GetOrCreate( string driverInstanceId, string hostName, diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs index 1e32128a..c2fdeda7 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/DriverResilienceStatusTracker.cs @@ -128,10 +128,12 @@ public sealed class DriverResilienceStatusTracker /// Snapshot of a specific (instance, host) pair; null if no counters recorded yet. /// The driver instance identifier. /// The host name. + /// The current resilience status snapshot, or null if no counters have been recorded for this pair. public ResilienceStatusSnapshot? TryGet(string driverInstanceId, string hostName) => _status.TryGetValue(new StatusKey(driverInstanceId, hostName), out var snapshot) ? snapshot : null; /// Copy of every currently-tracked (instance, host, snapshot) triple. Safe under concurrent writes. + /// A snapshot list of all tracked (driver instance, host, resilience snapshot) triples. public IReadOnlyList<(string DriverInstanceId, string HostName, ResilienceStatusSnapshot Snapshot)> Snapshot() => _status.Select(kvp => (kvp.Key.DriverInstanceId, kvp.Key.HostName, kvp.Value)).ToList(); diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs index 5963f36c..c0e89855 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryRecycle.cs @@ -9,7 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Stability; /// to restart the out-of-process Host. /// /// -/// Per docs/v2/plan.md decisions #74 and #145. Tier A/B hard-breach on an in-process +/// Per docs/v2/plan.md. Tier A/B hard-breach on an in-process /// driver would kill every OPC UA session and every co-hosted driver, so for Tier A/B this /// class logs a promotion-to-Tier-C recommendation and does NOT invoke any supervisor. /// A future tier-migration workflow acts on the recommendation. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryTracking.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryTracking.cs index 9dad09ee..921b4929 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryTracking.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/MemoryTracking.cs @@ -6,11 +6,11 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Stability; /// Tier-agnostic memory-footprint tracker. Captures the post-initialize baseline /// from the first samples after IDriver.InitializeAsync, then classifies each /// subsequent sample against a hybrid soft/hard threshold per -/// docs/v2/plan.md decision #146 — soft = max(multiplier × baseline, baseline + floor), +/// docs/v2/plan.mdsoft = max(multiplier × baseline, baseline + floor), /// hard = 2 × soft. /// /// -/// Per decision #145, this tracker never kills a process. Soft and hard breaches +/// This tracker never kills a process. Soft and hard breaches /// log + surface to the Admin UI via DriverInstanceResilienceStatus. The matching /// process-level recycle protection lives in a separate MemoryRecycle that activates /// for Tier C drivers only (where the driver runs out-of-process behind a supervisor that @@ -31,8 +31,9 @@ public sealed class MemoryTracking private TrackingPhase _phase = TrackingPhase.WarmingUp; private DateTime? _warmupStartUtc; - /// Tier-default multiplier/floor constants per decision #146. + /// Tier-default multiplier/floor constants. /// The driver tier. + /// The multiplier and floor-bytes constants for the tier. public static (int Multiplier, long FloorBytes) GetTierConstants(DriverTier tier) => tier switch { DriverTier.A => (Multiplier: 3, FloorBytes: 50L * 1024 * 1024), @@ -73,6 +74,7 @@ public sealed class MemoryTracking /// /// The current memory footprint in bytes. /// The current UTC time. + /// The tracking action the caller should surface for this sample. public MemoryTrackingAction Sample(long footprintBytes, DateTime utcNow) { if (_phase == TrackingPhase.WarmingUp) @@ -136,7 +138,7 @@ public enum MemoryTrackingAction /// /// ≥ hard threshold. Log + surface + (Tier C only, via MemoryRecycle) request /// process recycle via the driver supervisor. Tier A/B breach never invokes any - /// kill path per decisions #145 and #74. + /// kill path. /// HardBreach, } diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs index 29e60a2d..e92b2e93 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/ScheduledRecycleScheduler.cs @@ -4,7 +4,7 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Stability; /// -/// Tier C opt-in periodic-recycle driver per docs/v2/plan.md decision #67. +/// Tier C opt-in periodic-recycle driver. /// A tick method advanced by the caller (fed by a background timer in prod; by test clock /// in unit tests) decides whether the configured interval has elapsed and, if so, drives the /// supplied to recycle the Host. diff --git a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/WedgeDetector.cs b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/WedgeDetector.cs index 1c518dd0..28772675 100644 --- a/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/WedgeDetector.cs +++ b/src/Core/ZB.MOM.WW.OtOpcUa.Core/Stability/WedgeDetector.cs @@ -3,7 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Core.Stability; /// -/// Demand-aware driver-wedge detector per docs/v2/plan.md decision #147. +/// Demand-aware driver-wedge detector per docs/v2/plan.md. /// Flips a driver to only when BOTH of the following hold: /// (a) there is pending work outstanding, AND (b) no progress has been observed for longer /// than . Idle drivers, write-only burst drivers, and subscription-only diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/AbCipCommandBase.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/AbCipCommandBase.cs index e5d9e42a..5631d413 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/AbCipCommandBase.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/AbCipCommandBase.cs @@ -30,15 +30,6 @@ public abstract class AbCipCommandBase : DriverCommandBase public int TimeoutMs { get; init; } = 5000; /// - /// - /// The getter validates (Driver.AbCip.Cli-004) — a zero or - /// negative --timeout-ms would otherwise propagate as a non-positive - /// into the driver. The init accessor is unreachable - /// because CliFx binds rather than Timeout; it throws - /// so an object-initializer assignment - /// (new ReadCommand { Timeout = ... }) fails fast instead of being silently - /// discarded (Driver.AbCip.Cli-006). - /// public override TimeSpan Timeout { get @@ -60,6 +51,7 @@ public abstract class AbCipCommandBase : DriverCommandBase /// probe loop would race the operator's own reads. /// /// The list of tag definitions to include in the options. + /// The constructed . protected AbCipDriverOptions BuildOptions(IReadOnlyList tags) => new() { Devices = [new AbCipDeviceOptions( diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/ProbeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/ProbeCommand.cs index bbe037ef..a0cf284f 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/ProbeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/ProbeCommand.cs @@ -57,8 +57,8 @@ public sealed class ProbeCommand : AbCipCommandBase finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.AbCip.Cli-005 — flush Serilog before process exit so buffered log - // output emitted during driver shutdown is not lost. + // Flush Serilog before process exit so buffered log output emitted during + // driver shutdown is not lost. FlushLogging(); } } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/ReadCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/ReadCommand.cs index 69f1421a..0d0bbb80 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/ReadCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/ReadCommand.cs @@ -26,8 +26,6 @@ public sealed class ReadCommand : AbCipCommandBase "String / Dt (default DInt). UDT / composite types are not supported here — use a full driver config.")] public AbCipDataType DataType { get; init; } = AbCipDataType.DInt; - /// Executes the read operation. - /// The console for output and cancellation handling. /// public override async ValueTask ExecuteAsync(IConsole console) { @@ -54,8 +52,8 @@ public sealed class ReadCommand : AbCipCommandBase finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.AbCip.Cli-005 — flush Serilog before process exit so buffered log - // output emitted during driver shutdown is not lost. + // Flush Serilog before process exit so buffered log output emitted during + // driver shutdown is not lost. FlushLogging(); } } @@ -66,6 +64,7 @@ public sealed class ReadCommand : AbCipCommandBase /// /// The symbolic tag path. /// The data type. + /// A synthetic tag name combining the tag path and data type. internal static string SynthesiseTagName(string tagPath, AbCipDataType type) => $"{tagPath}:{type}"; } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/SubscribeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/SubscribeCommand.cs index d2811e84..b266fa3d 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/SubscribeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/SubscribeCommand.cs @@ -36,8 +36,6 @@ public sealed class SubscribeCommand : AbCipCommandBase ConfigureLogging(); RejectStructure(DataType); ValidateInterval(IntervalMs); - // Touch Timeout to surface the --timeout-ms guard (Driver.AbCip.Cli-004) before - // we open a driver — fast-fail with a clean CommandException on bad operator input. _ = Timeout; var ct = console.RegisterCancellationHandler(); @@ -56,10 +54,6 @@ public sealed class SubscribeCommand : AbCipCommandBase { await driver.InitializeAsync("{}", ct); - // Driver.AbCip.Cli-003 — emit the banner BEFORE wiring OnDataChange so the - // main-thread write cannot interleave with poll-thread change-event writes. - // TextWriter.WriteLine is not guaranteed thread-safe; once the handler is - // attached and SubscribeAsync starts, change events run on the poll thread. await console.Output.WriteLineAsync( $"Subscribed to {TagPath} @ {IntervalMs}ms. Ctrl+C to stop."); @@ -90,14 +84,12 @@ public sealed class SubscribeCommand : AbCipCommandBase catch { /* teardown best-effort */ } } await driver.ShutdownAsync(CancellationToken.None); - // Driver.AbCip.Cli-005 — flush Serilog before process exit so buffered log - // lines emitted just before Ctrl+C are not lost on abrupt termination. FlushLogging(); } } /// - /// Guards --interval-ms against zero or negative values (Driver.AbCip.Cli-004). + /// Guards --interval-ms against zero or negative values. /// A non-positive interval would produce a non-positive into /// SubscribeAsync; the CLI should fail fast with an actionable error rather /// than relying on the downstream PollGroupEngine to clamp the value. diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/WriteCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/WriteCommand.cs index 37adf83a..e4dadd38 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/WriteCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Cli/Commands/WriteCommand.cs @@ -64,7 +64,7 @@ public sealed class WriteCommand : AbCipCommandBase finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.AbCip.Cli-005 — flush Serilog before process exit so buffered log + // Flush Serilog before process exit so buffered log // output emitted during driver shutdown is not lost. FlushLogging(); } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/ProbeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/ProbeCommand.cs index a92b5414..c1ae0737 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/ProbeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/ProbeCommand.cs @@ -58,7 +58,7 @@ public sealed class ProbeCommand : AbLegacyCommandBase finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.AbLegacy.Cli-008 — flush Serilog before process exit so buffered + // Flush Serilog before process exit so buffered // log output emitted during driver shutdown is not lost. FlushLogging(); } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/ReadCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/ReadCommand.cs index ab8ed216..48c381c5 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/ReadCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/ReadCommand.cs @@ -52,7 +52,7 @@ public sealed class ReadCommand : AbLegacyCommandBase finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.AbLegacy.Cli-008 — flush Serilog before process exit so buffered + // Flush Serilog before process exit so buffered // log output emitted during driver shutdown is not lost. FlushLogging(); } @@ -61,6 +61,7 @@ public sealed class ReadCommand : AbLegacyCommandBase /// Tag-name key the driver uses internally. Address+type is already unique. /// The PCCC file address. /// The data type of the address. + /// The synthesised tag-name key combining and . internal static string SynthesiseTagName(string address, AbLegacyDataType type) => $"{address}:{type}"; } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/SubscribeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/SubscribeCommand.cs index bccb7bb6..e2e41aac 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/SubscribeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/SubscribeCommand.cs @@ -93,10 +93,6 @@ public sealed class SubscribeCommand : AbLegacyCommandBase catch { /* teardown best-effort */ } } await driver.ShutdownAsync(CancellationToken.None); - // Driver.AbLegacy.Cli-008 — flush Serilog before process exit so buffered - // log output emitted during driver shutdown is not lost. Particularly - // important here: subscribe runs until Ctrl+C; without this, lines logged - // during shutdown can be silently dropped. FlushLogging(); } } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/WriteCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/WriteCommand.cs index 41e58913..2a414bfc 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/WriteCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Cli/Commands/WriteCommand.cs @@ -62,8 +62,6 @@ public sealed class WriteCommand : AbLegacyCommandBase finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.AbLegacy.Cli-008 — flush Serilog before process exit so buffered - // log output emitted during driver shutdown is not lost. FlushLogging(); } } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/SnapshotFormatter.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/SnapshotFormatter.cs index 2f5c5654..906fee8e 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/SnapshotFormatter.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Cli.Common/SnapshotFormatter.cs @@ -23,6 +23,7 @@ public static class SnapshotFormatter /// /// The tag name to include in the output. /// The data value snapshot to format. + /// The multi-line rendered snapshot. public static string Format(string tagName, DataValueSnapshot snapshot) { ArgumentNullException.ThrowIfNull(snapshot); @@ -42,6 +43,7 @@ public static class SnapshotFormatter /// /// The tag name to include in the output. /// The write result to format. + /// The rendered write-result line. public static string FormatWrite(string tagName, WriteResult result) { ArgumentNullException.ThrowIfNull(result); @@ -54,6 +56,7 @@ public static class SnapshotFormatter /// /// The list of tag names to include as rows. /// The list of data value snapshots to format. + /// The rendered table as a multi-line string. public static string FormatTable( IReadOnlyList tagNames, IReadOnlyList snapshots) { diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/ProbeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/ProbeCommand.cs index 111084e4..d2d46458 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/ProbeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/ProbeCommand.cs @@ -27,7 +27,6 @@ public sealed class ProbeCommand : FocasCommandBase public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); - // Driver.FOCAS.Cli-003: validate numeric option ranges before any driver work. ValidateOptions(); var ct = console.RegisterCancellationHandler(); @@ -39,9 +38,6 @@ public sealed class ProbeCommand : FocasCommandBase Writable: false); var options = BuildOptions([probeTag]); - // Driver.FOCAS.Cli-004: `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync - // already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None) - // in a finally block ran shutdown twice. The await-using on the next line is enough. await using var driver = new FocasDriver(options, DriverInstanceId); try { @@ -59,9 +55,6 @@ public sealed class ProbeCommand : FocasCommandBase } finally { - // Driver.FOCAS.Cli-006: flush Serilog before process exit so buffered log output - // emitted during driver shutdown is not silently dropped (matching DriverCommandBase - // docs and every sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT). FlushLogging(); } } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/ReadCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/ReadCommand.cs index dc3584ef..df042344 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/ReadCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/ReadCommand.cs @@ -22,12 +22,11 @@ public sealed class ReadCommand : FocasCommandBase "Bit / Byte / Int16 / Int32 / Float32 / Float64 / String (default Int16).")] public FocasDataType DataType { get; init; } = FocasDataType.Int16; - /// Executes the read command against the FOCAS device. /// public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); - // Driver.FOCAS.Cli-003: validate numeric option ranges before any driver work. + // Validate numeric option ranges before any driver work. ValidateOptions(); var ct = console.RegisterCancellationHandler(); @@ -40,7 +39,7 @@ public sealed class ReadCommand : FocasCommandBase Writable: false); var options = BuildOptions([tag]); - // Driver.FOCAS.Cli-004: `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync + // `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync // already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None) // in a finally block ran shutdown twice. The await-using on the next line is enough. await using var driver = new FocasDriver(options, DriverInstanceId); @@ -52,7 +51,7 @@ public sealed class ReadCommand : FocasCommandBase } finally { - // Driver.FOCAS.Cli-006: flush Serilog before process exit so buffered log output + // Flush Serilog before process exit so buffered log output // emitted during driver shutdown is not silently dropped (matching DriverCommandBase // docs and every sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT). FlushLogging(); @@ -62,6 +61,7 @@ public sealed class ReadCommand : FocasCommandBase /// Constructs a tag name from address and data type. /// The FOCAS address. /// The data type. + /// The synthesised tag name in address:type form. internal static string SynthesiseTagName(string address, FocasDataType type) => $"{address}:{type}"; } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/SubscribeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/SubscribeCommand.cs index 9f053e96..1a3cb5f4 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/SubscribeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/SubscribeCommand.cs @@ -29,7 +29,7 @@ public sealed class SubscribeCommand : FocasCommandBase public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); - // Driver.FOCAS.Cli-003: validate numeric option ranges (including the subscribe-only + // Validate numeric option ranges (including the subscribe-only // --interval-ms) before any driver work so a zero/negative interval surfaces as a // clean CommandException rather than a tight-spinning poll loop. ValidateOptions(IntervalMs); @@ -44,12 +44,12 @@ public sealed class SubscribeCommand : FocasCommandBase Writable: false); var options = BuildOptions([tag]); - // Driver.FOCAS.Cli-004: `await using` is the sole driver-disposal mechanism — FocasDriver.DisposeAsync + // `await using` is the sole driver-disposal mechanism — FocasDriver.DisposeAsync // already invokes ShutdownAsync, so a redundant ShutdownAsync(CancellationToken.None) in finally // ran shutdown twice. Only UnsubscribeAsync stays in the finally block — that's a subscription // lifecycle concern that is not part of driver disposal. await using var driver = new FocasDriver(options, DriverInstanceId); - // Driver.FOCAS.Cli-002: serialize console writes from the PollGroupEngine background + // Serialize console writes from the PollGroupEngine background // thread so overlapping poll ticks (and the "Subscribed to ..." banner from the CliFx // invocation thread) can't interleave partial lines. var writeLock = new object(); @@ -58,7 +58,7 @@ public sealed class SubscribeCommand : FocasCommandBase { await driver.InitializeAsync("{}", ct); - // Driver.FOCAS.Cli-002: route every data-change event to the CliFx console (not + // Route every data-change event to the CliFx console (not // System.Console — the analyzer flags it + IConsole is the testable abstraction). // The handler is synchronous because OnDataChange is raised from a driver // background thread; the IConsole.Output writer is not documented as thread-safe @@ -89,7 +89,7 @@ public sealed class SubscribeCommand : FocasCommandBase handle = await driver.SubscribeAsync([tagName], TimeSpan.FromMilliseconds(IntervalMs), ct); - // Driver.FOCAS.Cli-002: hold the lock around the banner write so the first + // Hold the lock around the banner write so the first // poll-driven change line from the driver tick thread can't interleave with // this banner. lock (writeLock) @@ -110,7 +110,7 @@ public sealed class SubscribeCommand : FocasCommandBase { if (handle is not null) { - // Driver.FOCAS.Cli-002: stop the subscription before disposal — UnsubscribeAsync + // Stop the subscription before disposal — UnsubscribeAsync // halts the poll-group ticker so no further OnDataChange events fire. The // anonymous handler is never explicitly removed via -=; instead, driver disposal // (via `await using` immediately after this finally) tears down the PollGroupEngine, @@ -119,9 +119,9 @@ public sealed class SubscribeCommand : FocasCommandBase try { await driver.UnsubscribeAsync(handle, CancellationToken.None); } catch { /* teardown best-effort */ } } - // Driver.FOCAS.Cli-006: flush Serilog before process exit so buffered log output - // emitted during driver shutdown is not silently dropped (matching DriverCommandBase - // docs and every sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT). + // Flush Serilog before process exit so buffered log output emitted during driver + // shutdown is not silently dropped (matching DriverCommandBase docs and every + // sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT). FlushLogging(); } } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/WriteCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/WriteCommand.cs index 2c45712b..bd383041 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/WriteCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/Commands/WriteCommand.cs @@ -33,9 +33,9 @@ public sealed class WriteCommand : FocasCommandBase public override async ValueTask ExecuteAsync(IConsole console) { ConfigureLogging(); - // Driver.FOCAS.Cli-003: validate numeric option ranges before any driver work so - // a zero/negative port/timeout surfaces as a clean CommandException rather than an - // opaque downstream exception. + // Validate numeric option ranges before any driver work so a zero/negative + // port/timeout surfaces as a clean CommandException rather than an opaque + // downstream exception. ValidateOptions(); var ct = console.RegisterCancellationHandler(); @@ -50,8 +50,8 @@ public sealed class WriteCommand : FocasCommandBase var parsed = ParseValue(Value, DataType); - // Driver.FOCAS.Cli-004: `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync - // already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None) + // `await using` is the sole disposal mechanism — FocasDriver.DisposeAsync already + // invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None) // in a finally block ran shutdown twice. The await-using on the next line is enough. await using var driver = new FocasDriver(options, DriverInstanceId); try @@ -62,16 +62,16 @@ public sealed class WriteCommand : FocasCommandBase } finally { - // Driver.FOCAS.Cli-006: flush Serilog before process exit so buffered log output - // emitted during driver shutdown is not silently dropped (matching DriverCommandBase - // docs and every sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT). + // Flush Serilog before process exit so buffered log output emitted during driver + // shutdown is not silently dropped (matching DriverCommandBase docs and every + // sibling CLI — Modbus / AbCip / AbLegacy / TwinCAT). FlushLogging(); } } /// Parse --value per , invariant culture throughout. /// - /// Driver.FOCAS.Cli-001: numeric parses are wrapped so that malformed input + /// Numeric parses are wrapped so that malformed input /// ( / ) surfaces /// as a clean rather than a raw /// .NET stack trace — matching the friendly message the Bit path already produces. diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/FocasCommandBase.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/FocasCommandBase.cs index cfa9957f..bc994a50 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/FocasCommandBase.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli/FocasCommandBase.cs @@ -47,6 +47,7 @@ public abstract class FocasCommandBase : DriverCommandBase /// as BadCommunicationError. /// /// The tag definitions to include in the driver options. + /// A configured with a single device and the supplied tags. protected FocasDriverOptions BuildOptions(IReadOnlyList tags) => new() { Devices = [new FocasDeviceOptions( @@ -62,7 +63,7 @@ public abstract class FocasCommandBase : DriverCommandBase protected string DriverInstanceId => $"focas-cli-{CncHost}:{CncPort}"; /// - /// Driver.FOCAS.Cli-003: validate numeric option ranges at the CLI boundary so a + /// Validates numeric option ranges at the CLI boundary so a /// zero/negative --cnc-port, --timeout-ms, or --interval-ms /// surfaces as a clean rather than /// either an opaque downstream exception (invalid focas://host:<n> / diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ProbeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ProbeCommand.cs index a2b83cac..b8f2aee2 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ProbeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ProbeCommand.cs @@ -43,7 +43,7 @@ public sealed class ProbeCommand : ModbusCommandBase var snapshot = await driver.ReadAsync(["__probe"], ct); var health = driver.GetHealth(); - // Driver.Modbus.Cli-006: derive the headline verdict from BOTH the driver state + // Derive the headline verdict from BOTH the driver state // AND the probe-read StatusCode so the operator never sees the previous // contradictory pair (`Health: Healthy` over a Bad snapshot line). The bare // driver state is still printed below for diagnostics, but the verdict is what @@ -61,21 +61,21 @@ public sealed class ProbeCommand : ModbusCommandBase } catch (OperationCanceledException) when (ct.IsCancellationRequested) { - // Driver.Modbus.Cli-005: Ctrl+C during InitializeAsync — exit quietly so CliFx + // Ctrl+C during InitializeAsync — exit quietly so CliFx // does not render a full stack trace for a user-initiated cancellation. await console.Output.WriteLineAsync("Cancelled."); } finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.Modbus.Cli-009: flush Serilog before process exit so buffered log lines + // Flush Serilog before process exit so buffered log lines // emitted during driver shutdown are not lost. Matches Driver.AbCip.Cli pattern. FlushLogging(); } } /// - /// Driver.Modbus.Cli-006: combine the driver-side with the + /// Combine the driver-side with the /// probe snapshot's OPC UA StatusCode into a single headline verdict. Order /// of precedence: /// diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ReadCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ReadCommand.cs index bed668db..a2378a55 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ReadCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/ReadCommand.cs @@ -79,15 +79,15 @@ public sealed class ReadCommand : ModbusCommandBase } catch (OperationCanceledException) when (ct.IsCancellationRequested) { - // Driver.Modbus.Cli-005: Ctrl+C during driver connect/read — exit quietly so - // CliFx does not render a full stack trace for a user-initiated cancellation. + // Ctrl+C during driver connect/read — exit quietly so CliFx does not render a + // full stack trace for a user-initiated cancellation. await console.Output.WriteLineAsync("Cancelled."); } finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.Modbus.Cli-009: flush Serilog before process exit so buffered log lines - // emitted during driver shutdown are not lost. Matches Driver.AbCip.Cli pattern. + // Flush Serilog before process exit so buffered log lines emitted during driver + // shutdown are not lost. Matches Driver.AbCip.Cli pattern. FlushLogging(); } } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/SubscribeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/SubscribeCommand.cs index ee96c573..67cb419b 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/SubscribeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/SubscribeCommand.cs @@ -40,7 +40,7 @@ public sealed class SubscribeCommand : ModbusCommandBase "BigEndian (default) or WordSwap.")] public ModbusByteOrder ByteOrder { get; init; } = ModbusByteOrder.BigEndian; - // Driver.Modbus.Cli-001: subscribe previously lacked these three options that read and + // subscribe previously lacked these three options that read and // write both expose. Without them, BitInRegister always watches bit 0 and String runs with // StringLength=0, silently producing wrong results for any subscriber using those types. /// Gets or sets the bit index for type=BitInRegister (0-15, LSB-first). @@ -63,7 +63,7 @@ public sealed class SubscribeCommand : ModbusCommandBase { ConfigureLogging(); ValidateEndpoint(); - // Driver.Modbus.Cli-010: reject non-positive interval before opening the driver so + // Reject non-positive interval before opening the driver so // the operator gets a clean error instead of a confusing PollGroupEngine failure. ValidateInterval(IntervalMs); var ct = console.RegisterCancellationHandler(); @@ -82,7 +82,7 @@ public sealed class SubscribeCommand : ModbusCommandBase var options = BuildOptions([tag]); await using var driver = new ModbusDriver(options, DriverInstanceId); - // Driver.Modbus.Cli-004: serialize console writes from the PollGroupEngine background + // Serialize console writes from the PollGroupEngine background // thread so overlapping poll ticks can't interleave partial lines. var writeLock = new object(); ISubscriptionHandle? handle = null; @@ -90,7 +90,7 @@ public sealed class SubscribeCommand : ModbusCommandBase { await driver.InitializeAsync("{}", ct); - // Driver.Modbus.Cli-011: emit the banner BEFORE wiring OnDataChange so the + // Emit the banner BEFORE wiring OnDataChange so the // main-thread WriteLineAsync cannot interleave with poll-thread change-event // writes. TextWriter.WriteLine is not guaranteed thread-safe; once the handler // is attached and SubscribeAsync starts, change events run on the poll thread. @@ -102,7 +102,7 @@ public sealed class SubscribeCommand : ModbusCommandBase // analyzer flags it + IConsole is the testable abstraction). driver.OnDataChange += (_, e) => { - // Driver.Modbus.Cli-004: swallow + log write failures so a transient stdout + // Swallow + log write failures so a transient stdout // error (closed pipe, IO exception on a redirected stream) cannot tear down // the poll-engine background loop. Without this guard the unhandled // exception would fault the long-running subscribe. @@ -142,7 +142,7 @@ public sealed class SubscribeCommand : ModbusCommandBase catch { /* teardown best-effort */ } } await driver.ShutdownAsync(CancellationToken.None); - // Driver.Modbus.Cli-009: flush Serilog before process exit so buffered log lines + // Flush Serilog before process exit so buffered log lines // emitted just before Ctrl+C (e.g. during UnsubscribeAsync / ShutdownAsync) are // not lost. Matches the pattern in Driver.AbCip.Cli commands. FlushLogging(); @@ -150,7 +150,7 @@ public sealed class SubscribeCommand : ModbusCommandBase } /// - /// Driver.Modbus.Cli-010: guards --interval-ms against zero or negative values. + /// Guards --interval-ms against zero or negative values. /// A non-positive interval would produce a non-positive into /// SubscribeAsync; the CLI should fail fast with an actionable error rather /// than relying on the downstream PollGroupEngine to clamp the value. diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/WriteCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/WriteCommand.cs index 7db233b8..07bb3bf7 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/WriteCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/Commands/WriteCommand.cs @@ -59,7 +59,6 @@ public sealed class WriteCommand : ModbusCommandBase "For type=String: HighByteFirst (standard) or LowByteFirst (DirectLOGIC).")] public ModbusStringByteOrder StringByteOrder { get; init; } = ModbusStringByteOrder.HighByteFirst; - /// Executes the write command. /// public override async ValueTask ExecuteAsync(IConsole console) { @@ -71,11 +70,6 @@ public sealed class WriteCommand : ModbusCommandBase throw new CliFx.Exceptions.CommandException( $"Region '{Region}' is read-only in the Modbus spec; writes require Coils or HoldingRegisters."); - // Driver.Modbus.Cli-002: coils are single-bit outputs — only Bool makes sense. A - // non-boolean type (e.g. --region Coils --type UInt16) would silently coerce the value - // to a boolean via Convert.ToBoolean, landing as ON for any non-zero value, with no - // diagnostic. Reject it early so the operator sees a clear error rather than a silent - // type-mismatch coerce. if (Region == ModbusRegion.Coils && DataType != ModbusDataType.Bool) throw new CliFx.Exceptions.CommandException( $"Region 'Coils' only supports boolean values (--type Bool). " + @@ -105,15 +99,11 @@ public sealed class WriteCommand : ModbusCommandBase } catch (OperationCanceledException) when (ct.IsCancellationRequested) { - // Driver.Modbus.Cli-005: Ctrl+C during driver connect/write — exit quietly so - // CliFx does not render a full stack trace for a user-initiated cancellation. await console.Output.WriteLineAsync("Cancelled."); } finally { await driver.ShutdownAsync(CancellationToken.None); - // Driver.Modbus.Cli-009: flush Serilog before process exit so buffered log lines - // emitted during driver shutdown are not lost. Matches Driver.AbCip.Cli pattern. FlushLogging(); } } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ModbusCommandBase.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ModbusCommandBase.cs index d3441cc9..be974669 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ModbusCommandBase.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Cli/ModbusCommandBase.cs @@ -48,6 +48,7 @@ public abstract class ModbusCommandBase : DriverCommandBase /// command against its own keep-alive reads. /// /// The tag definitions to include in the options. + /// A ready to hand to the driver constructor. protected ModbusDriverOptions BuildOptions(IReadOnlyList tags) => new() { Host = Host, @@ -66,7 +67,7 @@ public abstract class ModbusCommandBase : DriverCommandBase protected string DriverInstanceId => $"modbus-cli-{Host}:{Port}"; /// - /// Driver.Modbus.Cli-003: validate the endpoint flags at parse time so the operator + /// Validate the endpoint flags at parse time so the operator /// gets a clear CliFx error instead of an opaque socket / argument exception thrown /// deep inside the driver. Ranges: /// diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ProbeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ProbeCommand.cs index 491102b3..edc0ab24 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ProbeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ProbeCommand.cs @@ -37,11 +37,11 @@ public sealed class ProbeCommand : S7CommandBase Writable: false); var options = BuildOptions([probeTag]); - // Driver.S7.Cli-004: `await using` is the sole disposal mechanism — S7Driver.DisposeAsync + // `await using` is the sole disposal mechanism — S7Driver.DisposeAsync // already invokes ShutdownAsync, so the previous explicit ShutdownAsync(CancellationToken.None) // call in a finally block ran shutdown twice. The await-using on the next line is enough. await using var driver = new S7Driver(options, DriverInstanceId); - // Driver.S7.Cli-003: wrap the entire probe sequence so that a refused/unreachable TCP + // Wrap the entire probe sequence so that a refused/unreachable TCP // connect still prints the structured Host/CPU/Health lines instead of crashing with a // full .NET stack trace. InitializeAsync sets health to Faulted with the exception // message before re-throwing, so GetHealth() always has something to report. diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs index c98c9e54..a7be7e8f 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/ReadCommand.cs @@ -20,7 +20,6 @@ public sealed class ReadCommand : S7CommandBase IsRequired = true)] public string Address { get; init; } = default!; - // Driver.S7.Cli-002: help text reflects the types the driver currently implements. /// Gets the data type to interpret the address as. [CommandOption("type", 't', Description = "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " + @@ -50,9 +49,9 @@ public sealed class ReadCommand : S7CommandBase StringLength: StringLength); var options = BuildOptions([tag]); - // Driver.S7.Cli-004: `await using` is the sole disposal mechanism — S7Driver.DisposeAsync - // already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None) - // in a finally block ran shutdown twice. The await-using on the next line is enough. + // `await using` is the sole disposal mechanism — S7Driver.DisposeAsync already invokes + // ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None) in a + // finally block would run shutdown twice. await using var driver = new S7Driver(options, DriverInstanceId); try { @@ -62,8 +61,8 @@ public sealed class ReadCommand : S7CommandBase } catch (OperationCanceledException) when (ct.IsCancellationRequested) { - // Driver.S7.Cli-009: Ctrl+C during driver connect/read — exit quietly so - // CliFx does not render a full stack trace for a user-initiated cancellation. + // Ctrl+C during driver connect/read — exit quietly so CliFx does not render a + // full stack trace for a user-initiated cancellation. await console.Output.WriteLineAsync("Cancelled."); } } @@ -71,6 +70,7 @@ public sealed class ReadCommand : S7CommandBase /// Tag-name key used internally. Address + type is already unique. /// The S7 address to encode in the tag name. /// The data type to encode in the tag name. + /// A synthetic tag name combining the address and data type. internal static string SynthesiseTagName(string address, S7DataType type) => $"{address}:{type}"; } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs index 611ed913..753f9d92 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/SubscribeCommand.cs @@ -17,7 +17,6 @@ public sealed class SubscribeCommand : S7CommandBase public string Address { get; init; } = default!; /// Gets the data type of the address. - // Driver.S7.Cli-002: help text reflects the types the driver currently implements. [CommandOption("type", 't', Description = "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " + "Wide types (Int64, UInt64, Float64/LReal, String, DateTime) are byte-anchored: address must use the B suffix " + @@ -44,23 +43,13 @@ public sealed class SubscribeCommand : S7CommandBase Writable: false); var options = BuildOptions([tag]); - // Driver.S7.Cli-004: `await using` is the sole driver-disposal mechanism — S7Driver.DisposeAsync - // already invokes ShutdownAsync, so a redundant ShutdownAsync(CancellationToken.None) in finally - // ran shutdown twice. Only UnsubscribeAsync stays in the finally block — that's a subscription - // lifecycle concern that is not part of driver disposal. await using var driver = new S7Driver(options, DriverInstanceId); - // Driver.S7.Cli-011: serialize console writes from the PollGroupEngine background - // thread so overlapping poll ticks cannot interleave partial lines on the output. var writeLock = new object(); ISubscriptionHandle? handle = null; try { await driver.InitializeAsync("{}", ct); - // Driver.S7.Cli-007: route every data-change event to the CliFx console (not - // System.Console — the analyzer flags it + IConsole is the testable abstraction). - // The handler is synchronous because OnDataChange is raised from a driver - // background thread; the IConsole.Output writer is thread-safe for line writes. driver.OnDataChange += (_, e) => { var line = $"[{DateTime.UtcNow:HH:mm:ss.fff}] " + @@ -92,10 +81,6 @@ public sealed class SubscribeCommand : S7CommandBase try { await driver.UnsubscribeAsync(handle, CancellationToken.None); } catch { /* teardown best-effort */ } } - // Driver.S7.Cli-010: flush Serilog before process exit so buffered log lines - // emitted just before Ctrl+C (e.g. reconnect warnings from the PollGroupEngine) - // are not lost on abrupt termination. DriverCommandBase.ConfigureLogging() docs - // require this call in a finally block. FlushLogging(); } } diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs index d8deab3a..4c577290 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/Commands/WriteCommand.cs @@ -20,7 +20,7 @@ public sealed class WriteCommand : S7CommandBase public string Address { get; init; } = default!; /// Gets or sets the data type of the value to write. - // Driver.S7.Cli-002: help text reflects the types the driver currently implements. + // Help text reflects the types the driver currently implements. [CommandOption("type", 't', Description = "Bool / Byte / Int16 / UInt16 / Int32 / UInt32 / Float32 / Int64 / UInt64 / Float64 / String / DateTime (default Int16). " + "Wide types (Int64, UInt64, Float64/LReal, String, DateTime) are byte-anchored: address must use the B suffix " + @@ -57,7 +57,7 @@ public sealed class WriteCommand : S7CommandBase var parsed = ParseValue(Value, DataType); - // Driver.S7.Cli-004: `await using` is the sole disposal mechanism — S7Driver.DisposeAsync + // `await using` is the sole disposal mechanism — S7Driver.DisposeAsync // already invokes ShutdownAsync, so a redundant explicit ShutdownAsync(CancellationToken.None) // in a finally block ran shutdown twice. The await-using on the next line is enough. await using var driver = new S7Driver(options, DriverInstanceId); @@ -69,7 +69,7 @@ public sealed class WriteCommand : S7CommandBase } catch (OperationCanceledException) when (ct.IsCancellationRequested) { - // Driver.S7.Cli-009: Ctrl+C during driver connect/write — exit quietly so + // Ctrl+C during driver connect/write — exit quietly so // CliFx does not render a full stack trace for a user-initiated cancellation. await console.Output.WriteLineAsync("Cancelled."); } @@ -77,7 +77,7 @@ public sealed class WriteCommand : S7CommandBase /// Parse --value per , invariant culture throughout. /// - /// Driver.S7.Cli-001: numeric and parses are wrapped so that + /// Numeric and parses are wrapped so that /// malformed input ( / ) /// surfaces as a clean rather than a /// raw .NET stack trace — matching the friendly message the Bool path already produces. diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/S7CommandBase.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/S7CommandBase.cs index 68bc68af..f25180c3 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/S7CommandBase.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.S7.Cli/S7CommandBase.cs @@ -52,6 +52,7 @@ public abstract class S7CommandBase : DriverCommandBase /// disabled — CLI runs are one-shot. /// /// The tag definitions to include in the options. + /// The constructed . protected S7DriverOptions BuildOptions(IReadOnlyList tags) => new() { Host = Host, @@ -68,7 +69,7 @@ public abstract class S7CommandBase : DriverCommandBase protected string DriverInstanceId => $"s7-cli-{Host}:{Port}"; /// - /// Driver.S7.Cli-008: validate the endpoint flags at parse time so the operator + /// Validate the endpoint flags at parse time so the operator /// gets a clear instead of an opaque socket or /// argument exception thrown deep inside the S7.Net stack. /// diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/BrowseCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/BrowseCommand.cs index 6e2a9223..8eb29b04 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/BrowseCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/BrowseCommand.cs @@ -14,7 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli.Commands; /// Inherits from rather than /// so the --poll-only flag does NOT surface in /// browse --help: browse never subscribes, the flag would be a no-op, and the help -/// text would mislead users (Driver.TwinCAT.Cli-004). +/// text would mislead users. /// [Command("browse", Description = "Enumerate controller symbols via the driver's DiscoverAsync walk.")] public sealed class BrowseCommand : TwinCATCommandBase @@ -91,6 +91,7 @@ public sealed class BrowseCommand : TwinCATCommandBase /// /// The source collection to filter. /// The prefix to filter on, or null to keep everything. + /// The subset of whose browse name starts with . internal static List<(string BrowseName, DriverAttributeInfo Info)> FilterByPrefix( IReadOnlyList<(string BrowseName, DriverAttributeInfo Info)> source, string? prefix) => source @@ -103,6 +104,8 @@ public sealed class BrowseCommand : TwinCATCommandBase /// /// The number of matched items. /// The maximum number to show, or 0 for unbounded. + /// The number of items to print — if + /// is unbounded, otherwise the smaller of the two. internal static int PrintLimit(int matchedCount, int max) => max <= 0 ? matchedCount : Math.Min(max, matchedCount); @@ -113,6 +116,7 @@ public sealed class BrowseCommand : TwinCATCommandBase /// authorization is enforced server-side. /// /// The attribute info to label. + /// "RO" when the attribute is view-only; otherwise "RW". internal static string AccessTag(DriverAttributeInfo info) => info.SecurityClass == SecurityClassification.ViewOnly ? "RO" : "RW"; diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/SubscribeCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/SubscribeCommand.cs index 12ade782..9ae289cc 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/SubscribeCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/SubscribeCommand.cs @@ -64,7 +64,7 @@ public sealed class SubscribeCommand : TwinCATTagCommandBase // write below and with subsequent change events if the PLC pushes faster than a // single console write completes. A TextWriter is not guaranteed thread-safe, so // we serialise every write through a lock to keep output clean for - // screen-recorded bug-report timelines (Driver.TwinCAT.Cli-002). + // screen-recorded bug-report timelines. var writeLock = new object(); driver.OnDataChange += (_, e) => @@ -80,7 +80,7 @@ public sealed class SubscribeCommand : TwinCATTagCommandBase handle = await driver.SubscribeAsync([tagName], TimeSpan.FromMilliseconds(IntervalMs), ct); - // Driver.TwinCAT.Cli-003: derive the banner mechanism from the actual subscription + // Derive the banner mechanism from the actual subscription // handle the driver returned, not from --poll-only. The native ADS path tags its // handle with a "twincat-native-sub-*" DiagnosticId; anything else means we landed // on the shared PollGroupEngine. That way the line cannot disagree with what the diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/WriteCommand.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/WriteCommand.cs index d19549e7..f72adea4 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/WriteCommand.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/Commands/WriteCommand.cs @@ -71,10 +71,11 @@ public sealed class WriteCommand : TwinCATTagCommandBase /// Parse --value per , invariant culture. Wraps /// and in a /// so the operator sees a clean one-line - /// error instead of a raw stack trace (Driver.TwinCAT.Cli-008). + /// error instead of a raw stack trace. /// /// The raw string value to parse. /// The target TwinCAT data type. + /// The parsed value boxed as an matching the given . internal static object ParseValue(string raw, TwinCATDataType type) { try diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/TwinCATCommandBase.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/TwinCATCommandBase.cs index 1e9ddab3..32f59c0d 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/TwinCATCommandBase.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/TwinCATCommandBase.cs @@ -28,20 +28,16 @@ public abstract class TwinCATCommandBase : DriverCommandBase [CommandOption("timeout-ms", Description = "Per-operation timeout in ms (default 5000).")] public int TimeoutMs { get; init; } = 5000; - /// - /// Gets the per-operation timeout, projected from . The CliFx - /// init accessor required by the abstract base property is intentionally a - /// no-op: is the only source of truth, so any value an - /// `init` initialiser supplies to this property directly is silently - /// dropped. Do NOT add a backing field "fixing" the empty body — it would diverge - /// from and the two would drift on every refactor - /// (Driver.TwinCAT.Cli-007). - /// /// + // Projected from TimeoutMs. The CliFx init accessor required by the abstract base property + // is intentionally a no-op: TimeoutMs is the only source of truth, so any value an init + // initialiser supplies to this property directly is silently dropped. Do NOT add a backing + // field "fixing" the empty body — it would diverge from TimeoutMs and the two would drift on + // every refactor. public override TimeSpan Timeout { get => TimeSpan.FromMilliseconds(TimeoutMs); - init { /* see XML summary — driven by TimeoutMs */ } + init { /* see comment above — driven by TimeoutMs */ } } /// @@ -57,7 +53,7 @@ public abstract class TwinCATCommandBase : DriverCommandBase /// Validates the numeric options every TwinCAT CLI command shares (timeout + AMS port). /// Subclasses override and call base.Validate() first to add their own range /// checks. Throwing here surfaces a clean CliFx one-line error before the driver gets - /// a chance to fail with an opaque transport error (Driver.TwinCAT.Cli-001). + /// a chance to fail with an opaque transport error. /// protected virtual void Validate() { @@ -72,7 +68,7 @@ public abstract class TwinCATCommandBase : DriverCommandBase // ---- Test hooks ---- // Protected members are exposed to the test assembly through these internal accessors so the // test project can cover Gateway / DriverInstanceId composition + range validation without - // needing reflection on every assertion (Driver.TwinCAT.Cli-006). + // needing reflection on every assertion. /// Gets the gateway string for testing. internal string GatewayForTest => Gateway; diff --git a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/TwinCATTagCommandBase.cs b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/TwinCATTagCommandBase.cs index 928a05d8..c7adb225 100644 --- a/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/TwinCATTagCommandBase.cs +++ b/src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli/TwinCATTagCommandBase.cs @@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Cli; /// single-tag probe, read, write, /// subscribe. Adds the --poll-only flag (relevant only when the driver is /// about to register native ADS notifications, which is why it does NOT live on the -/// browse command — Driver.TwinCAT.Cli-004) and the BuildOptions helper that +/// browse command) and the BuildOptions helper that /// assembles the driver-side options record. /// public abstract class TwinCATTagCommandBase : TwinCATCommandBase @@ -24,6 +24,7 @@ public abstract class TwinCATTagCommandBase : TwinCATCommandBase /// native notifications toggled by . /// /// Tag definitions for the driver. + /// The built . protected TwinCATDriverOptions BuildOptions(IReadOnlyList tags) => new() { Devices = [new TwinCATDeviceOptions( @@ -39,6 +40,7 @@ public abstract class TwinCATTagCommandBase : TwinCATCommandBase // ---- Test hook ---- /// Test hook that exposes BuildOptions for unit testing. /// Tag definitions for the driver. + /// The built . internal TwinCATDriverOptions BuildOptionsForTest(IReadOnlyList tags) => BuildOptions(tags); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs index 4064e475..775af04d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/AbCipDriverOptions.cs @@ -6,10 +6,6 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// the same driver; per-device routing is keyed on /// via IPerCallHostResolver. /// -/// -/// Per v2 plan decisions #11 (libplctag), #41 (AbCip vs AbLegacy split), #143–144 (per-call -/// host resolver + resilience keys), #144 (bulkhead keyed on (DriverInstanceId, HostName)). -/// public sealed class AbCipDriverOptions { /// @@ -44,7 +40,7 @@ public sealed class AbCipDriverOptions public bool EnableControllerBrowse { get; init; } /// - /// Task #177 — when true, declared ALMD tags are surfaced as alarm conditions + /// When true, declared ALMD tags are surfaced as alarm conditions /// via IAlarmSource; the driver polls each subscribed /// alarm's InFaulted + Severity members + fires OnAlarmEvent on /// state transitions. Default false — operators explicitly opt in because @@ -94,11 +90,11 @@ public sealed class AbCipDriverOptions /// other quirks; per-device overrides via and /// take precedence when set. /// Optional display label for Admin UI. Falls back to . -/// Driver.AbCip-013 — per-device override for CIP request-packing +/// Per-device override for CIP request-packing /// (firmware 20+). null (the default) inherits the family profile's /// SupportsRequestPacking; set explicitly to opt a single device in or out without /// touching every other device on the same family. -/// Driver.AbCip-013 — per-device override for the Forward Open +/// Per-device override for the Forward Open /// ConnectionSize (Large Forward Open packet size in bytes). null inherits the family /// profile's DefaultConnectionSize. Honoured by the driver layer; the underlying /// libplctag 1.5.2 wrapper has no direct ConnectionSize property, so the value is @@ -120,7 +116,7 @@ public sealed record AbCipDeviceOptions( /// Logix symbolic path (controller or program scope). /// Logix atomic type, or for UDT-typed tags. /// When true and the tag's ExternalAccess permits writes, IWritable routes writes here. -/// Per plan decisions #44–#45, #143 — safe to replay on write timeout. Default false. +/// Safe to replay on write timeout. Default false. /// For -typed tags, the declared UDT /// member layout. When supplied, discovery fans out the UDT into a folder + one Variable per /// member (member TagPath = {tag.TagPath}.{member.Name}). When null on a Structure @@ -199,7 +195,7 @@ public sealed class AbCipProbeOptions /// /// Tag path used for the probe. When is true but this is - /// null/blank, the driver logs a warning and runs no probe loops (Driver.AbCip-011); + /// null/blank, the driver logs a warning and runs no probe loops; /// GetHostStatuses() will then report every device as Unknown. A family-default /// system-tag fallback (e.g. @raw_cpu_type on ControlLogix) is a deferred follow-up; /// today an operator opting into the probe must supply a tag path explicitly. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipAlarmProjection.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipAlarmProjection.cs index 5d839281..c9c0e0d3 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipAlarmProjection.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipAlarmProjection.cs @@ -5,7 +5,7 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// -/// Task #177 — projects AB Logix ALMD alarm instructions onto the OPC UA alarm surface by +/// Projects AB Logix ALMD alarm instructions onto the OPC UA alarm surface by /// polling the ALMD UDT's InFaulted / Acked / Severity members at a /// configurable interval + translating state transitions into OnAlarmEvent /// callbacks on the owning . Feature-flagged off by default via @@ -18,7 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// the operator acked" shape maps cleanly onto the driver-agnostic /// contract without concessions. /// -/// Polling reuses , so ALMD reads get the #194 +/// Polling reuses , so ALMD reads get the /// whole-UDT optimization for free when the ALMD is declared with its standard members. /// One poll loop per subscription call; the loop batches every /// member read across the full source-node set into a single ReadAsync per tick. @@ -185,7 +185,7 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable // ToInt(null) returns 0 and MapSeverity buckets it as Low. This is acceptable because // InFaulted and Severity are members of the same ALMD UDT read in one batch, so a Good // InFaulted almost always implies a Good Severity. The "unknown severity → Low" fallback - // is intentional and matches the behaviour documented on Driver.AbCip-017. + // is intentional and matches the documented behaviour. var severity = ToInt(severityDv.Value); var wasFaulted = sub.LastInFaulted.GetValueOrDefault(nodeId, false); @@ -302,7 +302,7 @@ internal sealed class AbCipAlarmProjection : IAsyncDisposable /// Handle returned by . public sealed record AbCipAlarmSubscriptionHandle(long Id) : IAlarmSubscriptionHandle { - /// Gets a diagnostic identifier for this subscription. + /// public string DiagnosticId => $"abcip-alarm-sub-{Id}"; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataTypeExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataTypeExtensions.cs index 5d712559..94a1e585 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataTypeExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDataTypeExtensions.cs @@ -12,7 +12,7 @@ public static class AbCipDataTypeExtensions /// /// LInt (signed 64-bit) → Int64; ULInt (unsigned 64-bit) → UInt64. /// UDInt (unsigned 32-bit) → UInt32 so values above Int32.MaxValue are not - /// wrapped to negative (Driver.AbCip-004). + /// wrapped to negative. /// USInt / UInt widen into Int32; they can never overflow it. /// /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs index 17bfc0b8..698d476d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriver.cs @@ -11,12 +11,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// capabilities via the libplctag.NET wrapper. /// /// -/// Wire layer is libplctag 1.6.x (plan decision #11). Per-device host addresses use +/// Wire layer is libplctag 1.6.x. Per-device host addresses use /// the ab://gateway[:port]/cip-path canonical form parsed via /// ; those strings become the hostName key -/// for Polly bulkhead + circuit-breaker isolation per plan decision #144. +/// for Polly bulkhead + circuit-breaker isolation. /// -/// Tier A per plan decisions #143–145 — in-process, shares server lifetime, no +/// Tier A — in-process, shares server lifetime, no /// sidecar. is the Tier-B escape hatch for recovering /// from native-heap growth that the CLR allocator can't see; it tears down the /// libplctag.NET Tag instances held in DeviceState.Runtimes and reconnects @@ -214,26 +214,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// internal AbCipTemplateCache TemplateCache => _templateCache; - /// Gets the unique identifier for this driver instance. + /// public string DriverInstanceId => _driverInstanceId; - /// Gets the driver type identifier. + /// public string DriverType => "AbCip"; - /// - /// Initialize the driver from its DriverConfig JSON. When - /// carries a real configuration (any device or tag), - /// it is parsed via and the - /// parsed options REPLACE the construction-time options — this is what makes - /// pick up a changed config (new device, new tag, - /// changed timeout). A blank or empty-object JSON ("{}") is treated as "no - /// override" so callers that constructed the driver with explicit options — chiefly - /// unit tests — keep those options. The driver's address-space + runtime state is then - /// built from the effective . - /// - /// The driver configuration as JSON; empty or "{}" means no override. - /// Cancellation token for the operation. - /// A task representing the asynchronous initialization. + /// public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { _health = new DriverHealth(DriverState.Initializing, null, null); @@ -261,7 +248,6 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, { // Duplicate-key check: a collision means two configured tags have the same name. // Fail fast at init time with a diagnostic rather than silently clobbering. - // (Driver.AbCip-005) if (_tagsByName.TryGetValue(tag.Name, out var existingTag)) throw new InvalidOperationException( $"AbCip tag name collision: '{tag.Name}' is declared more than once. " + @@ -280,7 +266,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, DataType: member.DataType, Writable: member.Writable, WriteIdempotent: member.WriteIdempotent, - // Driver.AbCip-016 — carry the member's array shape into the fanned-out + // Carry the member's array shape into the fanned-out // runtime definition. Discovery already emits an array node for an array // member (member.IsArray || member.ElementCount > 1); without these the // runtime def defaulted to scalar and the read returned a single element @@ -308,13 +294,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, state.ProbeCts = new CancellationTokenSource(); var ct = state.ProbeCts.Token; // Keep the loop Task so ShutdownAsync can await its clean exit before - // disposing the CTS / handles the loop is still using (Driver.AbCip-008). + // disposing the CTS / handles the loop is still using. state.ProbeTask = Task.Run(() => ProbeLoopAsync(state, ct), ct); } } else if (_options.Probe.Enabled && _devices.Count > 0) { - // Driver.AbCip-011: probe is Enabled but no ProbeTagPath is configured. Without a + // Probe is Enabled but no ProbeTagPath is configured. Without a // tag path the loop has nothing to read, so HostState would stay Unknown forever // and GetHostStatuses() would report every device as Unknown with no warning. // Log a warning so the misconfiguration is visible in the rolling Serilog file. @@ -335,26 +321,14 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, return Task.CompletedTask; } - /// Reinitialize the driver by shutting down and reinitializing with new configuration. - /// The new driver configuration as JSON. - /// Cancellation token for the operation. - /// A task representing the asynchronous reinitialization. + /// public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { await ShutdownAsync(cancellationToken).ConfigureAwait(false); await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false); } - /// - /// Tear the driver down: stop the alarm projection + poll engine, then for each device - /// cancel its probe loop, await the loop's clean exit, and only then dispose - /// the probe CTS + runtime handles. Awaiting the probe Task before disposing closes the - /// race where a still-running loop touches a disposed CTS or a cleared runtime - /// dictionary (Driver.AbCip-008). Idempotent — safe to call twice (e.g. ShutdownAsync - /// from ReinitializeAsync followed by DisposeAsync). - /// - /// Cancellation token for the operation. - /// A task representing the asynchronous shutdown. + /// public async Task ShutdownAsync(CancellationToken cancellationToken) { await _alarmProjection.DisposeAsync().ConfigureAwait(false); @@ -397,39 +371,21 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- ISubscribable (polling overlay via shared engine) ---- - /// Subscribe to value changes for the specified tag references. - /// The tag references to subscribe to. - /// The interval at which to publish changes. - /// Cancellation token for the operation. - /// A handle representing the subscription. + /// public Task SubscribeAsync( IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) => Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval)); - /// Unsubscribe from value changes using a subscription handle. - /// The subscription handle to unsubscribe. - /// Cancellation token for the operation. - /// A completed task. + /// public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) { _poll.Unsubscribe(handle); return Task.CompletedTask; } - // ---- IAlarmSource (ALMD projection, #177) ---- + // ---- IAlarmSource (ALMD projection) ---- - /// - /// Subscribe to ALMD alarm transitions on . Each id - /// names a declared ALMD UDT tag; the projection polls the tag's InFaulted + - /// Severity members at and - /// fires on 0→1 (raise) + 1→0 (clear) transitions. - /// Feature-gated — when is - /// false (the default), returns a handle wrapping a no-op subscription so - /// capability negotiation still works; never fires. - /// - /// The node IDs of alarm sources to subscribe to. - /// Cancellation token for the operation. - /// A handle representing the alarm subscription. + /// public Task SubscribeAlarmsAsync( IReadOnlyList sourceNodeIds, CancellationToken cancellationToken) { @@ -441,19 +397,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, return _alarmProjection.SubscribeAsync(sourceNodeIds, cancellationToken); } - /// Unsubscribe from alarm events. - /// The alarm subscription handle. - /// Cancellation token for the operation. - /// A completed task. + /// public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) => _options.EnableAlarmProjection ? _alarmProjection.UnsubscribeAsync(handle, cancellationToken) : Task.CompletedTask; - /// Acknowledge alarms. - /// The alarm acknowledgements to process. - /// Cancellation token for the operation. - /// A completed task. + /// public Task AcknowledgeAsync( IReadOnlyList acknowledgements, CancellationToken cancellationToken) => _options.EnableAlarmProjection @@ -462,8 +412,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IHostConnectivityProbe ---- - /// Gets the connectivity status of all configured devices. - /// A read-only list of host connectivity statuses. + /// public IReadOnlyList GetHostStatuses() => [.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))]; @@ -530,16 +479,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IPerCallHostResolver ---- - /// - /// Resolve the device host address for a given tag full-reference. Per plan decision #144 - /// the Phase 6.1 resilience pipeline keys its bulkhead + breaker on - /// (DriverInstanceId, hostName) so multi-PLC drivers get per-device isolation — - /// one dead PLC trips only its own breaker. Unknown references fall back to the - /// first configured device's host address rather than throwing — the invoker handles the - /// mislookup at the capability level when the actual read returns BadNodeIdUnknown. - /// - /// The full tag reference to resolve. - /// The device host address for the tag. + /// public string ResolveHost(string fullReference) { if (_tagsByName.TryGetValue(fullReference, out var def)) @@ -549,16 +489,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IReadable ---- - /// - /// Read each fullReference in order. Unknown tags surface as - /// BadNodeIdUnknown; libplctag-layer failures map through - /// ; any other exception becomes - /// BadCommunicationError. The driver health surface is updated per-call so the - /// Admin UI sees a tight feedback loop between read failures + the driver's state. - /// - /// The tag references to read. - /// Cancellation token for the operation. - /// A read-only list of data value snapshots. + /// public async Task> ReadAsync( IReadOnlyList fullReferences, CancellationToken cancellationToken) { @@ -566,14 +497,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, var now = DateTime.UtcNow; var results = new DataValueSnapshot[fullReferences.Count]; - // Task #194 — plan the batch: members of the same parent UDT get collapsed into one + // Plan the batch: members of the same parent UDT get collapsed into one // whole-UDT read + in-memory member decode; every other reference falls back to the // per-tag read path. Planner is a pure function over the // current tag map; BOOL/String/Structure members stay on the fallback path because // declaration-only offsets can't place them under Logix alignment rules. Whole-UDT // grouping is itself gated behind EnableDeclarationOnlyUdtGrouping — Studio 5000 may - // reorder UDT members vs declaration order, so the fast path is opt-in only (see - // Driver.AbCip-003 / AbCipUdtMemberLayout remarks). + // reorder UDT members vs declaration order, so the fast path is opt-in only. var plan = AbCipUdtReadPlanner.Build( fullReferences, _tagsByName, _options.EnableDeclarationOnlyUdtGrouping); @@ -594,7 +524,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, results[fb.OriginalIndex] = new DataValueSnapshot(null, AbCipStatusMapper.BadNodeIdUnknown, null, now); return; } - // Driver.AbCip-005: a Structure tag whose Members are declared is a container — + // A Structure tag whose Members are declared is a container — // its bare name is readable via the whole-UDT grouping path (ReadGroupAsync), not the // per-tag path. Reading it here returns BadNotSupported rather than Good/null so the // caller knows to address individual member paths (e.g. "Motor.Speed"). @@ -617,7 +547,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, var status = runtime.GetStatus(); if (status != 0) { - // Evict the stale handle so the next call re-creates it (Driver.AbCip-010). + // Evict the stale handle so the next call re-creates it. // A non-zero status can mean the controller dropped the connection or the tag // handle became permanently invalid (e.g. after a PLC download). Evicting // mirrors the probe loop's recreate-on-failure behaviour. @@ -662,7 +592,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, } /// - /// Task #194 — perform one whole-UDT read on the parent tag, then decode each + /// Perform one whole-UDT read on the parent tag, then decode each /// grouped member from the runtime's buffer at its computed byte offset. A per-group /// failure (parent read raised, non-zero libplctag status, or missing device) stamps /// the mapped fault across every grouped member only — sibling groups + the @@ -687,7 +617,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, var status = runtime.GetStatus(); if (status != 0) { - EvictRuntime(device, parent.Name); // Driver.AbCip-010 + EvictRuntime(device, parent.Name); var mapped = AbCipStatusMapper.MapLibplctagStatus(status); StampGroupStatus(group, results, now, mapped); _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, @@ -712,7 +642,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, } catch (Exception ex) { - EvictRuntime(device, parent.Name); // Driver.AbCip-010 + EvictRuntime(device, parent.Name); StampGroupStatus(group, results, now, AbCipStatusMapper.BadCommunicationError); _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message); _logger.LogWarning(ex, @@ -730,16 +660,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IWritable ---- - /// - /// Write each request in order. Writes are NOT auto-retried by the driver — per plan - /// decisions #44, #45, #143 the caller opts in via - /// and the resilience pipeline (layered above the driver) decides whether to replay. - /// Non-writable configurations surface as BadNotWritable; type-conversion failures - /// as BadTypeMismatch; transport errors as BadCommunicationError. - /// - /// The write requests to execute. - /// Cancellation token for the operation. - /// A read-only list of write results. + /// public async Task> WriteAsync( IReadOnlyList writes, CancellationToken cancellationToken) { @@ -770,7 +691,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, { var parsedPath = AbCipTagPath.TryParse(def.TagPath); - // BOOL-within-DINT writes — per task #181, RMW against a parallel parent-DINT + // BOOL-within-DINT writes — RMW against a parallel parent-DINT // runtime. Dispatching here keeps the normal EncodeValue path clean; the // per-parent lock prevents two concurrent bit writes to the same DINT from // losing one another's update. @@ -791,7 +712,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, var status = runtime.GetStatus(); if (status != 0) { - EvictRuntime(device, def.Name); // Driver.AbCip-010 + EvictRuntime(device, def.Name); results[i] = new WriteResult(AbCipStatusMapper.MapLibplctagStatus(status)); _logger.LogWarning( "AbCip write returned non-zero libplctag status {LibplctagStatus} for tag {Tag} on device {Device}; " + @@ -845,7 +766,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, catch (Exception ex) { // Transport / wire error — evict so the next write creates a fresh handle. - EvictRuntime(device, def.Name); // Driver.AbCip-010 + EvictRuntime(device, def.Name); results[i] = new WriteResult(AbCipStatusMapper.BadCommunicationError); _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message); _logger.LogWarning(ex, @@ -861,7 +782,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// Read-modify-write one bit within a DINT parent. Creates / reuses a parallel /// parent-DINT runtime (distinct from the bit-selector handle) + serialises concurrent /// writers against the same parent via a per-parent . - /// Matches the Modbus BitInRegister + FOCAS PMC Bit pattern shipped in pass 1 of task #181. + /// Matches the Modbus BitInRegister + FOCAS PMC Bit pattern. /// private async Task WriteBitInDIntAsync( DeviceState device, AbCipTagPath bitPath, int bit, object? value, CancellationToken ct) @@ -965,7 +886,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// it so the next read/write call re-creates and re-initializes a fresh handle. /// Called from , , and /// after a non-zero libplctag status or transport exception — - /// mirroring the probe loop's recreate-on-failure behaviour (Driver.AbCip-010). + /// mirroring the probe loop's recreate-on-failure behaviour. /// private static void EvictRuntime(DeviceState device, string tagName) { @@ -975,21 +896,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, } } - /// Gets the current health status of the driver. - /// The driver health information. + /// public DriverHealth GetHealth() => _health; - /// - /// CLR-visible allocation footprint only — libplctag's native heap is invisible to the - /// GC. driver-specs.md §3 flags this: operators must watch whole-process RSS for the - /// full picture, and is the Tier-B remediation. - /// - /// The memory footprint in bytes. + /// public long GetMemoryFootprint() => 0; - /// Flushes optional caches to free memory. - /// Cancellation token for the operation. - /// A completed task. + /// public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) { _templateCache.Clear(); @@ -998,24 +911,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- ITagDiscovery ---- - /// - /// Run-once: emits pre-declared tags and (when - /// EnableControllerBrowse is set) fully awaits the @tags symbol-table walk + UDT-shape - /// resolution within the single call, streaming the complete node set in one pass — - /// nothing fills in asynchronously after connect, so a single discovery pass is sufficient. - /// + /// public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; - /// - /// Stream the driver's tag set into the builder. Pre-declared tags from - /// emit first; optionally, the - /// walks each device's symbol table and adds - /// controller-discovered tags under a Discovered/ sub-folder. System / module / - /// routine / task tags are hidden via . - /// - /// The address space builder to populate with discovered tags. - /// Cancellation token for the operation. - /// A task representing the asynchronous discovery. + /// public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(builder); @@ -1065,7 +964,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, } // Controller-discovered tags — opt-in via EnableControllerBrowse. The real @tags - // walker (LibplctagTagEnumerator) is the factory default since task #178 shipped, + // walker (LibplctagTagEnumerator) is the factory default, // so leaving the flag off keeps the strict-config path for deployments where only // declared tags should appear. if (_options.EnableControllerBrowse && _devices.TryGetValue(device.HostAddress, out var state)) @@ -1283,7 +1182,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// live libplctag.NET instances keyed by tag name. /// Native tag lifetime is owned by the Tag.Dispose() inside each /// ; libplctag.NET's own finalizer covers GC-collected - /// instances so no separate SafeHandle wrapper is needed here (Driver.AbCip-006). + /// instances so no separate SafeHandle wrapper is needed here. /// internal sealed class DeviceState( AbCipHostAddress parsedAddress, @@ -1313,7 +1212,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// can await the loop's clean exit after /// cancelling and BEFORE disposing the CTS or the runtime /// handles — otherwise the still-running loop can touch a disposed CTS or a cleared - /// runtime dictionary (Driver.AbCip-008). + /// runtime dictionary. /// public Task? ProbeTask { get; set; } @@ -1345,7 +1244,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery, _rmwLocks.GetOrAdd(parentTagName, _ => new SemaphoreSlim(1, 1)); /// - /// Driver.AbCip-013 — compute the effective for a + /// Compute the effective for a /// tag on this device. Combines the per-device options /// (, /// ) with the family profile defaults diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverFactoryExtensions.cs index 3eaad920..8ff8f366 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipDriverFactoryExtensions.cs @@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Static factory registration helper for . Server's Program.cs -/// calls once at startup; the bootstrapper (task #248) then +/// calls once at startup; the bootstrapper then /// materialises AB CIP DriverInstance rows from the central config DB into live driver /// instances. Mirrors GalaxyProxyDriverFactoryExtensions. /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs index 5dc74f8e..01138dc1 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipHostAddress.cs @@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// Parsed ab://gateway[:port]/cip-path host-address string used by the AbCip driver /// as the hostName key across , /// , and the Polly bulkhead key -/// (DriverInstanceId, hostName) per v2 plan decision #144. +/// (DriverInstanceId, hostName). /// /// /// Format matches what libplctag's gateway=... + path=... attributes diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs index c0fed449..ea17d256 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipStatusMapper.cs @@ -48,6 +48,7 @@ public static class AbCipStatusMapper /// Map a CIP general-status byte to an OPC UA StatusCode. /// The CIP general-status byte value. + /// The mapped OPC UA StatusCode value. public static uint MapCipGeneralStatus(byte status) => status switch { 0x00 => Good, @@ -72,6 +73,7 @@ public static class AbCipStatusMapper /// operation; every other (negative) member is an error. /// /// The libplctag status code as an integer. + /// The mapped OPC UA StatusCode value. public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status); /// @@ -80,6 +82,7 @@ public static class AbCipStatusMapper /// seam, which returns the boxed-as-int value. /// /// The libplctag Status enum value. + /// The mapped OPC UA StatusCode value. public static uint MapLibplctagStatus(Status status) => status switch { Status.Ok => Good, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipSystemTagFilter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipSystemTagFilter.cs index ed21a36b..f7fbe507 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipSystemTagFilter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipSystemTagFilter.cs @@ -6,10 +6,10 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// pointers, and __DEFVAL_* stubs that are noise for the OPC UA address space. /// /// -/// Lifted from the filter conventions documented across Rockwell Knowledgebase article -/// IC-12345 and the Logix 5000 Controllers General Instructions Reference. The list is -/// conservative — when in doubt, a tag is surfaced rather than hidden so operators can -/// see it and the config flow can explicitly hide it via UnsArea ACL. +/// Lifted from the filter conventions documented in the Logix 5000 Controllers General +/// Instructions Reference. The list is conservative — when in doubt, a tag is surfaced +/// rather than hidden so operators can see it and the config flow can explicitly hide it +/// via UnsArea ACL. /// public static class AbCipSystemTagFilter { @@ -19,6 +19,7 @@ public static class AbCipSystemTagFilter /// always preserved case and the system-tag prefixes are uppercase by convention. /// /// The tag name to check. + /// true when the tag name matches a known system-tag pattern; otherwise false. public static bool IsSystemTag(string tagName) { if (string.IsNullOrWhiteSpace(tagName)) return true; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTagPath.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTagPath.cs index 2f77b24a..d97231a2 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTagPath.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTagPath.cs @@ -24,6 +24,7 @@ public sealed record AbCipTagPath( int? BitIndex) { /// Rebuild the canonical Logix tag string. + /// The canonical Logix tag string. public string ToLibplctagName() { var buf = new System.Text.StringBuilder(); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTemplateCache.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTemplateCache.cs index 5618713a..719cd87f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTemplateCache.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipTemplateCache.cs @@ -23,6 +23,7 @@ public sealed class AbCipTemplateCache /// /// The device host address and port. /// The template instance ID. + /// The cached UDT shape, or null if not yet read. public AbCipUdtShape? TryGet(string deviceHostAddress, uint templateInstanceId) => _shapes.TryGetValue((deviceHostAddress, templateInstanceId), out var shape) ? shape : null; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtMemberLayout.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtMemberLayout.cs index daeb5d70..b4b271f3 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtMemberLayout.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtMemberLayout.cs @@ -2,7 +2,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// /// Computes byte offsets for declared UDT members under Logix natural-alignment rules so -/// a single whole-UDT read (task #194) can decode each member from one buffer without +/// a single whole-UDT read can decode each member from one buffer without /// re-reading per member. Declaration-driven — the caller supplies /// rows; this helper produces the offset each member /// sits at in the parent tag's read buffer. @@ -30,6 +30,7 @@ public static class AbCipUdtMemberLayout /// if any member type is unsupported for declaration-only layout. /// /// The list of UDT member declarations. + /// A dictionary of member offsets keyed by name, or null if any member type is unsupported. public static IReadOnlyDictionary? TryBuild( IReadOnlyList members) { @@ -44,7 +45,7 @@ public static class AbCipUdtMemberLayout if (!TryGetSizeAlign(member.DataType, out var size, out var align)) return null; - // Driver.AbCip-016 — an array member can't be placed by declaration-only layout: the + // An array member can't be placed by declaration-only layout: the // whole-UDT grouped read decodes one scalar per member at its offset and can't return // an array, and advancing the cursor by the scalar size (not size * count) would // mis-place every member after it. Opt the whole group out so array members fall back diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtReadPlanner.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtReadPlanner.cs index 62cef0aa..fd729773 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtReadPlanner.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/AbCipUdtReadPlanner.cs @@ -1,7 +1,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// -/// Task #194 — groups a ReadAsync batch of full-references into whole-UDT reads where +/// Groups a ReadAsync batch of full-references into whole-UDT reads where /// possible. A group is emitted for every parent UDT tag whose declared /// s produced a valid offset map AND at least two of /// its members appear in the batch; every other reference stays in the per-tag fallback @@ -28,6 +28,7 @@ public static class AbCipUdtReadPlanner /// The list of tag references to read. /// Dictionary mapping tag names to their definitions. /// Whether to enable UDT member grouping based on declaration order. + /// The resulting plan, containing whole-UDT read groups and per-tag fallback reads. public static AbCipUdtReadPlan Build( IReadOnlyList requests, IReadOnlyDictionary tagsByName, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagEnumerator.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagEnumerator.cs index de596e0c..353bac61 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagEnumerator.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagEnumerator.cs @@ -15,6 +15,7 @@ public interface IAbCipTagEnumerator : IDisposable /// /// Parameters for creating device tags. /// Cancellation token. + /// An asynchronous sequence of the tags discovered for the device. IAsyncEnumerable EnumerateAsync( AbCipTagCreateParams deviceParams, CancellationToken cancellationToken); @@ -26,6 +27,7 @@ public interface IAbCipTagEnumeratorFactory /// /// Creates a new tag enumerator instance. /// + /// A new instance. IAbCipTagEnumerator Create(); } @@ -66,11 +68,7 @@ public sealed record AbCipDiscoveredTag( /// internal sealed class EmptyAbCipTagEnumerator : IAbCipTagEnumerator { - /// - /// Enumerates an empty sequence of tags. - /// - /// Parameters for creating device tags. - /// Cancellation token. + /// public async IAsyncEnumerable EnumerateAsync( AbCipTagCreateParams deviceParams, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) @@ -88,8 +86,6 @@ internal sealed class EmptyAbCipTagEnumerator : IAbCipTagEnumerator /// Factory for . internal sealed class EmptyAbCipTagEnumeratorFactory : IAbCipTagEnumeratorFactory { - /// - /// Creates a new empty tag enumerator. - /// + /// public IAbCipTagEnumerator Create() => new EmptyAbCipTagEnumerator(); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs index b0520177..f05b5164 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTagRuntime.cs @@ -11,20 +11,24 @@ public interface IAbCipTagRuntime : IDisposable { /// Create the underlying native tag (equivalent to libplctag's plc_tag_create). /// Cancellation token. + /// A task that represents the asynchronous operation. Task InitializeAsync(CancellationToken cancellationToken); /// Issue a read; on completion the local buffer holds the current PLC value. /// Cancellation token. + /// A task that represents the asynchronous operation. Task ReadAsync(CancellationToken cancellationToken); /// Flush the local buffer to the PLC. /// Cancellation token. + /// A task that represents the asynchronous operation. Task WriteAsync(CancellationToken cancellationToken); /// /// Raw libplctag status code — mapped to an OPC UA StatusCode via /// . Zero on success, negative on error. /// + /// The raw libplctag status code (zero on success, negative on error). int GetStatus(); /// @@ -34,11 +38,12 @@ public interface IAbCipTagRuntime : IDisposable /// /// CIP data type to decode. /// Bit index for BOOL-within-DINT extraction, or null. + /// The decoded value, or null if the type is unsupported. object? DecodeValue(AbCipDataType type, int? bitIndex); /// - /// Decode a value at an arbitrary byte offset in the local buffer. Task #194 — - /// whole-UDT reads perform one on the parent UDT tag then + /// Decode a value at an arbitrary byte offset in the local buffer. Whole-UDT reads + /// perform one on the parent UDT tag then /// call this per declared member with its computed offset, avoiding one libplctag /// round-trip per member. Implementations that do not support offset-aware decoding /// may fall back to when is zero; @@ -48,6 +53,7 @@ public interface IAbCipTagRuntime : IDisposable /// CIP data type to decode. /// Byte offset in the buffer. /// Bit index for BOOL-within-DINT extraction, or null. + /// The decoded value, or null if the type is unsupported. object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex); /// @@ -61,6 +67,7 @@ public interface IAbCipTagRuntime : IDisposable /// /// CIP element data type to decode. /// Number of array elements to decode. + /// A boxed element-typed CLR array, or null if the type is unsupported. object? DecodeArray(AbCipDataType type, int count); /// @@ -81,6 +88,7 @@ public interface IAbCipTagFactory { /// Creates a tag runtime handle from the specified creation parameters. /// Parameters needed to create the tag runtime. + /// The created tag runtime handle. IAbCipTagRuntime Create(AbCipTagCreateParams createParams); } @@ -93,11 +101,11 @@ public interface IAbCipTagFactory /// libplctag operation timeout (applies to Initialize / Read / Write). /// CIP request-packing flag — combines the per-device override (if /// any) with the family profile's SupportsRequestPacking. Forwarded to the libplctag -/// Tag.AllowPacking property (Driver.AbCip-013). +/// Tag.AllowPacking property. /// Forward Open ConnectionSize — combines the per-device override /// (if any) with the family profile's DefaultConnectionSize. libplctag 1.5.2 has no /// direct ConnectionSize property; the value is plumbed for forward-compat with future -/// wrappers / a custom tag-attribute path (Driver.AbCip-013). +/// wrappers / a custom tag-attribute path. /// Phase 4c — libplctag elem_count. Forwarded to the /// libplctag Tag.ElementCount property so a 1-D array tag pulls all elements in one /// CIP transaction. Defaults to 1 (scalar); the driver sets it from the tag definition's diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTemplateReader.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTemplateReader.cs index 993db768..dfe1ac1e 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTemplateReader.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/IAbCipTemplateReader.cs @@ -16,6 +16,7 @@ public interface IAbCipTemplateReader : IDisposable /// The device connection parameters. /// The template instance ID to read. /// Token to cancel the operation. + /// The raw Template Object blob bytes. Task ReadAsync( AbCipTagCreateParams deviceParams, uint templateInstanceId, @@ -26,5 +27,6 @@ public interface IAbCipTemplateReader : IDisposable public interface IAbCipTemplateReaderFactory { /// Creates a new template reader instance. + /// A new instance. IAbCipTemplateReader Create(); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs index 2dc7d5ec..e7e55e3c 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagEnumerator.cs @@ -21,10 +21,7 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator { private Tag? _tag; - /// Enumerates all tags in the controller symbol table. - /// Device connection parameters including gateway and path. - /// Cancellation token for the enumeration. - /// An async enumerable of discovered tags. + /// public async IAsyncEnumerable EnumerateAsync( AbCipTagCreateParams deviceParams, [EnumeratorCancellation] CancellationToken cancellationToken) @@ -67,7 +64,6 @@ internal sealed class LibplctagTagEnumerator : IAbCipTagEnumerator /// Factory for . internal sealed class LibplctagTagEnumeratorFactory : IAbCipTagEnumeratorFactory { - /// Creates a new libplctag-based tag enumerator. - /// A new tag enumerator instance. + /// public IAbCipTagEnumerator Create() => new LibplctagTagEnumerator(); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs index 0c88f554..9584fa06 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTagRuntime.cs @@ -25,50 +25,29 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime Protocol = Protocol.ab_eip, Name = p.TagName, Timeout = p.Timeout, - // Driver.AbCip-013 — honour the per-device or family-default AllowPacking knob so - // operators can disable CIP request-packing for older firmware or a single device. AllowPacking = p.AllowPacking, // Phase 4c — libplctag elem_count. For a 1-D array tag the driver passes the element // count so libplctag pulls every element in one CIP read; scalar tags pass 1. ElementCount = p.ElementCount > 1 ? p.ElementCount : 1, }; - // ConnectionSize is captured on AbCipTagCreateParams for forward-compat (driver-specs.md - // exposes it as a per-device option) but libplctag.NET 1.5.2 has no direct Tag property - // for it. Until the wrapper exposes one (or we ship a custom tag-attribute path), the - // family profile DefaultConnectionSize is what the underlying CIP Forward Open - // negotiates with — Driver.AbCip-013. } - /// Initializes the tag asynchronously. - /// The cancellation token. - /// A task representing the asynchronous initialization. + /// public Task InitializeAsync(CancellationToken cancellationToken) => _tag.InitializeAsync(cancellationToken); - /// Reads the tag value asynchronously. - /// The cancellation token. - /// A task representing the asynchronous read operation. + /// public Task ReadAsync(CancellationToken cancellationToken) => _tag.ReadAsync(cancellationToken); - /// Writes the tag value asynchronously. - /// The cancellation token. - /// A task representing the asynchronous write operation. + /// public Task WriteAsync(CancellationToken cancellationToken) => _tag.WriteAsync(cancellationToken); - /// Gets the current status of the tag. - /// The tag status as an integer. + /// public int GetStatus() => (int)_tag.GetStatus(); - /// Decodes the tag value with the specified data type. - /// The data type to decode. - /// The bit index for bit-level access, if applicable. - /// The decoded value. + /// public object? DecodeValue(AbCipDataType type, int? bitIndex) => DecodeValueAt(type, 0, bitIndex); - /// Decodes the tag value at the specified offset with the specified data type. - /// The data type to decode. - /// The byte offset within the tag buffer. - /// The bit index for bit-level access, if applicable. - /// The decoded value. + /// public object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex) => type switch { AbCipDataType.Bool => bitIndex is int bit @@ -79,9 +58,9 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime AbCipDataType.Int => (int)_tag.GetInt16(offset), AbCipDataType.UInt => (int)_tag.GetUInt16(offset), AbCipDataType.DInt => _tag.GetInt32(offset), - AbCipDataType.UDInt => _tag.GetUInt32(offset), // UInt32 to match ToDriverDataType (Driver.AbCip-004) - AbCipDataType.LInt => _tag.GetInt64(offset), // Int64 to match ToDriverDataType (Driver.AbCip-004) - AbCipDataType.ULInt => _tag.GetUInt64(offset), // UInt64 to match ToDriverDataType (Driver.AbCip-004) + AbCipDataType.UDInt => _tag.GetUInt32(offset), + AbCipDataType.LInt => _tag.GetInt64(offset), + AbCipDataType.ULInt => _tag.GetUInt64(offset), AbCipDataType.Real => _tag.GetFloat32(offset), AbCipDataType.LReal => _tag.GetFloat64(offset), AbCipDataType.String => _tag.GetString(offset), @@ -90,18 +69,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime _ => null, }; - /// - /// Phase 4c — decode array elements from the post-read buffer - /// into an element-typed CLR array. libplctag has already pulled all elements (the tag - /// was created with elem_count = count); we slice the local buffer per element at - /// byte stride using the same per-offset decoders the - /// scalar / UDT-member paths use. The boxed result is a strongly-typed array - /// (int[], float[], bool[], string[], …) so the OPC UA layer - /// materialises a 1-D array variant. - /// - /// The element data type to decode. - /// The number of elements to decode. - /// A boxed element-typed CLR array, or null for an unsupported element type. + /// public object? DecodeArray(AbCipDataType type, int count) { if (count < 1) count = 1; @@ -163,10 +131,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime _ => 4, }; - /// Encodes the specified value to the tag with the specified data type. - /// The data type to encode. - /// The bit index for bit-level access, if applicable. - /// The value to encode. + /// public void EncodeValue(AbCipDataType type, int? bitIndex, object? value) { switch (type) @@ -253,9 +218,7 @@ internal sealed class LibplctagTagRuntime : IAbCipTagRuntime /// Default implementation of IAbCipTagFactory that creates LibplctagTagRuntime instances. internal sealed class LibplctagTagFactory : IAbCipTagFactory { - /// Creates a new tag runtime with the specified creation parameters. - /// The parameters for creating the tag. - /// A new IAbCipTagRuntime instance. + /// public IAbCipTagRuntime Create(AbCipTagCreateParams createParams) => new LibplctagTagRuntime(createParams); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs index bd687f2e..be67216b 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/LibplctagTemplateReader.cs @@ -8,28 +8,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip; /// internally via a normal read call, + returns the raw byte buffer so /// can decode it. /// -/// -/// Driver.AbCip-012 — by design each FetchUdtShapeAsync call creates one fresh -/// , pays one CIP connection handshake, reads, and disposes. Per-type -/// connect cost is accepted because (a) template reads are a low-frequency discovery path -/// (one-shot per UDT type, then the decoded shape is cached in -/// ), (b) libplctag pools its underlying CIP connections per -/// gateway+path so the underlying TCP/EIP session is reused even when individual -/// instances are torn down, and (c) pooling at the wrapper layer here would -/// buy a single Forward Open per device per discovery run — small relative to the rest of a -/// bulk-tag-walk discovery. If telemetry ever shows discovery latency dominated by -/// template-read connects, revisit by holding one @udt-capable per -/// device for the duration of a discovery run. -/// internal sealed class LibplctagTemplateReader : IAbCipTemplateReader { private Tag? _tag; - /// Reads a template object from the PLC asynchronously. - /// The device connection parameters. - /// The template instance ID to read. - /// Cancellation token for the operation. - /// A task representing the asynchronous read operation. + /// public async Task ReadAsync( AbCipTagCreateParams deviceParams, uint templateInstanceId, @@ -50,7 +33,7 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader return _tag.GetBuffer(); } - /// + /// Disposes the template reader and releases the underlying libplctag tag. public void Dispose() => _tag?.Dispose(); private static PlcType MapPlcType(string attribute) => attribute switch @@ -64,7 +47,6 @@ internal sealed class LibplctagTemplateReader : IAbCipTemplateReader internal sealed class LibplctagTemplateReaderFactory : IAbCipTemplateReaderFactory { - /// Creates a new instance of the libplctag template reader. - /// A new instance of . + /// public IAbCipTemplateReader Create() => new LibplctagTemplateReader(); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs index 0c5c3495..acaede0d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/PlcFamilies/AbCipPlcFamilyProfile.cs @@ -21,6 +21,7 @@ public sealed record AbCipPlcFamilyProfile( { /// Look up the profile for a configured family. /// The PLC family to look up the profile for. + /// The matching profile, or if the family is unrecognized. public static AbCipPlcFamilyProfile ForFamily(AbCipPlcFamily family) => family switch { AbCipPlcFamily.ControlLogix => ControlLogix, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs index a1d28ace..598fb18b 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyDriverOptions.cs @@ -5,7 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; /// /// AB Legacy (PCCC) driver configuration. One instance supports N devices (SLC 500 / -/// MicroLogix / PLC-5 / LogixPccc). Per plan decision #41 AbLegacy ships separately from +/// MicroLogix / PLC-5 / LogixPccc). AbLegacy ships separately from /// AbCip because PCCC's file-based addressing (N7:0) and Logix's symbolic addressing /// (Motor1.Speed) pull the abstraction in different directions. /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyEquipmentTagParser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyEquipmentTagParser.cs index 6f54ce2b..22b7edbd 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyEquipmentTagParser.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyEquipmentTagParser.cs @@ -40,14 +40,6 @@ public static class AbLegacyEquipmentTagParser if (string.IsNullOrWhiteSpace(address)) return false; var dataType = ReadEnum(root, "dataType", AbLegacyDataType.Int); var deviceHostAddress = ReadString(root, "deviceHostAddress"); - // Phase 4c #137 — thread the equipment tag's array element count. The canonical - // foundation contract: a tag is an ARRAY ⟺ isArray:true. arrayLength (the element - // count, ≥1) is honoured ONLY when isArray is the JSON literal true, so a stale - // length behind a cleared / absent isArray never produces an orphan array tag that - // mismatches its scalar OPC UA node (review C-2). A 1-element array (isArray:true, - // arrayLength:1) is a valid [1] array. Clamp to the PCCC file maximum - // (AbLegacyArray.MaxElements = 256) so a fat-fingered count can never request a span - // larger than a single data file holds. isArray:false / absent → null (scalar). int? arrayLength = null; if (IsArrayFlag(root)) { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyPlcFamilyProfile.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyPlcFamilyProfile.cs index 7bedc9f3..5d533d33 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyPlcFamilyProfile.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Contracts/AbLegacyPlcFamilyProfile.cs @@ -26,6 +26,7 @@ public sealed record AbLegacyPlcFamilyProfile( /// forward-compatibility for device configs authored against a build that predates a new /// family enum member, preferring a safe default over a startup exception. /// + /// The libplctag profile defaults for . public static AbLegacyPlcFamilyProfile ForFamily(AbLegacyPlcFamily family) => family switch { AbLegacyPlcFamily.Slc500 => Slc500, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs index a509d951..4ff25df0 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyAddress.cs @@ -33,6 +33,7 @@ public sealed record AbLegacyAddress( string? SubElement) { /// Converts the address to the libplctag library address format. + /// The libplctag name=... attribute value for this address. public string ToLibplctagName() { var file = FileNumber is null ? FileLetter : $"{FileLetter}{FileNumber}"; @@ -44,6 +45,7 @@ public sealed record AbLegacyAddress( /// Attempts to parse a string into an AB legacy address. /// The address string to parse. + /// The parsed address, or when is not a valid PCCC address. public static AbLegacyAddress? TryParse(string? value) { if (string.IsNullOrWhiteSpace(value)) return null; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs index 547e109c..49115074 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriver.cs @@ -68,22 +68,13 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot))); } - /// - /// Gets the driver instance identifier. - /// + /// public string DriverInstanceId => _driverInstanceId; - /// - /// Gets the driver type. - /// + /// public string DriverType => "AbLegacy"; - /// - /// Initializes the driver asynchronously. - /// - /// The driver configuration JSON. - /// The cancellation token. - /// A task representing the asynchronous operation. + /// public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { _health = new DriverHealth(DriverState.Initializing, null, null); @@ -132,9 +123,6 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover catch (Exception ex) { _health = new DriverHealth(DriverState.Faulted, null, ex.Message); - // Driver.AbLegacy-005 — structured log of the init failure so a field operator sees - // the exception in the rolling Serilog file rather than only as a transient Detail - // string on DriverHealth. _logger.LogError(ex, "AbLegacy driver initialise failed. Driver={DriverInstanceId}", _driverInstanceId); // Tear down any probe loops and cached state that were created before the failure so @@ -157,23 +145,14 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover return Task.CompletedTask; } - /// - /// Reinitializes the driver asynchronously. - /// - /// The driver configuration JSON. - /// The cancellation token. - /// A task representing the asynchronous operation. + /// public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { await ShutdownAsync(cancellationToken).ConfigureAwait(false); await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false); } - /// - /// Shuts down the driver asynchronously. - /// - /// The cancellation token. - /// A task representing the asynchronous operation. + /// public async Task ShutdownAsync(CancellationToken cancellationToken) { await _poll.DisposeAsync().ConfigureAwait(false); @@ -190,23 +169,13 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null); } - /// - /// Gets the driver health status. - /// - /// The driver health status. + /// public DriverHealth GetHealth() => _health; - /// - /// Gets the memory footprint of the driver. - /// - /// The memory footprint in bytes. + /// public long GetMemoryFootprint() => 0; - /// - /// Flushes optional caches asynchronously. - /// - /// The cancellation token. - /// A task representing the asynchronous operation. + /// public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; /// @@ -224,12 +193,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover // ---- IReadable ---- - /// - /// Reads data values asynchronously. - /// - /// The full references to read. - /// The cancellation token. - /// A list of data value snapshots. + /// public async Task> ReadAsync( IReadOnlyList fullReferences, CancellationToken cancellationToken) { @@ -267,11 +231,6 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover await runtime.ReadAsync(cancellationToken).ConfigureAwait(false); status = runtime.GetStatus(); var parsed = AbLegacyAddress.TryParse(def.Address); - // Phase 4c #137 — an ARRAY tag (non-null ArrayLength, ≥1) decodes the whole - // contiguous read into a typed CLR array of that count, INCLUDING a 1-element - // array (review I-3); a SCALAR tag (null ArrayLength) decodes a single value. - // The runtime was created with a matching ElementCount in EnsureTagRuntimeAsync - // so its buffer holds all the elements. if (status != 0) value = null; else if (IsArrayTag(def)) @@ -290,11 +249,6 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover AbLegacyStatusMapper.MapLibplctagStatus(status), null, now); _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, $"libplctag status {status} reading {reference}"); - // Driver.AbLegacy-005 — log the FIRST non-zero libplctag status per device so - // a field operator can correlate a comms problem with a structured log - // entry. Detail on DriverHealth is overwritten by the very next read; the - // log entry persists. Subsequent occurrences on the same device stay quiet so - // a permanently-bad PLC doesn't flood the rolling file. if (!device.FirstNonZeroStatusLogged) { device.FirstNonZeroStatusLogged = true; @@ -325,12 +279,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover // ---- IWritable ---- - /// - /// Writes data values asynchronously. - /// - /// The write requests. - /// The cancellation token. - /// A list of write results. + /// public async Task> WriteAsync( IReadOnlyList writes, CancellationToken cancellationToken) { @@ -422,19 +371,10 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover // ---- ITagDiscovery ---- - /// - /// Run-once: emits the complete node set synchronously from - /// the configured device/tag tables within a single pass — there is no shape that fills - /// in asynchronously after connect, so a single discovery pass is sufficient. - /// + /// public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; - /// - /// Discovers tags and populates the address space asynchronously. - /// - /// The address space builder. - /// The cancellation token. - /// A task representing the asynchronous operation. + /// public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(builder); @@ -447,14 +387,6 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover string.Equals(t.DeviceHostAddress, device.HostAddress, StringComparison.OrdinalIgnoreCase)); foreach (var tag in tagsForDevice) { - // Phase 4c #137 — PCCC data files are inherently arrays of elements (a single N7 - // file is up to 256 words). The canonical contract: a tag is an ARRAY ⟺ its - // ArrayLength is non-null (≥1, set by the parser only when isArray:true). A tag - // with a non-null ArrayLength materialises a 1-D array OPC UA node, INCLUDING a - // 1-element array (ArrayLength:1 → a [1] node — review I-3). ArrayDim is clamped - // to the PCCC file maximum (AbLegacyArray.MaxElements = 256) so the declared - // dimension can never exceed what a single data file holds; ArrayLength == null - // (scalar) stays scalar. var isArray = tag.ArrayLength is int len && len >= 1; var arrayDim = isArray ? (uint)Math.Min(tag.ArrayLength!.Value, AbLegacyArray.MaxElements) @@ -477,23 +409,12 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover // ---- ISubscribable (polling overlay via shared engine) ---- - /// - /// Subscribes to data changes asynchronously. - /// - /// The full references to subscribe to. - /// The publishing interval. - /// The cancellation token. - /// A subscription handle. + /// public Task SubscribeAsync( IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) => Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval)); - /// - /// Unsubscribes from data changes asynchronously. - /// - /// The subscription handle. - /// The cancellation token. - /// A task representing the asynchronous operation. + /// public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) { _poll.Unsubscribe(handle); @@ -502,10 +423,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover // ---- IHostConnectivityProbe ---- - /// - /// Gets the host connectivity statuses. - /// - /// A list of host connectivity statuses. + /// public IReadOnlyList GetHostStatuses() => [.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))]; @@ -560,9 +478,6 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover state.HostState = newState; state.HostStateChangedUtc = DateTime.UtcNow; } - // Driver.AbLegacy-005 — structured log of every probe-driven transition. Operators can - // grep the rolling Serilog file for the device address to see when a PLC was last - // reachable. Downgrades to Stopped log as Warning; recoveries log as Information. if (newState == HostState.Stopped) _logger.LogWarning( "AbLegacy probe transition. Driver={DriverInstanceId} Device={DeviceHostAddress} From={Old} To={New}", @@ -577,23 +492,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover // ---- IPerCallHostResolver ---- - /// - /// Map a full reference to the host string used as the resilience-pipeline breaker key. - /// Driver.AbLegacy-013 — the contract on requires that - /// implementations never throw on an unknown reference. The fallback chain is therefore: - /// - /// Known tag → its DeviceHostAddress. - /// Unknown reference but devices configured → the first device's host address - /// (multi-device drivers degrade to single-host behaviour rather than failing). - /// Unknown reference and no devices configured → the driver instance id, which - /// the dispatch layer treats as the single-host key per the interface - /// documentation. Reaching this branch indicates a misconfigured driver (no - /// devices) so callers that want to surface that should validate - /// before relying on per-tag routing. - /// - /// - /// The full reference to resolve. - /// The host address for the reference. + /// public string ResolveHost(string fullReference) { if (_tagsByName.TryGetValue(fullReference, out var def)) @@ -698,7 +597,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover } /// - /// Phase 4c #137 — whether a tag definition is an ARRAY. The canonical contract: a tag is + /// Whether a tag definition is an ARRAY. The canonical contract: a tag is /// an array ⟺ its is non-null (the parser /// sets it ≥1 only when isArray:true), so a 1-element array (ArrayLength:1) IS an array /// (review I-3). null ArrayLength ⇒ scalar. @@ -706,7 +605,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover private static bool IsArrayTag(AbLegacyTagDefinition def) => def.ArrayLength is int len && len >= 1; /// - /// Phase 4c #137 — the effective libplctag element count for a tag definition: the tag's + /// The effective libplctag element count for a tag definition: the tag's /// clamped to the PCCC file maximum /// ( = 256) when it is an array (≥1, INCLUDING 1), /// or 1 when the tag is scalar (null ArrayLength). Used both to size the runtime at @@ -740,9 +639,6 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover LibplctagPlcAttribute: device.Profile.LibplctagPlcAttribute, TagName: parsed.ToLibplctagName(), Timeout: _options.Timeout, - // Phase 4c #137 — multi-element PCCC file read. A multi-element span (ArrayLength - // > 1) creates the libplctag tag with that element count so a single read fetches - // the whole array from the base address; scalar tags pass 1 and read unchanged. ElementCount: EffectiveArrayLength(def))); try { @@ -763,7 +659,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover } /// - /// Driver.AbLegacy-011 — synchronous teardown. Mirrors the body of + /// Synchronous teardown. Mirrors the body of /// but never wraps the async path in /// .AsTask().GetAwaiter().GetResult(). The poll engine's DisposeAsync is /// drained with a ConfigureAwait(false) awaiter so a captured single-threaded @@ -841,7 +737,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover new(StringComparer.OrdinalIgnoreCase); /// - /// Parent-word runtimes for bit-within-word RMW writes (task #181). Keyed by the + /// Parent-word runtimes for bit-within-word RMW writes. Keyed by the /// parent address (bit suffix stripped) — e.g. writes to N7:0/3 + N7:0/5 share a /// single parent runtime for N7:0. /// @@ -916,7 +812,7 @@ public sealed class AbLegacyDriver : IDriver, IReadable, IWritable, ITagDiscover public bool ProbeInitialized { get; set; } /// - /// Driver.AbLegacy-005 — per-device latch for the structured "first non-zero + /// Per-device latch for the structured "first non-zero /// libplctag status" log. Reset to on a successful read so a /// future outage re-fires the warning rather than being suppressed by a stale flag. /// Concurrent readers on the same device may race the unlatched check + set, but the diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs index ace2128d..eaf42dda 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/AbLegacyDriverFactoryExtensions.cs @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy; /// /// Static factory registration helper for . Server's Program.cs -/// calls once at startup; the bootstrapper (task #248) then +/// calls once at startup; the bootstrapper then /// materialises AB Legacy DriverInstance rows from the central config DB into live /// driver instances. Mirrors GalaxyProxyDriverFactoryExtensions. /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/IAbLegacyTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/IAbLegacyTagRuntime.cs index 0dadf95b..6bc3dfa7 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/IAbLegacyTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/IAbLegacyTagRuntime.cs @@ -9,22 +9,27 @@ public interface IAbLegacyTagRuntime : IDisposable { /// Initializes the tag runtime. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task InitializeAsync(CancellationToken cancellationToken); /// Reads the current value of the tag. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task ReadAsync(CancellationToken cancellationToken); /// Writes the encoded value to the tag. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task WriteAsync(CancellationToken cancellationToken); /// Gets the current status of the tag operation. + /// The PCCC/libplctag status code of the last operation. int GetStatus(); /// Decodes the tag value according to the specified data type. /// The data type to decode. /// Optional bit index for bit-level access. + /// The decoded scalar value, boxed as . object? DecodeValue(AbLegacyDataType type, int? bitIndex); /// @@ -37,6 +42,7 @@ public interface IAbLegacyTagRuntime : IDisposable /// /// The PCCC element data type. /// The number of elements to decode. + /// The decoded elements as a typed CLR array, boxed as . object? DecodeArray(AbLegacyDataType type, int count); /// Encodes a value for writing to the tag. @@ -50,6 +56,7 @@ public interface IAbLegacyTagFactory { /// Creates a tag runtime instance with the specified parameters. /// The tag creation parameters. + /// The created tag runtime instance. IAbLegacyTagRuntime Create(AbLegacyTagCreateParams createParams); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs index 5e1a1b41..72ce1c57 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbLegacy/LibplctagLegacyTagRuntime.cs @@ -24,13 +24,6 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime Protocol = Protocol.ab_eip, // PCCC-over-EIP; libplctag routes via the PlcType-specific PCCC layer Name = p.TagName, Timeout = p.Timeout, - // Phase 4c #137 — multi-element PCCC file read. ElementCount tells libplctag how many - // consecutive elements to fetch from the base address (e.g. N7:0 with ElementCount=5 - // reads N7:0..N7:4 in one PCCC transaction). ElementCount=1 (the scalar default) leaves - // libplctag's own per-name element inference untouched, so scalar tags read exactly as - // before. ASSUMPTION: libplctag's ab_pccc layer honours elem_count for SLC/PLC-5 data - // files and lays the elements out contiguously in the tag buffer (verified against the - // libplctag.NET API surface; not live-proven — no PCCC fixture on this build host). ElementCount = p.ElementCount > 1 ? p.ElementCount : null, }; } @@ -142,10 +135,6 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime // silently clobbering the whole word. throw new NotSupportedException( "Bit-with-bitIndex writes must go through AbLegacyDriver.WriteBitInWordAsync."); - // Driver.AbLegacy-014 — use SetInt16 to match DecodeValue's GetInt16(0) decode. - // A PCCC B-file element is a 16-bit word; SetInt8 only writes 1 byte, leaving - // bits 8-15 from the previous read in the tag buffer and creating a - // decode/encode asymmetry. SetInt16(0, 0/1) writes the full 16-bit word. _tag.SetInt16(0, Convert.ToBoolean(value) ? (short)1 : (short)0); break; case AbLegacyDataType.Int: @@ -171,7 +160,7 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime } } - /// + /// Disposes the tag and releases its resources. public void Dispose() => _tag.Dispose(); private static PlcType MapPlcType(string attribute) => attribute switch @@ -186,9 +175,7 @@ internal sealed class LibplctagLegacyTagRuntime : IAbLegacyTagRuntime internal sealed class LibplctagLegacyTagFactory : IAbLegacyTagFactory { - /// Creates a new libplctag-backed tag runtime instance. - /// The parameters for tag creation. - /// A new tag runtime instance. + /// public IAbLegacyTagRuntime Create(AbLegacyTagCreateParams createParams) => new LibplctagLegacyTagRuntime(createParams); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/FocasDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/FocasDriverOptions.cs index 7e7ef820..00236c8a 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/FocasDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Contracts/FocasDriverOptions.cs @@ -3,9 +3,8 @@ using System.ComponentModel.DataAnnotations; namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS; /// -/// FOCAS driver configuration. One instance supports N CNC devices. Per plan decision #144 -/// each device gets its own (DriverInstanceId, HostAddress) bulkhead key at the -/// Phase 6.1 resilience layer. +/// FOCAS driver configuration. One instance supports N CNC devices. Each device gets its own +/// (DriverInstanceId, HostAddress) bulkhead key at the Phase 6.1 resilience layer. /// public sealed class FocasDriverOptions { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasAlarmProjection.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasAlarmProjection.cs index fb1d9218..c773ca87 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasAlarmProjection.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasAlarmProjection.cs @@ -277,6 +277,6 @@ internal sealed class FocasAlarmProjection : IAsyncDisposable /// Handle returned by . public sealed record FocasAlarmSubscriptionHandle(long Id) : IAlarmSubscriptionHandle { - /// Gets the diagnostic identifier for this subscription. + /// public string DiagnosticId => $"focas-alarm-sub-{Id}"; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs index 1e588f13..d36d6f0b 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriver.cs @@ -34,7 +34,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, private readonly EquipmentTagRefResolver _resolver; // Per-tag-name cache of the FocasAddress parsed once at InitializeAsync. ReadAsync / // WriteAsync look up the pre-parsed value instead of re-parsing tag.Address on every hot - // call — resolves Driver.FOCAS-008. ConcurrentDictionary is required because the poll loop + // call. ConcurrentDictionary is required because the poll loop // (ReadAsync) and the host write thread (WriteAsync) both mutate this cache when resolver- // produced equipment tags are encountered for the first time. private readonly ConcurrentDictionary _parsedAddressesByTagName = @@ -75,15 +75,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot))); } - /// Gets the driver instance identifier. + /// public string DriverInstanceId => _driverInstanceId; - /// Gets the driver type name. + /// public string DriverType => "FOCAS"; - /// Initializes the driver with configuration and prepares device connections and polling. - /// JSON configuration string for the driver. - /// Cancellation token for the operation. - /// A task representing the asynchronous initialization operation. + /// public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { Volatile.Write(ref _health, new DriverHealth(DriverState.Initializing, null, null)); @@ -91,7 +88,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, { // Fail fast if the factory is a stub/unimplemented backend — the operator must // see an actionable error at init rather than a phantom-Healthy driver that fails - // every read/write/subscribe silently (Driver.FOCAS-009). + // every read/write/subscribe silently. _clientFactory.EnsureUsable(); foreach (var device in _options.Devices) @@ -122,7 +119,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, } _tagsByName[tag.Name] = tag; // Cache the parsed FocasAddress so ReadAsync / WriteAsync don't re-parse on every - // hot-path call (Driver.FOCAS-008). The address string has already been validated + // hot-path call. The address string has already been validated // by FocasAddress.TryParse above; reusing the parsed record avoids per-tick allocs // on subscription pollers. _parsedAddressesByTagName[tag.Name] = parsed; @@ -171,19 +168,14 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, return Task.CompletedTask; } - /// Reinitializes the driver by shutting down and restarting with new configuration. - /// JSON configuration string for the driver. - /// Cancellation token for the operation. - /// A task representing the asynchronous reinitialization operation. + /// public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { await ShutdownAsync(cancellationToken).ConfigureAwait(false); await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false); } - /// Shuts down the driver, cancelling all running operations and releasing resources. - /// Cancellation token for the operation. - /// A task representing the asynchronous shutdown operation. + /// public async Task ShutdownAsync(CancellationToken cancellationToken) { await _poll.DisposeAsync().ConfigureAwait(false); @@ -195,8 +187,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, foreach (var state in _devices.Values) { // Cancel-then-dispose can race in tight shutdown loops; swallowing is intentional - // but we now log the cause so a noisy shutdown leaves a Debug trace - // (Driver.FOCAS-007). + // but we now log the cause so a noisy shutdown leaves a Debug trace. try { state.ProbeCts?.Cancel(); } catch (Exception ex) { _logger.LogDebug(ex, "Cancelling probe CTS for {Host} failed", state.Options.HostAddress); } state.ProbeCts?.Dispose(); @@ -218,13 +209,11 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, Volatile.Write(ref _health, new DriverHealth(DriverState.Unknown, Volatile.Read(ref _health).LastSuccessfulRead, null)); } - /// Gets the current health status of the driver. + /// public DriverHealth GetHealth() => Volatile.Read(ref _health); - /// Gets the current memory footprint of the driver. + /// public long GetMemoryFootprint() => 0; - /// Flushes optional internal caches. - /// Cancellation token for the operation. - /// A task representing the asynchronous cache flush operation. + /// public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; /// Gets the number of configured devices. @@ -240,12 +229,14 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// Test seam — returns true when a parsed for the given /// reference is present in the address cache (both authored and equipment-tag paths). + /// The tag or equipment-tag reference to check. + /// if a parsed address is cached for the reference; otherwise . internal bool IsParsedAddressCached(string reference) => _parsedAddressesByTagName.ContainsKey(reference); // Resolves a tag definition to its parsed FocasAddress, caching the result so that // equipment tags (resolver-produced, not seeded at InitializeAsync) don't re-parse the - // address string on every ReadAsync / WriteAsync hot-path call (Driver.FOCAS-008). + // address string on every ReadAsync / WriteAsync hot-path call. // Throwing inside the GetOrAdd factory propagates the exception to the caller and does // NOT store anything in the dictionary — consistent with the existing "fail fast on a // malformed address" behaviour from the init-time validation of authored tags. @@ -257,10 +248,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IReadable ---- - /// Reads values from one or more tags asynchronously. - /// A read-only list of tag references to read. - /// Cancellation token for the operation. - /// A task representing the asynchronous read operation. + /// public async Task> ReadAsync( IReadOnlyList fullReferences, CancellationToken cancellationToken) { @@ -329,10 +317,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IWritable ---- - /// Writes values to one or more tags asynchronously. - /// A read-only list of write requests. - /// Cancellation token for the operation. - /// A task representing the asynchronous write operation. + /// public async Task> WriteAsync( IReadOnlyList writes, CancellationToken cancellationToken) { @@ -401,18 +386,14 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- ITagDiscovery ---- - /// - /// Retry-until-stable: the FixedTree subtree is filled in asynchronously by - /// a couple of seconds AFTER connect, so the first - /// post-connect pass would miss it — the host must re-run - /// discovery until the captured node set is non-empty and stable. - /// + /// + // Retry-until-stable: the FixedTree subtree is filled in asynchronously by + // FixedTreeLoopAsync a couple of seconds AFTER connect, so the first post-connect + // DiscoverAsync pass would miss it — the host must re-run discovery until the captured + // node set is non-empty and stable. public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.UntilStable; - /// Discovers tags and builds the OPC UA address space asynchronously. - /// The address space builder for constructing the OPC UA namespace. - /// Cancellation token for the operation. - /// A task representing the asynchronous discovery operation. + /// public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(builder); @@ -589,19 +570,12 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- ISubscribable (polling overlay via shared engine) ---- - /// Subscribes to data changes on one or more tags. - /// A read-only list of tag references to subscribe to. - /// The interval at which to publish data changes. - /// Cancellation token for the operation. - /// A task representing the asynchronous subscription operation. + /// public Task SubscribeAsync( IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) => Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval)); - /// Unsubscribes from a previous subscription. - /// The subscription handle to unsubscribe from. - /// Cancellation token for the operation. - /// A task representing the asynchronous unsubscription operation. + /// public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) { _poll.Unsubscribe(handle); @@ -610,7 +584,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IHostConnectivityProbe ---- - /// Gets the connectivity status of all configured devices. + /// public IReadOnlyList GetHostStatuses() => [.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))]; @@ -623,7 +597,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, { var client = await EnsureConnectedAsync(state, ct).ConfigureAwait(false); // Apply Probe.Timeout so a hung CNC socket gets cancelled at the configured - // budget rather than blocking until the OS TCP timeout (Driver.FOCAS-009). + // budget rather than blocking until the OS TCP timeout. // TimeSpan.Zero / negative means "no per-probe timeout" — fall back to the loop // cancellation token unmodified. var probeTimeout = _options.Probe.Timeout; @@ -1048,10 +1022,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IAlarmSource ---- - /// Subscribes to alarm events from the driver. - /// A read-only list of source node IDs to subscribe to. - /// Cancellation token for the operation. - /// A task representing the asynchronous subscription operation. + /// public Task SubscribeAlarmsAsync( IReadOnlyList sourceNodeIds, CancellationToken cancellationToken) { @@ -1061,17 +1032,11 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, return _alarmProjection.SubscribeAsync(sourceNodeIds, cancellationToken); } - /// Unsubscribes from a previous alarm subscription. - /// The alarm subscription handle to unsubscribe from. - /// Cancellation token for the operation. - /// A task representing the asynchronous unsubscription operation. + /// public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) => _alarmProjection is { } p ? p.UnsubscribeAsync(handle, cancellationToken) : Task.CompletedTask; - /// Acknowledges one or more alarms. - /// A read-only list of alarm acknowledgement requests. - /// Cancellation token for the operation. - /// A task representing the asynchronous acknowledgement operation. + /// public Task AcknowledgeAsync( IReadOnlyList acknowledgements, CancellationToken cancellationToken) => _alarmProjection is { } p ? p.AcknowledgeAsync(acknowledgements, cancellationToken) : Task.CompletedTask; @@ -1113,9 +1078,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, // ---- IPerCallHostResolver ---- - /// Resolves the host address for a given tag reference. - /// The full reference of the tag. - /// The host address for the tag reference. + /// public string ResolveHost(string fullReference) { if (_tagsByName.TryGetValue(fullReference, out var def)) @@ -1159,6 +1122,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, /// Disposes the driver and releases all resources synchronously. public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); /// Disposes the driver and releases all resources asynchronously. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() => await ShutdownAsync(CancellationToken.None).ConfigureAwait(false); /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriverFactoryExtensions.cs index 49f2d667..d8f90458 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/FocasDriverFactoryExtensions.cs @@ -205,6 +205,7 @@ public static class FocasDriverFactoryExtensions /// internal sealed class FlexibleStringConverter : JsonConverter { + /// public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => reader.TokenType switch { @@ -216,6 +217,7 @@ public static class FocasDriverFactoryExtensions _ => throw new JsonException($"Expected string, number, or null but got {reader.TokenType}."), }; + /// public override void Write(Utf8JsonWriter writer, string? value, JsonSerializerOptions options) { if (value is null) writer.WriteNullValue(); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs index ce5486c1..3242223e 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/IFocasClient.cs @@ -20,6 +20,7 @@ public interface IFocasClient : IDisposable /// The CNC host address and port. /// The connection timeout duration. /// The cancellation token. + /// A task that represents the asynchronous operation. Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken); /// True when the FWLIB handle is valid + the socket is up. @@ -33,6 +34,7 @@ public interface IFocasClient : IDisposable /// The CNC memory address to read from. /// The FOCAS data type to read. /// The cancellation token. + /// A tuple of the boxed value read and the mapped OPC UA status code. Task<(object? value, uint status)> ReadAsync( FocasAddress address, FocasDataType type, @@ -46,6 +48,7 @@ public interface IFocasClient : IDisposable /// The FOCAS data type to write. /// The value to write. /// The cancellation token. + /// The mapped OPC UA status code (0 = Good). Task WriteAsync( FocasAddress address, FocasDataType type, @@ -57,6 +60,7 @@ public interface IFocasClient : IDisposable /// responds with any valid status. /// /// The cancellation token. + /// true when the CNC responds with a valid status; otherwise false. Task ProbeAsync(CancellationToken cancellationToken); /// @@ -66,6 +70,7 @@ public interface IFocasClient : IDisposable /// emits transitions (raise / clear) on the driver's OnAlarmEvent. /// /// The cancellation token. + /// The list of currently active alarms; empty when none are active. Task> ReadAlarmsAsync(CancellationToken cancellationToken); // ---- Fixed-tree T1 (identity + axis discovery + fast-poll dynamic bundle) ---- @@ -76,6 +81,7 @@ public interface IFocasClient : IDisposable /// values don't change across the session. /// /// The cancellation token. + /// The CNC identity snapshot. Task GetSysInfoAsync(CancellationToken cancellationToken); /// @@ -84,6 +90,7 @@ public interface IFocasClient : IDisposable /// calls. /// /// The cancellation token. + /// The CNC's configured axis names. Task> GetAxisNamesAsync(CancellationToken cancellationToken); /// @@ -91,6 +98,7 @@ public interface IFocasClient : IDisposable /// the Spindle/{name}/ subtree. /// /// The cancellation token. + /// The CNC's configured spindle names. Task> GetSpindleNamesAsync(CancellationToken cancellationToken); /// @@ -101,6 +109,7 @@ public interface IFocasClient : IDisposable /// /// The axis index to read dynamics for. /// The cancellation token. + /// The fast-poll dynamic bundle for the requested axis. Task ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken); // ---- Fixed-tree T2 (program + operation mode) ---- @@ -113,6 +122,7 @@ public interface IFocasClient : IDisposable /// on human-operator timescales. /// /// The cancellation token. + /// The aggregate program + operation-mode snapshot. Task GetProgramInfoAsync(CancellationToken cancellationToken); // ---- Fixed-tree T3 (timers) ---- @@ -124,6 +134,7 @@ public interface IFocasClient : IDisposable /// /// The timer kind to read. /// The cancellation token. + /// The requested cumulative timer reading. Task GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken); // ---- Fixed-tree T3.5 (servo meters) ---- @@ -134,6 +145,7 @@ public interface IFocasClient : IDisposable /// disconnected session or unsupported CNC. /// /// The cancellation token. + /// The servo-load meter percentages across all configured axes. Task> GetServoLoadsAsync(CancellationToken cancellationToken); // ---- Fixed-tree T3.6 (spindle meters) ---- @@ -145,6 +157,7 @@ public interface IFocasClient : IDisposable /// series like 16i may return EW_FUNC). /// /// The cancellation token. + /// Per-spindle load percentages, indexed as per . Task> GetSpindleLoadsAsync(CancellationToken cancellationToken); /// @@ -152,6 +165,7 @@ public interface IFocasClient : IDisposable /// bootstrap. Index alignment as per . /// /// The cancellation token. + /// Per-spindle maximum RPM values, indexed as per . Task> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken); /// @@ -161,6 +175,7 @@ public interface IFocasClient : IDisposable /// PositionDecimalPlaces. Values are clamped non-negative. /// /// The cancellation token. + /// Per-axis position decimal-place figures; empty when the CNC/backend doesn't report them. Task> GetPositionFiguresAsync(CancellationToken cancellationToken); } @@ -317,17 +332,10 @@ public sealed class UnimplementedFocasClientFactory : IFocasClientFactory "FOCAS driver backend is 'unimplemented'. Switch to 'Backend: \"wire\"' in driver config " + "once the CNC is provisioned — see docs/drivers/FOCAS.md."; - /// - /// Config-time probe — throws immediately so - /// faults the driver before any background loops start. This prevents the footgun where - /// the driver appears Healthy at init but every read/write/subscribe fails. - /// - /// Always thrown. + /// public void EnsureUsable() => throw new NotSupportedException(Message); - /// Creates a new client instance (always throws NotSupportedException). - /// Never returns; always throws NotSupportedException. - /// Always thrown to indicate backend is not yet provisioned. + /// public IFocasClient Create() => throw new NotSupportedException(Message); } @@ -352,5 +360,5 @@ public static class FocasAlarmType public const short Servo = 4; // ALM_S public const short DataIo = 5; // ALM_T public const short MemoryCheck = 6; // ALM_M - public const short MacroAlarm = 13; // ALM_MC — used by #3006 etc. + public const short MacroAlarm = 13; // ALM_MC } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/SynchronizedFocasClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/SynchronizedFocasClient.cs index e3f64078..66010716 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/SynchronizedFocasClient.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/SynchronizedFocasClient.cs @@ -110,7 +110,7 @@ public sealed class SynchronizedFocasClient : IFocasClient public Task> GetPositionFiguresAsync(CancellationToken cancellationToken) => RunBoundedAsync(ct => _inner.GetPositionFiguresAsync(ct), cancellationToken); - /// + /// Disposes the inner client and the call gate. public void Dispose() { _inner.Dispose(); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasConstants.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasConstants.cs index 5df6e15d..aab2aafd 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasConstants.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasConstants.cs @@ -61,7 +61,7 @@ public static class FocasOperationModeExtensions /// "EDIT"). Delegates to so the wire layer /// and the fixed-tree projection render identical labels — historically these two /// surfaces diverged ("TJOG" vs "T-JOG", "TEACH_IN_HANDLE" vs "TEACH-IN-HANDLE", - /// and different unknown-code fallbacks). Resolved by Driver.FOCAS-010. + /// and different unknown-code fallbacks). /// /// The operation mode. /// The operator-facing label. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireClient.cs index 9aa27033..02dce813 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireClient.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireClient.cs @@ -70,6 +70,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// The FOCAS/2 TCP port (typically 8193). /// Connection timeout in seconds; zero or negative disables the timeout. /// Cancellation token for the connect operation. + /// A task that represents the asynchronous operation. public Task ConnectAsync( string host, int port, @@ -90,6 +91,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// The FOCAS/2 TCP port (typically 8193). /// Connection timeout duration; disables the timeout. /// Cancellation token for the connect operation. + /// A task that represents the asynchronous operation. public Task ConnectAsync( string host, int port, @@ -183,6 +185,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Async dispose — sends the close PDU when connected and tears down both sockets. /// Idempotent. /// + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { await _lifetimeGate.WaitAsync(CancellationToken.None).ConfigureAwait(false); @@ -218,6 +221,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The CNC identity/sysinfo, wrapped in a . public async Task> ReadSysInfoAsync( CancellationToken cancellationToken = default, TimeSpan? timeout = null, @@ -233,6 +237,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The aggregated CNC status bits, wrapped in a . public async Task> ReadStatusAsync( CancellationToken cancellationToken = default, TimeSpan? timeout = null, @@ -272,6 +277,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The configured axis name records, wrapped in a . public async Task>> ReadAxisNamesAsync( short maxCount = 32, CancellationToken cancellationToken = default, @@ -288,6 +294,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The configured spindle name records, wrapped in a . public async Task>> ReadSpindleNamesAsync( short maxCount = 8, CancellationToken cancellationToken = default, @@ -308,6 +315,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The fast-poll dynamic snapshot for the axis, wrapped in a . public async Task> ReadDynamic2Async( short axis = 1, CancellationToken cancellationToken = default, @@ -353,6 +361,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The servo-meter load percentages, wrapped in a . public async Task>> ReadServoMeterAsync( short maxCount = 32, CancellationToken cancellationToken = default, @@ -387,6 +396,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The per-axis position decimal-place figures, wrapped in a . public async Task>> ReadPositionFiguresAsync( CancellationToken cancellationToken = default, TimeSpan? timeout = null, @@ -413,6 +423,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The per-spindle load percentages, wrapped in a . public Task>> ReadSpindleLoadAsync( short spindleSelector = -1, CancellationToken cancellationToken = default, @@ -425,6 +436,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The per-spindle maximum RPMs, wrapped in a . public Task>> ReadSpindleMaxRpmAsync( short spindleSelector = -1, CancellationToken cancellationToken = default, @@ -442,6 +454,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The raw parameter payload bytes, wrapped in a . public async Task> ReadParameterBytesAsync( short dataNumber, short axis = 0, @@ -461,6 +474,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The typed parameter value, wrapped in a . public async Task> ReadParameterAsync( short dataNumber, short type = 0, @@ -481,6 +495,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The typed 8-bit parameter value, wrapped in a . public async Task> ReadParameterByteAsync(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null) { var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false); @@ -495,6 +510,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The typed 16-bit parameter value, wrapped in a . public async Task> ReadParameterInt16Async(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null) { var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false); @@ -509,6 +525,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The typed 32-bit parameter value, wrapped in a . public async Task> ReadParameterInt32Async(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null) { var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false); @@ -523,6 +540,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The typed single-precision parameter value, wrapped in a . public async Task> ReadParameterFloat32Async(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null) { var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false); @@ -537,6 +555,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The typed double-precision parameter value, wrapped in a . public async Task> ReadParameterFloat64Async(short dataNumber, short axis = 0, CancellationToken cancellationToken = default, TimeSpan? timeout = null, ushort? pathId = null) { var result = await ReadParameterBytesAsync(dataNumber, axis, cancellationToken, timeout, pathId).ConfigureAwait(false); @@ -550,6 +569,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The macro variable value, wrapped in a . public Task> ReadMacroAsync( short number, CancellationToken cancellationToken = default, @@ -573,6 +593,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The decoded PMC range values, wrapped in a . public async Task> ReadPmcRangeAsync( short area, short dataType, @@ -601,6 +622,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The decoded PMC range values, wrapped in a . public Task> ReadPmcRangeAsync( FocasPmcArea area, FocasPmcDataType dataType, @@ -621,6 +643,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The active alarm list, wrapped in a . public async Task>> ReadAlarmsAsync( short type = -1, short count = 32, @@ -640,6 +663,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The current operation mode, wrapped in a . public Task> ReadOperationModeAsync( CancellationToken cancellationToken = default, TimeSpan? timeout = null, @@ -657,6 +681,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The raw operation mode code, wrapped in a . public Task> ReadOperationModeCodeAsync( CancellationToken cancellationToken = default, TimeSpan? timeout = null, @@ -670,6 +695,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The executing program name and O-number, wrapped in a . public Task> ReadExecutingProgramNameAsync( CancellationToken cancellationToken = default, TimeSpan? timeout = null, @@ -680,6 +706,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The executed block count, wrapped in a . public Task> ReadBlockCountAsync( CancellationToken cancellationToken = default, TimeSpan? timeout = null, @@ -697,6 +724,7 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// Cancellation token for the read operation. /// Optional per-call timeout override. /// Optional path ID override; defaults to . + /// The cumulative timer value, wrapped in a . public Task> ReadTimerAsync( short type, CancellationToken cancellationToken = default, @@ -895,6 +923,9 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// cutting-time payload ac f2 10 00 90 a3 00 00 → minute=1110188, msec=41872. See /// docs/plans/2026-06-25-focas-pdu-v3-30i-b-support.md. /// + /// Timer type selector, carried through into the returned . + /// The raw 8-byte cnc_rdtimer response payload. + /// The decoded minute/msec timer value. internal static WireTimer ParseTimer(short type, byte[] payload) => new( type, payload.Length >= 4 ? BinaryPrimitives.ReadInt32LittleEndian(payload.AsSpan(0, 4)) : 0, @@ -914,6 +945,10 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// machine's servo-meter screen — confirm magnitude at commissioning. The alignment + name /// fix here is what removes the gross misaligned garbage. /// + /// The raw cnc_rdsvmeter response payload. + /// The raw cnc_rdaxisname response payload used to correlate axis names by index. + /// Maximum number of servo meter records to return. + /// The decoded servo-meter records. internal static IReadOnlyList ParseServoMeters(byte[] svPayload, byte[] axisNamePayload, int maxCount) { var result = new List(); @@ -935,6 +970,8 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable } /// Byte width of one PMC slot for a FOCAS data-type code: Byte=1, Word=2, Long/Real=4, Double=8. + /// The PMC data width code. + /// The byte width of one slot for the given data-type code. internal static int PmcByteWidth(short dataType) => dataType switch { 1 => 2, @@ -950,6 +987,12 @@ public sealed class FocasWireClient : IAsyncDisposable, IDisposable /// WireFocasClient.ReadPmcAsync) or a trailing partial slot is dropped — which on the /// 31i-B surfaced as a spurious BadOutOfRange for a single Word read. 2026-06-25. /// + /// The PMC address-letter code numeric value. + /// The PMC data width code numeric value. + /// The starting address. + /// The ending address. + /// The raw pmc_rdpmcrng response payload. + /// The decoded PMC range values. internal static WirePmcRange ParsePmcRange(short area, short dataType, ushort start, ushort end, byte[] payload) { var width = PmcByteWidth(dataType); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireProtocol.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireProtocol.cs index 502a1a48..35df33d3 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireProtocol.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/FocasWireProtocol.cs @@ -32,6 +32,8 @@ internal static class FocasWireProtocol private static readonly ushort[] SupportedReadVersions = [1, 3]; /// True when is a PDU version this client can frame-parse. + /// The PDU version from an inbound header. + /// true if the version is one of the supported read PDU versions; otherwise, false. internal static bool IsSupportedReadVersion(ushort version) => Array.IndexOf(SupportedReadVersions, version) >= 0; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs index 5138ca1b..bea99a40 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.FOCAS/Wire/WireFocasClient.cs @@ -28,7 +28,7 @@ public sealed class WireFocasClient : IFocasClient /// /// Construct with an optional logger. Threaded through to /// so the per-response Debug entries actually reach - /// the host's logging pipeline (Driver.FOCAS-007). + /// the host's logging pipeline. /// /// Optional logger for debug output from wire client responses. public WireFocasClient(ILogger? logger) @@ -36,13 +36,10 @@ public sealed class WireFocasClient : IFocasClient _wire = new FocasWireClient(logger); } - /// Gets a value indicating whether the wire client is connected to the FOCAS host. + /// public bool IsConnected => _wire.IsConnected; - /// Connects to a FOCAS host at the specified address. - /// The host address containing the machine name and port. - /// The connection timeout; values less than or equal to zero are clamped to 1 second. - /// Cancellation token for the operation. + /// public async Task ConnectAsync(FocasHostAddress address, TimeSpan timeout, CancellationToken cancellationToken) { if (_wire.IsConnected) return; @@ -54,11 +51,7 @@ public sealed class WireFocasClient : IFocasClient await _wire.ConnectAsync(address.Host, address.Port, effective, cancellationToken).ConfigureAwait(false); } - /// Reads a value from the specified FOCAS address. - /// The FOCAS address to read from. - /// The FOCAS data type of the value. - /// Cancellation token for the operation. - /// A tuple containing the read value and FOCAS status code. + /// public async Task<(object? value, uint status)> ReadAsync( FocasAddress address, FocasDataType type, CancellationToken cancellationToken) { @@ -74,19 +67,12 @@ public sealed class WireFocasClient : IFocasClient }; } - /// Writes a value to a FOCAS address (always returns BadNotWritable as OtOpcUa is read-only). - /// The FOCAS address to write to. - /// The FOCAS data type of the value. - /// The value to write. - /// Cancellation token for the operation. - /// A task that returns the BadNotWritable status code. + /// public Task WriteAsync( FocasAddress address, FocasDataType type, object? value, CancellationToken cancellationToken) => Task.FromResult(FocasStatusMapper.BadNotWritable); - /// Probes the FOCAS host to verify connectivity. - /// Cancellation token for the operation. - /// True if the probe succeeds; otherwise false. + /// public async Task ProbeAsync(CancellationToken cancellationToken) { if (!_wire.IsConnected) return false; @@ -101,9 +87,7 @@ public sealed class WireFocasClient : IFocasClient } } - /// Reads all active alarms from the FOCAS host. - /// Cancellation token for the operation. - /// A list of active alarms; empty if read fails or not connected. + /// public async Task> ReadAlarmsAsync(CancellationToken cancellationToken) { if (!_wire.IsConnected) return []; @@ -125,9 +109,7 @@ public sealed class WireFocasClient : IFocasClient Message: a.Message ?? string.Empty); } - /// Gets system information from the FOCAS host. - /// Cancellation token for the operation. - /// The FOCAS system information. + /// public async Task GetSysInfoAsync(CancellationToken cancellationToken) { RequireConnected(); @@ -147,9 +129,7 @@ public sealed class WireFocasClient : IFocasClient AxesCount: axesCount); } - /// Gets the names of all axes on the FOCAS host. - /// Cancellation token for the operation. - /// A list of axis names; empty if read fails or not connected. + /// public async Task> GetAxisNamesAsync(CancellationToken cancellationToken) { if (!_wire.IsConnected) return []; @@ -169,9 +149,7 @@ public sealed class WireFocasClient : IFocasClient } } - /// Gets the names of all spindles on the FOCAS host. - /// Cancellation token for the operation. - /// A list of spindle names; empty if read fails or not connected. + /// public async Task> GetSpindleNamesAsync(CancellationToken cancellationToken) { if (!_wire.IsConnected) return []; @@ -190,10 +168,7 @@ public sealed class WireFocasClient : IFocasClient } } - /// Reads the dynamic state of a specified axis. - /// The index of the axis to read. - /// Cancellation token for the operation. - /// The dynamic snapshot of the axis. + /// public async Task ReadDynamicAsync(int axisIndex, CancellationToken cancellationToken) { // FOCAS axes are 1-based; cnc_rddynamic2 with axis 0 returns EW_4 (live-confirmed on the @@ -220,9 +195,7 @@ public sealed class WireFocasClient : IFocasClient DistanceToGo: pos.Distance); } - /// Gets information about the currently executing program. - /// Cancellation token for the operation. - /// The current program information. + /// public async Task GetProgramInfoAsync(CancellationToken cancellationToken) { RequireConnected(); @@ -250,10 +223,7 @@ public sealed class WireFocasClient : IFocasClient Mode: modeResult.IsOk ? modeResult.Value : 0); } - /// Gets a timer value from the FOCAS host. - /// The kind of timer to read (run time, cutting time, etc.). - /// Cancellation token for the operation. - /// The timer value. + /// public async Task GetTimerAsync(FocasTimerKind kind, CancellationToken cancellationToken) { RequireConnected(); @@ -263,9 +233,7 @@ public sealed class WireFocasClient : IFocasClient return new FocasTimer(kind, t.Minutes, t.Milliseconds); } - /// Gets servo load information for all axes. - /// Cancellation token for the operation. - /// A list of servo load values for each axis; empty if read fails or not connected. + /// public async Task> GetServoLoadsAsync(CancellationToken cancellationToken) { if (!_wire.IsConnected) return []; @@ -277,28 +245,18 @@ public sealed class WireFocasClient : IFocasClient .ToList(); } - /// Gets spindle load information for all spindles. - /// Cancellation token for the operation. - /// A list of spindle load percentages; empty if read fails or not connected. + /// public Task> GetSpindleLoadsAsync(CancellationToken cancellationToken) => ReadSpindleMetricAsync((sel, ct) => _wire.ReadSpindleLoadAsync(sel, ct), cancellationToken); - /// Gets maximum RPM information for all spindles. - /// Cancellation token for the operation. - /// A list of maximum RPM values for each spindle; empty if read fails or not connected. + /// public Task> GetSpindleMaxRpmsAsync(CancellationToken cancellationToken) => ReadSpindleMetricAsync((sel, ct) => _wire.ReadSpindleMaxRpmAsync(sel, ct), cancellationToken); - /// Gets the per-axis position decimal-place figures via cnc_getfigure. - /// Cancellation token for the operation. - /// - /// The per-axis decimal-place figures read from the CNC via - /// , or an empty list when the read - /// fails (non-zero RC — e.g. the series lacks cnc_getfigure) or is not connected. - /// Per the contract an empty list signals - /// the driver to fall back to the configured PositionDecimalPlaces. Never throws — - /// figure reads degrade to the config knob rather than faulting. - /// + /// + // Reads via FocasWireClient.ReadPositionFiguresAsync; returns an empty list when the read + // fails (non-zero RC — e.g. the series lacks cnc_getfigure) or is not connected. Never + // throws — figure reads degrade to the configured PositionDecimalPlaces rather than faulting. public async Task> GetPositionFiguresAsync(CancellationToken cancellationToken) { if (!_wire.IsConnected) return []; @@ -450,9 +408,8 @@ public sealed class WireFocasClientFactory : IFocasClientFactory /// /// Construct the factory with a logger that every created - /// forwards to its . Resolves Driver.FOCAS-007 — the wire - /// client already emits Debug entries per FOCAS response, but the previous no-arg - /// factory path discarded them. + /// forwards to its — the wire client already emits Debug + /// entries per FOCAS response, but the previous no-arg factory path discarded them. /// /// Optional logger for debug output from wire client responses. public WireFocasClientFactory(ILogger? logger) @@ -460,13 +417,10 @@ public sealed class WireFocasClientFactory : IFocasClientFactory _logger = logger; } - /// - /// No-op usability probe — the wire backend is always usable at config time. - /// Implements . - /// + /// + // No-op — the wire backend is always usable at config time. public void EnsureUsable() { } - /// Creates a new WireFocasClient instance. - /// A new IFocasClient implementation. + /// public IFocasClient Create() => new WireFocasClient(_logger); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs index f0f5aa71..fee2bdb1 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyBrowseSession.cs @@ -22,10 +22,10 @@ internal sealed class GalaxyBrowseSession : IBrowseSession private volatile bool _disposed; private IReadOnlyList? _roots; - /// Opaque token identifying this session in the AdminUI registry. + /// public Guid Token { get; } = Guid.NewGuid(); - /// Wall-clock time of the most recent successful Root/Expand/Attributes call. + /// public DateTime LastUsedUtc { get; private set; } = DateTime.UtcNow; /// @@ -40,11 +40,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession _client = client ?? throw new ArgumentNullException(nameof(client)); } - /// - /// Fetches the top-level s from the gateway and - /// returns them as s. Result is cached; a second call - /// returns the cached roots without a re-fetch. - /// + /// public async Task> RootAsync(CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -63,12 +59,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession } } - /// - /// Fetches the direct children of the cached node identified by - /// (the object's TagName) via - /// . Throws - /// if the tag hasn't been handed out by a prior Root/Expand call. - /// + /// public async Task> ExpandAsync(string nodeId, CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -84,11 +75,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession return Project(node.Children); } - /// - /// Fetches the attributes of the Galaxy object identified by - /// via DiscoverHierarchyAsync(MaxDepth=0, RootTagName=nodeId, IncludeAttributes=true). - /// Returns an empty list if the gateway has no matching object. - /// + /// public async Task> AttributesAsync(string nodeId, CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -151,6 +138,8 @@ internal sealed class GalaxyBrowseSession : IBrowseSession /// 0=FreeAccess, 1=Operate, 2=SecuredWrite, 3=VerifiedWrite, /// 4=Tune, 5=Configure, 6=ViewOnly; anything else surfaces as Unknown(N). /// + /// The raw Galaxy security-classification integer. + /// The display string for the classification, or Unknown(N) if unrecognized. // internal for unit-test access (InternalsVisibleTo on the src project). internal static string MapSecurityClass(int raw) => raw switch { @@ -170,6 +159,7 @@ internal sealed class GalaxyBrowseSession : IBrowseSession /// Both and the client dispose are wrapped /// in try/catch so a concurrent second dispose call never propagates. /// + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyDriverBrowser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyDriverBrowser.cs index a53a2386..d8a50d06 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyDriverBrowser.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser/GalaxyDriverBrowser.cs @@ -42,24 +42,17 @@ public sealed class GalaxyDriverBrowser : IDriverBrowser _logger = logger ?? NullLogger.Instance; } - /// Driver type key — matches the AdminUI's persisted "GalaxyMxGateway" value. + /// // Hardcoded literal: this project references Driver.Galaxy.Contracts, not Driver.Galaxy, // so GalaxyDriverFactoryExtensions.DriverTypeName isn't available here. public string DriverType => "GalaxyMxGateway"; - /// - /// Deserializes a blob, opens a transient - /// against the configured gateway endpoint, - /// and returns a browse session over it. The session owns the client and disposes - /// it on . - /// - /// Driver options serialized as JSON; same shape the runtime - /// driver would consume. - /// Cancellation for the connect phase only. - /// - /// Thrown when the JSON deserialises to null, when Gateway.Endpoint is empty, - /// or when MxAccess.ClientName is empty. - /// + // Deserializes a GalaxyDriverOptions blob, opens a transient GalaxyRepositoryClient + // against the configured gateway endpoint, and returns a browse session over it. The + // session owns the client and disposes it on IBrowseSession.DisposeAsync. Throws + // InvalidOperationException when the JSON deserialises to null, when Gateway.Endpoint + // is empty, or when MxAccess.ClientName is empty. + /// public async Task OpenAsync(string configJson, CancellationToken cancellationToken) { var opts = JsonSerializer.Deserialize(configJson, JsonOpts) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/GalaxyDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/GalaxyDriverOptions.cs index 68446e17..594b2d77 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/GalaxyDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/GalaxyDriverOptions.cs @@ -67,8 +67,8 @@ public sealed record GalaxyGatewayOptions( /// /// /// Wonderware client identity. MUST be unique per OtOpcUa instance — when two instances -/// share a name, the older session loses subscription state. Redundancy pairs (decision -/// #149) enforce uniqueness via install scripts. +/// share a name, the older session loses subscription state. Redundancy pairs +/// enforce uniqueness via install scripts. /// /// /// Hint forwarded as buffered_update_interval_ms on subscribe; lets the worker diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/GalaxySecretRef.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/GalaxySecretRef.cs index f11efe4e..db123265 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/GalaxySecretRef.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Contracts/GalaxySecretRef.cs @@ -18,7 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Config; /// Anything else — used as the literal API key for back-compat with /// configs that pre-date this resolver. When a logger is supplied the /// resolver emits a startup warning so an operator who accidentally -/// committed a cleartext key sees it (Driver.Galaxy-010). +/// committed a cleartext key sees it. /// /// A future PR can swap any of these arms for a DPAPI-backed lookup without /// changing the call site. @@ -39,6 +39,7 @@ public static class GalaxySecretRef /// /// The secret reference string to resolve. /// Optional logger for warning on cleartext keys. + /// The resolved API-key string. public static string ResolveApiKey(string secretRef, ILogger? logger = null) { ArgumentException.ThrowIfNullOrEmpty(secretRef); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/AlarmRefBuilder.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/AlarmRefBuilder.cs index 2ab50439..9c9bb6ef 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/AlarmRefBuilder.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/AlarmRefBuilder.cs @@ -39,6 +39,7 @@ internal static class AlarmRefBuilder /// The full reference of the alarm-bearing attribute. /// The initial alarm severity level. /// The initial alarm description. + /// The populated with all five sub-attribute references. public static AlarmConditionInfo Build( string fullReference, AlarmSeverity initialSeverity = AlarmSeverity.Medium, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs index 92f08c6c..16e85cd2 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DataTypeMap.cs @@ -14,12 +14,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browse; /// and encode (Boolean, Int32, Int64, Float32, Float64, String, DateTime). Without it an /// Int64 attribute fell through to the default, /// creating a String address-space node while runtime reads decoded a boxed long — -/// a metadata / coercion mismatch (Driver.Galaxy-002). +/// a metadata / coercion mismatch. /// internal static class DataTypeMap { /// Maps an MXAccess data type ID to a driver data type. /// The MXAccess data type ID. + /// The corresponding , or for unknown codes. public static DriverDataType Map(int mxDataType) => mxDataType switch { 0 => DriverDataType.Boolean, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs index c11325fb..75b7da40 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/DeployWatcher.cs @@ -98,6 +98,7 @@ public sealed class DeployWatcher : IDisposable } /// Cancels the loop and waits for it to exit cleanly. + /// A task that represents the asynchronous operation. public async Task StopAsync() { var cts = _cts; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GalaxyDiscoverer.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GalaxyDiscoverer.cs index f1febd84..9caeeac2 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GalaxyDiscoverer.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GalaxyDiscoverer.cs @@ -44,6 +44,7 @@ public sealed class GalaxyDiscoverer /// /// The address space builder to populate with discovery results. /// The cancellation token for the operation. + /// A task that represents the asynchronous operation. public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GatewayGalaxyDeployWatchSource.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GatewayGalaxyDeployWatchSource.cs index c984c251..4486efc8 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GatewayGalaxyDeployWatchSource.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GatewayGalaxyDeployWatchSource.cs @@ -22,10 +22,7 @@ public sealed class GatewayGalaxyDeployWatchSource : IGalaxyDeployWatchSource _client = client ?? throw new ArgumentNullException(nameof(client)); } - /// Watches for deploy events asynchronously. - /// The last deploy time that was observed. - /// The cancellation token. - /// An async enumerable of deploy events. + /// public IAsyncEnumerable WatchAsync( DateTimeOffset? lastSeenDeployTime, CancellationToken cancellationToken) => _client.WatchDeployEventsAsync(lastSeenDeployTime, cancellationToken); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GatewayGalaxyHierarchySource.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GatewayGalaxyHierarchySource.cs index d3382ddb..bafabb59 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GatewayGalaxyHierarchySource.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/GatewayGalaxyHierarchySource.cs @@ -20,10 +20,7 @@ public sealed class GatewayGalaxyHierarchySource : IGalaxyHierarchySource _client = client ?? throw new ArgumentNullException(nameof(client)); } - /// - /// Discovers the Galaxy object hierarchy asynchronously via the gateway. - /// - /// Cancellation token for the operation. + /// public Task> GetHierarchyAsync(CancellationToken cancellationToken) => _client.DiscoverHierarchyAsync(cancellationToken); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/IGalaxyDeployWatchSource.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/IGalaxyDeployWatchSource.cs index 8ed170d3..cde27e83 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/IGalaxyDeployWatchSource.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/IGalaxyDeployWatchSource.cs @@ -21,6 +21,7 @@ public interface IGalaxyDeployWatchSource /// /// The last seen deploy time, or null to receive a bootstrap event. /// The cancellation token. + /// An async stream of deploy events, starting with a bootstrap event unless suppressed. IAsyncEnumerable WatchAsync( DateTimeOffset? lastSeenDeployTime, CancellationToken cancellationToken); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/IGalaxyHierarchySource.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/IGalaxyHierarchySource.cs index fd3c0b06..b1f96ba3 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/IGalaxyHierarchySource.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Browse/IGalaxyHierarchySource.cs @@ -16,5 +16,6 @@ public interface IGalaxyHierarchySource /// callers don't reimplement paging. /// /// Cancellation token for the operation. + /// The full materialised Galaxy object hierarchy. Task> GetHierarchyAsync(CancellationToken cancellationToken); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs index 65a199de..aacd8190 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs @@ -87,7 +87,7 @@ public sealed class GalaxyDriver private bool _alarmFeedWired; // List preserves insertion order so OnAlarmFeedTransition always picks the // earliest-registered handle — a deterministic choice that doesn't vary as - // handles are added/removed (Driver.Galaxy-006 fix: HashSet.First() is unstable). + // handles are added/removed (HashSet.First() is unstable). private readonly List _alarmSubscriptions = new(); // Production runtime owned by InitializeAsync (BuildProductionRuntimeAsync). The @@ -200,9 +200,10 @@ public sealed class GalaxyDriver /// drives this on a /// background task in production; tests prefer to invoke it directly so the /// branch can be - /// asserted deterministically (Driver.Galaxy-013). + /// asserted deterministically. /// /// Cancellation token for the replay operation. + /// A task that represents the asynchronous operation. internal Task InvokeReplayForTestAsync(CancellationToken cancellationToken) => ReplayAsync(cancellationToken); @@ -310,7 +311,7 @@ public sealed class GalaxyDriver /// handles are dead once the session reopened. The faulted /// is recreated first so the replayed subscriptions have a live StreamEvents /// consumer; without that restart the replayed tags are subscribed on the gw but - /// never reach OnDataChange (Driver.Galaxy-008). PR 6.x can swap this for + /// never reach OnDataChange. PR 6.x can swap this for /// the gw's batched ReplaySubscriptionsCommand once it ships. /// private async Task ReplayAsync(CancellationToken cancellationToken) @@ -319,11 +320,11 @@ public sealed class GalaxyDriver var entries = _subscriptions.SnapshotEntries(); if (entries.Count == 0) return; - // Driver.Galaxy-013: honor ReplayOnSessionLost. When operators opt out (false) - // we skip the per-tag SubscribeBulk fan-out — they're delegating to the - // gateway's session-level ReplaySubscriptions or accept post-reconnect tag - // loss. We still restart the EventPump so a future Subscribe call lands on - // a live consumer. + // Honor ReplayOnSessionLost. When operators opt out (false) we skip the + // per-tag SubscribeBulk fan-out — they're delegating to the gateway's + // session-level ReplaySubscriptions or accept post-reconnect tag loss. We + // still restart the EventPump so a future Subscribe call lands on a live + // consumer. if (!_options.Reconnect.ReplayOnSessionLost) { RestartEventPumpForReplay(); @@ -440,10 +441,10 @@ public sealed class GalaxyDriver private MxGatewayClientOptions BuildClientOptions(GalaxyGatewayOptions gw) => new() { Endpoint = new Uri(gw.Endpoint, UriKind.Absolute), - // Driver.Galaxy-010: pass the logger so the literal-arm cleartext fallback - // surfaces a startup warning rather than silently shipping the key. The - // resolver lives in Driver.Galaxy.Contracts (GalaxySecretRef) so the runtime - // driver and the AdminUI browser share one implementation. + // Pass the logger so the literal-arm cleartext fallback surfaces a startup + // warning rather than silently shipping the key. The resolver lives in + // Driver.Galaxy.Contracts (GalaxySecretRef) so the runtime driver and the + // AdminUI browser share one implementation. ApiKey = GalaxySecretRef.ResolveApiKey(gw.ApiKeySecretRef, _logger), UseTls = gw.UseTls, CaCertificatePath = gw.CaCertificatePath, @@ -459,8 +460,8 @@ public sealed class GalaxyDriver // Reuse the lazily-built repository client (DiscoverAsync constructs it on demand). // If discovery hasn't run yet, build the client here so the watcher has a target. - // Driver.Galaxy-009 fix: guard with ??= so if BuildDefaultHierarchySource later runs - // it reuses this client rather than overwriting the field and leaking the first instance. + // Guard with ??= so if BuildDefaultHierarchySource later runs it reuses this client + // rather than overwriting the field and leaking the first instance. _ownedRepositoryClient ??= ZB.MOM.WW.MxGateway.Client.GalaxyRepositoryClient.Create( BuildClientOptions(_options.Gateway)); @@ -470,31 +471,26 @@ public sealed class GalaxyDriver // StartAsync schedules the background loop and returns Task.CompletedTask immediately. // It throws InvalidOperationException synchronously if called twice (programming error). - // Driver.Galaxy-009 fix: don't discard the return value — observe any synchronous throw. + // Don't discard the return value — observe any synchronous throw. var startTask = _deployWatcher.StartAsync(CancellationToken.None); // The task is already completed (StartAsync is synchronous); surface any synchronous fault. if (startTask.IsFaulted) startTask.GetAwaiter().GetResult(); } /// - /// - /// - /// In-place config reapply. The driver does not currently support - /// hot-swapping at runtime — changing the - /// gateway endpoint, MxAccess client name, or reconnect policy requires - /// tearing down the gw session, supervisor, event pump, and address space. - /// The host stack handles that via DriverInstance restart, so this method - /// only accepts an equivalent config (no meaningful change) and refreshes - /// health; a non-equivalent reapply throws - /// so the caller knows the change wasn't applied (Driver.Galaxy-013: - /// previously the method silently ignored driverConfigJson). - /// - /// public Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); if (!string.IsNullOrWhiteSpace(driverConfigJson)) { + // In-place config reapply. The driver does not currently support hot-swapping + // GalaxyDriverOptions at runtime — changing the gateway endpoint, MxAccess + // client name, or reconnect policy requires tearing down the gw session, + // supervisor, event pump, and address space. The host stack handles that via + // DriverInstance restart, so this method only accepts an equivalent config (no + // meaningful change) and refreshes health; a non-equivalent reapply throws + // NotSupportedException so the caller knows the change wasn't applied. + // // Materialise the incoming config and compare against the live options. We // refuse any change that would require a session teardown rather than // pretending to apply it. @@ -562,17 +558,13 @@ public sealed class GalaxyDriver public IReadOnlyList GetHostStatuses() => _hostStatuses.Snapshot(); /// - /// - /// Estimated footprint: 64 bytes × tracked item handles (one gw subscription entry - /// per bound tag) + 256 bytes × tracked driver subscriptions (registry overhead per - /// OPC UA monitored item). Returns 0 when no subscriptions are active. These - /// constants are conservative — a 50k-tag set occupies ~3 MB and registers clearly - /// with the server's cache-flush heuristic. Driver.Galaxy-011: the stale - /// "PR 4.4 sets this" comment is removed; PR 4.4 shipped the SubscriptionRegistry - /// but never wired it here. - /// public long GetMemoryFootprint() { + // Estimated footprint: 64 bytes × tracked item handles (one gw subscription entry + // per bound tag) + 256 bytes × tracked driver subscriptions (registry overhead per + // OPC UA monitored item). Returns 0 when no subscriptions are active. These + // constants are conservative — a 50k-tag set occupies ~3 MB and registers clearly + // with the server's cache-flush heuristic. const long BytesPerItemHandle = 64L; // TagBinding + reverse-map entry const long BytesPerSubscription = 256L; // SubscriptionEntry overhead return (_subscriptions.TrackedItemHandleCount * BytesPerItemHandle) @@ -584,13 +576,12 @@ public sealed class GalaxyDriver // ===== ITagDiscovery (PR 4.1) ===== - /// - /// Run-once: fetches the full Galaxy hierarchy inline and - /// streams the complete node set within a single awaited call — there is no FOCAS-style - /// background cache that fills in after connect. Galaxy is a heavy network driver, so the - /// bounded post-connect retry loop is deliberately avoided; re-discovery on Galaxy - /// redeploy is handled separately via + the deploy-event watcher. - /// + /// + // Run-once: DiscoverAsync fetches the full Galaxy hierarchy inline and streams the complete + // node set within a single awaited call — there is no FOCAS-style background cache that fills + // in after connect. Galaxy is a heavy network driver, so the bounded post-connect retry loop + // is deliberately avoided; re-discovery on Galaxy redeploy is handled separately via + // IRediscoverable + the deploy-event watcher. public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; /// @@ -710,10 +701,10 @@ public sealed class GalaxyDriver } // Register bindings so the pump knows to dispatch events for these handles. - // Driver.Galaxy-012: index the SubscribeBulk results once and correlate to - // references in O(1) instead of FirstOrDefault per element (O(n²) over the - // batch). On the 50k-tag soak path this turns a 2.5G-comparison loop into - // a single Dictionary build + linear scan. + // Index the SubscribeBulk results once and correlate to references in O(1) + // instead of FirstOrDefault per element (O(n²) over the batch). On the + // 50k-tag soak path this turns a 2.5G-comparison loop into a single + // Dictionary build + linear scan. var resultIndex = BuildResultIndex(results); var bindings = new List(fullReferences.Count); for (var i = 0; i < fullReferences.Count; i++) @@ -853,8 +844,8 @@ public sealed class GalaxyDriver // recorded with a non-positive ItemHandle so the caller can detect partial failure // by inspecting the returned handle's diagnostic context — full per-tag error // surface lands in PR 5.3's parity tests. - // Driver.Galaxy-012: index results once, correlate in O(1) per reference rather - // than FirstOrDefault inside the loop (O(n²) on the 50k-tag path). + // Index results once, correlate in O(1) per reference rather than + // FirstOrDefault inside the loop (O(n²) on the 50k-tag path). var resultIndex = BuildResultIndex(results); var bindings = new List(fullReferences.Count); for (var i = 0; i < fullReferences.Count; i++) @@ -1084,9 +1075,9 @@ public sealed class GalaxyDriver // by SourceNodeId (not by handle), so every active subscriber sees the same // transition regardless of which handle is attached here. Using the first // insertion-order entry is deterministic and stable as long as at least one - // subscription remains — HashSet.First() was unstable across mutations - // (Driver.Galaxy-006 fix). _alarmSubscriptions is a List, so [0] is always - // the earliest-registered handle. + // subscription remains — HashSet.First() was unstable across mutations. + // _alarmSubscriptions is a List, so [0] is always the earliest-registered + // handle. subCount = _alarmSubscriptions.Count; handle = subCount > 0 ? _alarmSubscriptions[0] : null; } @@ -1162,9 +1153,9 @@ public sealed class GalaxyDriver /// private IGalaxyHierarchySource BuildDefaultHierarchySource() { - // Driver.Galaxy-009 fix: reuse a client that StartDeployWatcher may have already - // created (??=) rather than always overwriting the field and leaking the first - // instance. Both paths produce equivalent clients from the same options. + // Reuse a client that StartDeployWatcher may have already created (??=) rather + // than always overwriting the field and leaking the first instance. Both paths + // produce equivalent clients from the same options. _ownedRepositoryClient ??= GalaxyRepositoryClient.Create(BuildClientOptions(_options.Gateway)); return new TracedGalaxyHierarchySource( new GatewayGalaxyHierarchySource(_ownedRepositoryClient), _options.MxAccess.ClientName); @@ -1173,10 +1164,11 @@ public sealed class GalaxyDriver /// /// Asynchronous disposal. Prefer await using over using — the /// async path does not block the caller while awaiting EventPump / session / - /// client shutdown (Driver.Galaxy-007: the sync path blocked on - /// GetAwaiter().GetResult() for every async sub-component, risking a - /// deadlock under thread-pool starvation). + /// client shutdown (the sync path blocks on GetAwaiter().GetResult() + /// for every async sub-component, risking a deadlock under thread-pool + /// starvation). /// + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_disposed) return; @@ -1250,26 +1242,18 @@ public sealed class GalaxyDriver System.Collections.Concurrent.ConcurrentDictionary map) : IAddressSpaceBuilder { - /// Creates a folder node and returns a builder for populating it. - /// The OPC UA BrowseName of the folder. - /// The display name for the folder. + /// public IAddressSpaceBuilder Folder(string browseName, string displayName) => new SecurityCapturingBuilder(inner.Folder(browseName, displayName), map); - /// Creates a variable node and captures its security classification. - /// The OPC UA BrowseName of the variable. - /// The display name for the variable. - /// The driver attribute metadata including security classification. + /// public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo) { map[attributeInfo.FullName] = attributeInfo.SecurityClass; return inner.Variable(browseName, displayName, attributeInfo); } - /// Adds a property node to the current parent. - /// The OPC UA BrowseName of the property. - /// The OPC UA data type of the property. - /// The property value. + /// public void AddProperty(string browseName, DriverDataType dataType, object? value) => inner.AddProperty(browseName, dataType, value); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs index 3a2cbd50..d4f4f1d1 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs @@ -34,6 +34,7 @@ public static class GalaxyDriverFactoryExtensions /// Convenience for tests + standalone callers. /// The unique identifier for the driver instance. /// The driver configuration in JSON format. + /// The constructed Galaxy driver instance. public static GalaxyDriver CreateInstance(string driverInstanceId, string driverConfigJson) => CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null); @@ -41,6 +42,7 @@ public static class GalaxyDriverFactoryExtensions /// The unique identifier for the driver instance. /// The driver configuration in JSON format. /// The optional logger factory for creating drivers. + /// The constructed Galaxy driver instance. public static GalaxyDriver CreateInstance( string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory) { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverProbe.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverProbe.cs index 2155f639..a4010d07 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverProbe.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverProbe.cs @@ -145,6 +145,10 @@ public sealed class GalaxyDriverProbe : IDriverProbe /// ) counts as reachable because it proves a live /// gateway gRPC server answered. /// + /// The gRPC status code returned by the ping RPC. + /// The probed host, used to compose the failure message. + /// The probed port, used to compose the failure message. + /// Whether the gateway should be considered reachable, plus a human-readable message. internal static (bool ok, string message) ClassifyRpc(StatusCode code, string host, int port) => code switch { StatusCode.OK => (true, "gateway gRPC OK"), diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostConnectivityForwarder.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostConnectivityForwarder.cs index eda0c0bd..fdc09873 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostConnectivityForwarder.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostConnectivityForwarder.cs @@ -13,7 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health; /// /// /// The eventual production source for this signal is the gateway's StreamSessionHealth -/// RPC (mxaccessgw issue gw-6). Until that ships, the driver-side reconnect supervisor +/// RPC. Until that ships, the driver-side reconnect supervisor /// (PR 4.5) calls on transport state transitions: /// when the gw session re-Registers, /// when the supervisor moves to TransportLost. The forwarder is intentionally @@ -57,7 +57,7 @@ public sealed class HostConnectivityForwarder : IDisposable /// Disposes the forwarder and marks it as disposed. public void Dispose() { - // No-op today; reserved for the eventual gw-6 StreamSessionHealth consumer that + // No-op today; reserved for the eventual StreamSessionHealth consumer that // will own a long-running task this method tears down. _disposed = true; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostStatusAggregator.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostStatusAggregator.cs index 01f8cc3f..29a3d6cf 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostStatusAggregator.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/HostStatusAggregator.cs @@ -39,6 +39,7 @@ public sealed class HostStatusAggregator /// Snapshot the current host set. Suitable as the body of /// IHostConnectivityProbe.GetHostStatuses(). /// + /// The current set of tracked host connectivity statuses. public IReadOnlyList Snapshot() { lock (_lock) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/PerPlatformProbeWatcher.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/PerPlatformProbeWatcher.cs index daefb2ea..b766e38b 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/PerPlatformProbeWatcher.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Health/PerPlatformProbeWatcher.cs @@ -77,6 +77,7 @@ public sealed class PerPlatformProbeWatcher : IDisposable /// /// The platform tag names to synchronize. /// A cancellation token to cancel the operation. + /// A task that represents the asynchronous operation. public async Task SyncPlatformsAsync( IEnumerable platformTagNames, CancellationToken cancellationToken) { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/EventPump.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/EventPump.cs index 29d8a865..0d7dd12e 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/EventPump.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/EventPump.cs @@ -100,8 +100,8 @@ internal sealed class EventPump : IAsyncDisposable // false immediately, which we account for via the EventsDropped counter. // We deliberately do NOT use BoundedChannelFullMode.DropWrite — that // would silently discard the new event inside Channel without - // surfacing the drop on a counter (Driver.Galaxy-005: keep the comment - // and the FullMode value consistent). + // surfacing the drop on a counter (keep this comment and the FullMode + // value consistent). FullMode = BoundedChannelFullMode.Wait, SingleReader = true, SingleWriter = true, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GalaxyMxSession.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GalaxyMxSession.cs index 4758b7db..0daae292 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GalaxyMxSession.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GalaxyMxSession.cs @@ -68,6 +68,7 @@ public sealed class GalaxyMxSession : IAsyncDisposable /// /// The MX gateway client options. /// The cancellation token. + /// A task that represents the asynchronous operation. public async Task ConnectAsync(MxGatewayClientOptions clientOptions, CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -107,6 +108,7 @@ public sealed class GalaxyMxSession : IAsyncDisposable /// /// The MX gateway client options. /// The cancellation token. + /// A task that represents the asynchronous operation. public async Task RecreateAsync(MxGatewayClientOptions clientOptions, CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, this); @@ -136,6 +138,7 @@ public sealed class GalaxyMxSession : IAsyncDisposable public MxGatewaySession? Session => _session; /// Disposes the session and underlying gateway client resources. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GalaxySubscriptionHandle.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GalaxySubscriptionHandle.cs index 9103e395..290984d6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GalaxySubscriptionHandle.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GalaxySubscriptionHandle.cs @@ -8,6 +8,6 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; /// internal sealed record GalaxySubscriptionHandle(long SubscriptionId) : ISubscriptionHandle { - /// Gets the diagnostic identifier for the subscription. + /// public string DiagnosticId => $"galaxy-sub-{SubscriptionId}"; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyAlarmAcknowledger.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyAlarmAcknowledger.cs index 19b957da..c0427d53 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyAlarmAcknowledger.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyAlarmAcknowledger.cs @@ -32,11 +32,7 @@ internal sealed class GatewayGalaxyAlarmAcknowledger : IGalaxyAlarmAcknowledger _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - /// Acknowledges an alarm via the gateway. - /// The full reference path of the alarm to acknowledge. - /// An operator-supplied comment attached to the acknowledgement. - /// The name of the operator performing the acknowledgement. - /// A token to cancel the asynchronous operation. + /// public async Task AcknowledgeAsync( string alarmFullReference, string comment, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyAlarmFeed.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyAlarmFeed.cs index e01c1c61..0e45269d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyAlarmFeed.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyAlarmFeed.cs @@ -39,6 +39,7 @@ internal sealed class GatewayGalaxyAlarmFeed : IGalaxyAlarmFeed /// /// The stream request parameters. /// A cancellation token. + /// The async stream of decoded alarm-feed messages. internal delegate IAsyncEnumerable AlarmStreamFactory( StreamAlarmsRequest request, CancellationToken cancellationToken); @@ -90,7 +91,7 @@ internal sealed class GatewayGalaxyAlarmFeed : IGalaxyAlarmFeed _clientTag = new KeyValuePair("galaxy.client", clientName ?? ""); } - /// Starts the alarm feed by opening the stream and processing messages in a background task. + /// public void Start() { ObjectDisposedException.ThrowIf(_disposed, this); @@ -261,6 +262,7 @@ internal sealed class GatewayGalaxyAlarmFeed : IGalaxyAlarmFeed }; /// Releases the alarm feed resources and stops the background stream task. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyDataWriter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyDataWriter.cs index d9c8f9e5..1389b51c 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyDataWriter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxyDataWriter.cs @@ -115,11 +115,7 @@ public sealed class GatewayGalaxyDataWriter : IGalaxyDataWriter return null; } - /// Writes values to Galaxy tags through the gateway. - /// The write requests. - /// Function to resolve security classification per tag. - /// The cancellation token. - /// The write results per request. + /// public async Task> WriteAsync( IReadOnlyList writes, Func securityResolver, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxySubscriber.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxySubscriber.cs index 6dee938b..0d03d7ec 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxySubscriber.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/GatewayGalaxySubscriber.cs @@ -37,11 +37,7 @@ public sealed class GatewayGalaxySubscriber : IGalaxySubscriber _logger = logger ?? NullLogger.Instance; } - /// Subscribes to a bulk list of Galaxy references with optional buffered update interval. - /// The full Galaxy tag references to subscribe to. - /// The buffered update interval in milliseconds. - /// The cancellation token. - /// A task that returns a list of subscribe results. + /// public async Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) { @@ -142,10 +138,7 @@ public sealed class GatewayGalaxySubscriber : IGalaxySubscriber /// true if the interval should be recorded as applied; otherwise false. internal static bool ClassifyIntervalReply(ProtocolStatusCode? code) => code == ProtocolStatusCode.Ok; - /// Unsubscribes from a bulk list of item handles. - /// The item handles to unsubscribe from. - /// The cancellation token. - /// A task representing the unsubscribe operation. + /// public async Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) { if (itemHandles.Count == 0) return; @@ -159,9 +152,7 @@ public sealed class GatewayGalaxySubscriber : IGalaxySubscriber .ConfigureAwait(false); } - /// Streams Galaxy MX events asynchronously. - /// The cancellation token. - /// An async enumerable of MX events. + /// public IAsyncEnumerable StreamEventsAsync(CancellationToken cancellationToken) { var session = _session.Session diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxyAlarmAcknowledger.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxyAlarmAcknowledger.cs index aa9b1c89..dc57f456 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxyAlarmAcknowledger.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxyAlarmAcknowledger.cs @@ -24,6 +24,7 @@ internal interface IGalaxyAlarmAcknowledger /// OPC UA session by the server-side ACL layer before reaching the driver. /// /// Cancels the gateway RPC. + /// A task that represents the asynchronous operation. Task AcknowledgeAsync( string alarmFullReference, string comment, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxyDataWriter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxyDataWriter.cs index 5032463c..bdcd730f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxyDataWriter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxyDataWriter.cs @@ -26,6 +26,7 @@ public interface IGalaxyDataWriter /// (the safest default — non-secured Write). /// /// Aborts the in-flight batch. + /// One per request entry, in input order. Task> WriteAsync( IReadOnlyList writes, Func securityResolver, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxySubscriber.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxySubscriber.cs index 9d33968e..427c902f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxySubscriber.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/IGalaxySubscriber.cs @@ -20,12 +20,14 @@ public interface IGalaxySubscriber /// The list of tag references to subscribe to. /// The buffered update interval in milliseconds. /// Cancellation token for the operation. + /// One per requested reference, in input order. Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken); /// Unsubscribe a batch of item handles obtained from . /// The item handles to unsubscribe. /// Cancellation token for the operation. + /// A task that represents the asynchronous operation. Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken); /// @@ -34,5 +36,6 @@ public interface IGalaxySubscriber /// its . /// /// Cancellation token for the stream. + /// An asynchronous stream of s as they arrive from the gateway. IAsyncEnumerable StreamEventsAsync(CancellationToken cancellationToken); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/MxAccessSeverityMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/MxAccessSeverityMapper.cs index 338b94fa..4c5b05f9 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/MxAccessSeverityMapper.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/MxAccessSeverityMapper.cs @@ -37,6 +37,7 @@ internal static class MxAccessSeverityMapper /// + OPC UA Part 9 numeric severity tuple. /// /// The raw MXAccess severity value (0-999 range, clamped if out of range). + /// The mapped four-bucket and its OPC UA Part 9 numeric severity. public static (AlarmSeverity Bucket, int OpcUaSeverity) Map(int rawMxAccessSeverity) { if (rawMxAccessSeverity < 250) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/MxValueDecoder.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/MxValueDecoder.cs index 93f60aad..1f861c97 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/MxValueDecoder.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/MxValueDecoder.cs @@ -15,6 +15,7 @@ internal static class MxValueDecoder { /// Decodes a gateway MxValue into a boxed CLR object. /// The MxValue to decode, or null. + /// The decoded boxed CLR value, or null when is null or carries a null MxValue. public static object? Decode(MxValue? value) { if (value is null) return null; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/ReconnectSupervisor.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/ReconnectSupervisor.cs index 6e3fbd90..15022173 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/ReconnectSupervisor.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/ReconnectSupervisor.cs @@ -25,7 +25,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; /// /// Replay: caller-supplied callback that re-establishes every active /// subscription. Production wraps gw's ReplaySubscriptionsCommand -/// (mxaccessgw issue #0.3); when that's not available, the callback falls +/// (tracked in the mxaccessgw backlog); when that's not available, the callback falls /// back to walking the SubscriptionRegistry and re-issuing SubscribeBulk for /// every tracked tag. /// @@ -147,6 +147,7 @@ public sealed class ReconnectSupervisor : IDisposable /// and for orchestration that wants to gate calls on recovery completing. /// /// Token to cancel the wait operation. + /// A task that represents the asynchronous operation. public async Task WaitForHealthyAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested && IsDegraded) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/SubscriptionRegistry.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/SubscriptionRegistry.cs index ff664d9a..b2fd411a 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/SubscriptionRegistry.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/SubscriptionRegistry.cs @@ -19,10 +19,6 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; internal sealed class SubscriptionRegistry { private readonly ConcurrentDictionary _bySubscriptionId = new(); - // Driver.Galaxy-012: use ImmutableHashSet for the reverse map so removal is - // O(log n) instead of "rebuild the entire ConcurrentBag from a LINQ filter on every - // unsubscribe"; reads are lock-free because the immutable snapshot is published - // atomically via ConcurrentDictionary AddOrUpdate. private readonly ConcurrentDictionary> _subscribersByItemHandle = new(); // Forward index for the Galaxy writer: fullRef (case-insensitive) → live item handle. // Maintained in lock-step with _subscribersByItemHandle; entries are cleaned up when @@ -38,6 +34,7 @@ internal sealed class SubscriptionRegistry public int TrackedItemHandleCount => _subscribersByItemHandle.Count; /// Allocate a fresh subscription id. Monotonic; unique per registry lifetime. + /// The newly allocated subscription id. public long NextSubscriptionId() => Interlocked.Increment(ref _nextSubscriptionId); /// @@ -75,8 +72,6 @@ internal sealed class SubscriptionRegistry foreach (var binding in entry.Bindings) { if (binding.ItemHandle <= 0) continue; - // Driver.Galaxy-012: ImmutableHashSet.Remove is O(log n) and the result is - // published atomically — no need to rebuild from a LINQ filter. if (!_subscribersByItemHandle.TryGetValue(binding.ItemHandle, out var set)) continue; var remaining = set.Remove(subscriptionId); if (remaining.IsEmpty) @@ -97,12 +92,6 @@ internal sealed class SubscriptionRegistry /// Look up the (subscription id, full reference) pairs that should receive an /// OnDataChange for the given gw item handle. Returns empty when nobody subscribes. /// - /// - /// Driver.Galaxy-012: O(1) per subscriber via the per-entry - /// FullRefByItemHandle index, rather than a FirstOrDefault linear - /// scan of the binding list. At 50k tags / 1Hz this turns each dispatch from a - /// 50k-element scan into a single dictionary lookup. - /// /// The gateway item handle. /// A list of subscription and reference pairs for the item handle. public IReadOnlyList<(long SubscriptionId, string FullReference)> ResolveSubscribers(int itemHandle) @@ -153,6 +142,7 @@ internal sealed class SubscriptionRegistry } /// Snapshot every active binding for diagnostic output. + /// A flattened list of every tag binding across all tracked subscriptions. public IReadOnlyList SnapshotAllBindings() => [.. _bySubscriptionId.Values.SelectMany(entry => entry.Bindings)]; @@ -161,6 +151,7 @@ internal sealed class SubscriptionRegistry /// Used by the reconnect replay path so it can re-issue SubscribeBulk per subscription /// and then each one with the post-reconnect item handles. /// + /// Every tracked subscription id paired with its current tag bindings. public IReadOnlyList<(long SubscriptionId, IReadOnlyList Bindings)> SnapshotEntries() => [.. _bySubscriptionId.Values.Select(entry => (entry.SubscriptionId, entry.Bindings))]; @@ -178,7 +169,6 @@ internal sealed class SubscriptionRegistry // Drop this subscription from every reverse-map set it currently appears in. The // pre-reconnect item handles are stale once the gw re-issues fresh ones. - // Driver.Galaxy-012: ImmutableHashSet.Remove is O(log n) — no LINQ rebuild. foreach (var binding in oldEntry.Bindings) { if (binding.ItemHandle <= 0) continue; @@ -210,8 +200,8 @@ internal sealed class SubscriptionRegistry /// /// Per-subscription bookkeeping. is an index /// over keyed by item handle so ResolveSubscribers - /// is O(1) per subscriber instead of a linear scan of every binding - /// (Driver.Galaxy-012). Failed bindings (item handle ≤ 0) are excluded from the + /// is O(1) per subscriber instead of a linear scan of every binding. + /// Failed bindings (item handle ≤ 0) are excluded from the /// index because the EventPump only dispatches for positive handles. /// private sealed class SubscriptionEntry diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/TracedGalaxyDataWriter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/TracedGalaxyDataWriter.cs index 37d745ef..40365263 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/TracedGalaxyDataWriter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/TracedGalaxyDataWriter.cs @@ -11,13 +11,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; internal sealed class TracedGalaxyDataWriter(IGalaxyDataWriter inner, string clientName) : IGalaxyDataWriter { /// - /// No span — this is a local cache-clear operation, not a gateway round-trip. public void InvalidateHandleCaches() => inner.InvalidateHandleCaches(); - /// Writes data to Galaxy while recording telemetry span. - /// The list of write requests to process. - /// Function to resolve security classification for tag references. - /// Cancellation token for the operation. + /// public async Task> WriteAsync( IReadOnlyList writes, Func securityResolver, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/TracedGalaxySubscriber.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/TracedGalaxySubscriber.cs index 55717860..dac430f5 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/TracedGalaxySubscriber.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/Runtime/TracedGalaxySubscriber.cs @@ -10,10 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime; /// internal sealed class TracedGalaxySubscriber(IGalaxySubscriber inner, string clientName) : IGalaxySubscriber { - /// Subscribes to multiple Galaxy tags in bulk with tracing. - /// The full tag references to subscribe to. - /// The buffered update interval in milliseconds. - /// The cancellation token. + /// public async Task> SubscribeBulkAsync( IReadOnlyList fullReferences, int bufferedUpdateIntervalMs, CancellationToken cancellationToken) { @@ -35,9 +32,7 @@ internal sealed class TracedGalaxySubscriber(IGalaxySubscriber inner, string cli } } - /// Unsubscribes from multiple Galaxy tags in bulk with tracing. - /// The item handles to unsubscribe from. - /// The cancellation token. + /// public async Task UnsubscribeBulkAsync(IReadOnlyList itemHandles, CancellationToken cancellationToken) { using var activity = GalaxyTelemetry.ActivitySource.StartActivity("galaxy.unsubscribe_bulk"); @@ -54,12 +49,10 @@ internal sealed class TracedGalaxySubscriber(IGalaxySubscriber inner, string cli } } - /// - /// Streaming RPC — one parent span covers the entire stream lifetime. Per-event - /// spans would dominate the trace volume at 50k tags / 1Hz; ops gets per-event - /// visibility through 's metrics in PR 6.2 instead. - /// - /// The cancellation token. + // Streaming RPC — one parent span covers the entire stream lifetime. Per-event + // spans would dominate the trace volume at 50k tags / 1Hz; ops gets per-event + // visibility through EventPump's metrics instead. + /// public async IAsyncEnumerable StreamEventsAsync( [EnumeratorCancellation] CancellationToken cancellationToken) { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianDataSource.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianDataSource.cs index 0eccf516..60db2223 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianDataSource.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/GatewayHistorianDataSource.cs @@ -133,18 +133,6 @@ public sealed class GatewayHistorianDataSource : IHistorianDataSource, IAsyncDis } /// - /// - /// Depends on the target gateway running with RuntimeDb:EventReadsEnabled=true (the - /// SQL alarm-history path). The is passed through to the - /// gateway, but its SQL ReadEvents source filter may not be present yet — so this - /// adapter also filters the mapped events by - /// client-side (defensive; remove once the server filter is confirmed). The - /// cap is enforced client-side by early stream termination: - /// a non-positive value applies no client cap (the gateway may still apply its - /// EventReadMaxRows); a positive cap stops at N and sets a non-null - /// iff at least one further matching - /// event existed (the Core.Abstractions-009 truncation signal). - /// public async Task ReadEventsAsync( string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken cancellationToken) @@ -169,7 +157,7 @@ public sealed class GatewayHistorianDataSource : IHistorianDataSource, IAsyncDis } // One more matching event arriving once the cap is full means the result is - // truncated — stop draining and flag it (Core.Abstractions-009). + // truncated — stop draining and flag it. if (hasCap && collected.Count == maxEvents) { truncated = true; @@ -180,7 +168,7 @@ public sealed class GatewayHistorianDataSource : IHistorianDataSource, IAsyncDis } RecordOutcome(success: true, error: null); - // A non-null, opaque token signals truncation to the caller (Core.Abstractions-009). + // A non-null, opaque token signals truncation to the caller. // The gateway has no resumable cursor, so the token's contents carry no paging state — // its presence alone is the "more events exist" signal. A fresh array per call keeps it // from being shared/mutated. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs index f69426e8..251b0fc6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/HistorianGatewayClientAdapter.cs @@ -73,14 +73,6 @@ public sealed class HistorianGatewayClientAdapter : IHistorianGatewayClient, IDi _inner.ReadAtTimeAsync(tag, timestampsUtc, ct); /// - /// - /// is rendered into the gateway's one server-filterable predicate — - /// a Source_Object filter the SQL ReadEvents - /// path binds as WHERE Source_Object = @source. A null source passes a null filter - /// (full window). is intentionally ignored here: the gateway wire - /// contract carries no per-call cap, so the cap is enforced upstream by - /// via early stream termination. - /// public IAsyncEnumerable ReadEventsAsync( string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken ct) { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/IHistorianGatewayClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/IHistorianGatewayClient.cs index e11bad58..eef687c0 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/IHistorianGatewayClient.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/IHistorianGatewayClient.cs @@ -12,6 +12,12 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway; public interface IHistorianGatewayClient : IAsyncDisposable { /// Streams raw historian samples for a tag over a time window. + /// The historian tag name to read. + /// Inclusive start of the requested time window (UTC). + /// Exclusive end of the requested time window (UTC). + /// The maximum number of raw samples to return. + /// Cancellation token. + /// An async stream of raw historian samples for the tag. IAsyncEnumerable ReadRawAsync( string tag, DateTime startUtc, @@ -20,6 +26,13 @@ public interface IHistorianGatewayClient : IAsyncDisposable CancellationToken ct); /// Streams aggregate samples for a tag using the given retrieval mode and interval. + /// The historian tag name to read. + /// Inclusive start of the requested time window (UTC). + /// Exclusive end of the requested time window (UTC). + /// The aggregate retrieval mode to apply. + /// The aggregation bucket interval. + /// Cancellation token. + /// An async stream of aggregate samples for the tag. IAsyncEnumerable ReadAggregateAsync( string tag, DateTime startUtc, @@ -29,6 +42,10 @@ public interface IHistorianGatewayClient : IAsyncDisposable CancellationToken ct); /// Reads the samples nearest to each of the requested timestamps (unary). + /// The historian tag name to read. + /// The timestamps (UTC) to find the nearest sample for. + /// Cancellation token. + /// The samples nearest to each requested timestamp. Task> ReadAtTimeAsync( string tag, IReadOnlyList timestampsUtc, @@ -45,6 +62,7 @@ public interface IHistorianGatewayClient : IAsyncDisposable /// RuntimeDb:EventReadMaxRows); a positive value stops draining after that many events. /// /// Cancellation token. + /// An async stream of historian events within the window. IAsyncEnumerable ReadEventsAsync( string? sourceName, DateTime startUtc, @@ -53,22 +71,36 @@ public interface IHistorianGatewayClient : IAsyncDisposable CancellationToken ct); /// Writes live values for a tag through the gateway's SQL live-write path. + /// The historian tag name to write values for. + /// The live values to write. + /// Cancellation token. + /// The write acknowledgement returned by the gateway. Task WriteLiveValuesAsync( string tag, IReadOnlyList values, CancellationToken ct); /// Sends a single historian event. + /// The historian event to send. + /// Cancellation token. + /// The write acknowledgement returned by the gateway. Task SendEventAsync(HistorianEvent evt, CancellationToken ct); /// Ensures the supplied tag definitions exist (create-or-update). + /// The tag definitions to create or update. + /// Cancellation token. + /// Per-tag outcomes for the create-or-update operation. Task EnsureTagsAsync( IReadOnlyList definitions, CancellationToken ct); /// Probes gateway/historian reachability. + /// Cancellation token. + /// true when the gateway and historian are reachable; otherwise false. Task ProbeAsync(CancellationToken ct); /// Reads the gateway's current historian connection status. + /// Cancellation token. + /// The current connection status reported by the gateway. Task GetConnectionStatusAsync(CancellationToken ct); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs index 97631961..57b7b76d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Mapping/HistorianTypeMapper.cs @@ -47,6 +47,7 @@ internal static class HistorianTypeMapper /// provisioning hook skip deferred types without catching . /// /// The driver-agnostic data type. + /// True if the type is one of the nine historizable numeric types; otherwise false. public static bool IsHistorizable(DriverDataType dataType) => dataType switch { DriverDataType.Boolean diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Recorder/GatewayHistorianValueWriter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Recorder/GatewayHistorianValueWriter.cs index 3fa9106a..ac3ccacb 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Recorder/GatewayHistorianValueWriter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Recorder/GatewayHistorianValueWriter.cs @@ -91,5 +91,6 @@ public sealed class GatewayHistorianValueWriter : IHistorianValueWriter, IAsyncD /// Disposes the underlying gateway client (and its gRPC channel). The DI container owns this /// writer as a singleton, so this fires once at host shutdown — closing the channel gracefully. /// + /// A task that represents the asynchronous operation. public ValueTask DisposeAsync() => _client.DisposeAsync(); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Recorder/HistorizationOutboxEntrySerializer.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Recorder/HistorizationOutboxEntrySerializer.cs index 108909b4..f02e37dd 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Recorder/HistorizationOutboxEntrySerializer.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway/Recorder/HistorizationOutboxEntrySerializer.cs @@ -18,6 +18,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder; internal static class HistorizationOutboxEntrySerializer { /// Serializes to a fixed-layout little-endian byte array. + /// The outbox entry to serialize. + /// The fixed-layout little-endian byte array. public static byte[] Serialize(HistorizationOutboxEntry entry) { ArgumentNullException.ThrowIfNull(entry); @@ -41,6 +43,8 @@ internal static class HistorizationOutboxEntrySerializer } /// Reconstructs a from its serialized bytes. + /// The fixed-layout little-endian bytes produced by . + /// The reconstructed outbox entry. public static HistorizationOutboxEntry Deserialize(ReadOnlySpan span) { var id = new Guid(span[..16]); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/DirectLogicAddress.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/DirectLogicAddress.cs index 7bdc8e25..2bf1d261 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/DirectLogicAddress.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/DirectLogicAddress.cs @@ -27,6 +27,7 @@ public static class DirectLogicAddress /// V-address that may fall in either bank, call , which routes /// system-bank addresses through instead. /// + /// The 0-based Modbus PDU address for the given user V-memory address. /// Input is null / empty / contains non-octal digits (8,9). /// Parsed value exceeds ushort.MaxValue (0xFFFF). public static ushort UserVMemoryToPdu(string vAddress) @@ -80,11 +81,16 @@ public static class DirectLogicAddress /// Octal 40400 == decimal 16640 (0x4100). public const ushort SystemVMemoryOctalBase = 0x4100; // octal 40400 decoded + /// + /// Convert a 0-based offset within the system V-memory bank to its absolute Modbus PDU + /// address, relocated to the fixed base. + /// /// /// 0-based register offset within the system bank. Pass 0 for V40400 itself; pass 1 for /// V40401 (octal), and so on. NOT an octal-decoded value — the system bank lives at /// consecutive PDU addresses, so the offset is plain decimal. /// + /// The 0-based Modbus PDU address for the given system-bank offset. public static ushort SystemVMemoryToPdu(ushort offsetWithinSystemBank) { var pdu = SystemVMemoryBasePdu + offsetWithinSystemBank; @@ -107,6 +113,7 @@ public static class DirectLogicAddress /// octal-decoded value itself: V40400 → PDU 0x2100, V40401 → 0x2101, and so on. /// See docs/v2/dl205.md §V-Memory Addressing. /// + /// The 0-based Modbus PDU address for the given V-memory address. /// Input is null / empty / contains non-octal digits. /// The result exceeds the 16-bit Modbus PDU range. public static ushort VMemoryToPdu(string vAddress) @@ -116,11 +123,11 @@ public static class DirectLogicAddress return (ushort)octalValue; // System bank: the registers are contiguous from V40400, so the offset within the bank - // is the plain decimal distance from the octal base, not another octal decode. - // Driver.Modbus.Addressing-011: the subtraction result is provably <= 0xBEFF because - // DecodeOctalVAddress already caps octalValue at 0xFFFF, so no overflow guard is needed - // here. The real overflow guard (pdu > ushort.MaxValue) lives in SystemVMemoryToPdu and - // is reachable only when that helper is called directly with a large explicit offset. + // is the plain decimal distance from the octal base, not another octal decode. The + // subtraction result is provably <= 0xBEFF because DecodeOctalVAddress already caps + // octalValue at 0xFFFF, so no overflow guard is needed here. The real overflow guard + // (pdu > ushort.MaxValue) lives in SystemVMemoryToPdu and is reachable only when that + // helper is called directly with a large explicit offset. var offsetWithinBank = (ushort)(octalValue - SystemVMemoryOctalBase); return SystemVMemoryToPdu(offsetWithinBank); } @@ -156,6 +163,7 @@ public static class DirectLogicAddress /// ladder-logic editor's notation. /// /// The Y-output address (octal, with optional Y prefix). + /// The 0-based Modbus coil address. public static ushort YOutputToCoil(string yAddress) => AddOctalOffset(YOutputBaseCoil, StripPrefix(yAddress, 'Y')); @@ -164,6 +172,7 @@ public static class DirectLogicAddress /// 0-based Modbus coil address. /// /// The C-relay address (octal, with optional C prefix). + /// The 0-based Modbus coil address. public static ushort CRelayToCoil(string cAddress) => AddOctalOffset(CRelayBaseCoil, StripPrefix(cAddress, 'C')); @@ -173,6 +182,7 @@ public static class DirectLogicAddress /// exception — the CPU sizes the table to configured I/O, not installed modules. /// /// The X-input address (octal, with optional X prefix). + /// The 0-based Modbus discrete-input address. public static ushort XInputToDiscrete(string xAddress) => AddOctalOffset(XInputBaseDiscrete, StripPrefix(xAddress, 'X')); @@ -181,6 +191,7 @@ public static class DirectLogicAddress /// Modbus discrete-input address. Accepts "SP" prefix case-insensitively. /// /// The SP special-relay address (octal, with optional SP prefix). + /// The 0-based Modbus discrete-input address. public static ushort SpecialToDiscrete(string spAddress) { if (string.IsNullOrWhiteSpace(spAddress)) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/MelsecAddress.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/MelsecAddress.cs index d3f272aa..b98ad45e 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/MelsecAddress.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/MelsecAddress.cs @@ -53,6 +53,7 @@ public static class MelsecAddress /// 0-based Modbus DI address the assignment-block has configured X0 to land at. /// Typical default on QJ71MT91 sample projects: 0. Pass the site-specific value. /// + /// The 0-based Modbus discrete-input address. public static ushort XInputToDiscrete(string xAddress, MelsecFamily family, ushort xBankBase = 0) => AddFamilyOffset(xBankBase, StripPrefix(xAddress, 'X'), family); @@ -63,6 +64,7 @@ public static class MelsecAddress /// MELSEC Y address. Y prefix optional, case-insensitive. /// The PLC family — determines whether the trailing digits are hex or octal. /// 0-based Modbus coil address the assignment-block has configured Y0 to land at. + /// The 0-based Modbus coil address. public static ushort YOutputToCoil(string yAddress, MelsecFamily family, ushort yBankBase = 0) => AddFamilyOffset(yBankBase, StripPrefix(yAddress, 'Y'), family); @@ -73,6 +75,7 @@ public static class MelsecAddress /// /// MELSEC M address. M prefix optional, case-insensitive. /// 0-based Modbus coil address the assignment-block has configured M0 to land at. + /// The 0-based Modbus coil address. public static ushort MRelayToCoil(string mAddress, ushort mBankBase = 0) { var digits = StripPrefix(mAddress, 'M'); @@ -93,6 +96,7 @@ public static class MelsecAddress /// /// MELSEC D address. D prefix optional, case-insensitive. /// 0-based Modbus holding register address the assignment-block has configured D0 to land at. + /// The 0-based Modbus holding register address. public static ushort DRegisterToHolding(string dAddress, ushort dBankBase = 0) { var digits = StripPrefix(dAddress, 'D'); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusAddressParser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusAddressParser.cs index 7c3aaf2e..bb6f7a54 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusAddressParser.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusAddressParser.cs @@ -15,7 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// spreadsheets from any of those tools without per-tag manual translation. /// /// -/// Examples (post-#146 type codes — verified against Wonderware DASMBTCP + Ignition): +/// Examples (type codes — verified against Wonderware DASMBTCP + Ignition): /// /// 40001 — HoldingRegisters[0], Int16 (default). /// 400001 — HoldingRegisters[0], Int16 (6-digit form). @@ -30,7 +30,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// /// /// -/// Grammar scope — out of band (Driver.Modbus.Addressing-007): per-string byte +/// Grammar scope — out of band: per-string byte /// order () is NOT expressible through this grammar. /// The DL205 low-byte-first string-packing knob is configurable only via the structured /// tag form (the driver's ModbusTagDefinition.StringByteOrder field). The 3rd @@ -46,7 +46,7 @@ public static class ModbusAddressParser /// The parsed Modbus address. public static ParsedModbusAddress Parse(string address) => Parse(address, ModbusFamily.Generic, MelsecFamily.Q_L_iQR); - /// Parse with a family hint (#144 family-native branch). + /// Parse with a family hint (family-native branch). /// The address string to parse. /// The Modbus family hint. /// The MELSEC subfamily hint. @@ -113,7 +113,7 @@ public static class ModbusAddressParser // else surfaces a clear error in whichever slot it lands. if (parts.Length == 3) { - // Driver.Modbus.Addressing-002: reject an empty 3rd field (e.g. "40001:F:") rather + // Reject an empty 3rd field (e.g. "40001:F:") rather // than silently dropping it. Enumerable.All returns true for an empty sequence, so // without this guard the empty string would be classified as a valid array count and // then quietly ignored, leaving the user with no diagnostic for a typo'd trailing colon. @@ -126,7 +126,7 @@ public static class ModbusAddressParser else if (parts[2].All(char.IsDigit)) countPart = parts[2]; else { - // Driver.Modbus.Addressing-003: when TryParseByteOrder would fail on a 4-letter + // When TryParseByteOrder would fail on a 4-letter // token that looks like a type code (e.g. BOOL), improve the diagnostic so the // user knows field 3 is a byte order and field 2 is the type. var mightBeTypeCode = parts[2].Length == 4 && parts[2].All(char.IsLetterOrDigit); @@ -223,7 +223,7 @@ public static class ModbusAddressParser } // Optional bit suffix: '.N' at the end, N in 0..15. Strip before parsing region/offset. - // Driver.Modbus.Addressing-004: use LastIndexOf so a multi-dot input like "40001.5.3" + // Use LastIndexOf so a multi-dot input like "40001.5.3" // produces a descriptive error ("bit index '5.3' must be 0..15") rather than silently // parsing "5" as the bit and leaving ".3" as part of the address text. Also validate // the address segment is non-empty (a leading dot like ".5" is not a valid Modbus addr). @@ -252,7 +252,7 @@ public static class ModbusAddressParser bit = bitVal; } - // Family-native branch (#144) — when a non-Generic family is configured, try its native + // Family-native branch — when a non-Generic family is configured, try its native // syntax first. Successful native parse wins; failure falls through to Modicon / mnemonic. // The order matters for cross-family ambiguity: DL205 'C100' is a control relay, not a // Modicon coil, when the user has explicitly selected DL205. @@ -271,11 +271,11 @@ public static class ModbusAddressParser if (TryParseMnemonicAddress(addrText, out region, out offset, out error)) return true; - // Fall back to Modicon (5/6-digit). Reuses #136's parser. + // Fall back to Modicon (5/6-digit). Reuses the shared Modicon parser. if (ModbusModiconAddress.TryParse(addrText, out region, out offset, out error)) return true; - // Driver.Modbus.Addressing-005: when a non-Generic family was configured and the + // When a non-Generic family was configured and the // family-native parser set a specific error (meaning the address matched a recognised // family prefix but the value was invalid, e.g. "contains non-octal digit"), prefer // that error over the generic Modicon fallback diagnostic, which otherwise says @@ -371,7 +371,7 @@ public static class ModbusAddressParser } catch (Exception ex) { - // Driver.Modbus.Addressing-006: a try-parse method must never throw, so any helper + // A try-parse method must never throw, so any helper // exception is converted to a structured error. The current helpers throw only // ArgumentException (incl. ArgumentOutOfRangeException) and OverflowException, but // catching narrowly would silently break the TryParse contract if a helper ever @@ -447,17 +447,17 @@ public static class ModbusAddressParser return true; } - // #146 — codes aligned with Wonderware DASMBTCP + Ignition Modbus driver after the + // Codes aligned with Wonderware DASMBTCP + Ignition Modbus driver after the // 2026-04-25 vendor-doc verification: // - `:I` is Int32 (Wonderware: "letter 'I' follow ... 32-bit signed quantity, two - // consecutive registers"). Ignition's HRI is also Int32. The pre-#146 mapping + // consecutive registers"). Ignition's HRI is also Int32. The previous mapping // `:I` = Int16 silently produced wrong-typed data + offset-shifted neighbours when // a tag spreadsheet was pasted from another vendor. // - `:S` is the explicit Int16 code (Wonderware: "letter 'S' ... 16-bit signed"). // - `:US` is UInt16 (Ignition: HRUS = "Unsigned Short"). // - `:UI` is UInt32 (parallel to `:I` shape; matches Ignition HRUI). // - `:I_64` / `:UI_64` for 64-bit (Ignition HRI_64 / HRUI_64 underscore-N convention). - // - `:BCD_32` for 32-bit BCD (Ignition HRBCD_32). The pre-#146 `:LBCD` is dropped. + // - `:BCD_32` for 32-bit BCD (Ignition HRBCD_32). The previous `:LBCD` is dropped. // - HR/IR with no explicit type still default to Int16 (matches Ignition `HR`). type = text.ToUpperInvariant() switch { @@ -503,13 +503,12 @@ public static class ModbusAddressParser if ((int)order == -1) { - // Driver.Modbus.Addressing-003: if the unknown token is a VALID type code in this - // parser, direct the user to put it in field 2 instead. Only BOOL (4-letter) and - // STR are valid type codes that could appear here. "REAL", "DINT", "UINT" look - // like type names from other tools (STEP 7 / RSLogix) but are NOT valid in this - // parser's type table (Driver.Modbus.Addressing-010) — listing them here would give - // advice that leads to a second error ("Unknown type code 'REAL'"). var upper = text.ToUpperInvariant(); + // If the unknown token is a VALID type code in this parser, direct the user to put + // it in field 2 instead. Only BOOL (4-letter) and STR are valid type codes that + // could appear here. "REAL", "DINT", "UINT" look like type names from other tools + // (STEP 7 / RSLogix) but are NOT valid in this parser's type table — listing them + // here would give advice that leads to a second error ("Unknown type code 'REAL'"). var isActualTypeCode = upper is "BOOL" || (text.Length > 3 && text.StartsWith("STR", StringComparison.OrdinalIgnoreCase) && text[3..].All(char.IsDigit)); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusDataType.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusDataType.cs index 5e14dd9a..7edce59a 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusDataType.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusDataType.cs @@ -90,7 +90,7 @@ public enum ModbusByteOrder /// /// /// -/// Grammar scope (Driver.Modbus.Addressing-007): this enum is intentionally NOT +/// Grammar scope: this enum is intentionally NOT /// expressible through the grammar string. The grammar /// has no token form for it, and has no field for it — /// a DL205 string tag parsed from the grammar always carries the driver's default order. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusFamily.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusFamily.cs index 5e75bdec..c811439d 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusFamily.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusFamily.cs @@ -1,7 +1,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// -/// PLC family selector that drives the parser's family-native branch (#144). When the +/// PLC family selector that drives the parser's family-native branch. When the /// driver is configured for a specific family, address strings using that family's native /// notation (DirectLOGIC V2000 octal, MELSEC X20 hex/octal, etc.) are /// translated to + PDU offset directly — without forcing @@ -10,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// /// /// When set to (the default), the parser only accepts Modicon and -/// mnemonic forms — preserves pre-#144 behaviour exactly. Setting a non-Generic family +/// mnemonic forms — preserves the previous behaviour exactly. Setting a non-Generic family /// is the only way to enable family-native parsing. /// /// diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusModiconAddress.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusModiconAddress.cs index 942e7a36..9fa4cb25 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusModiconAddress.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Addressing/ModbusModiconAddress.cs @@ -45,6 +45,7 @@ public static class ModbusModiconAddress /// The parsed Modbus region, or default if parsing fails. /// The zero-based PDU offset, or 0 if parsing fails. /// The error message if parsing fails, or null on success. + /// true if the address parsed successfully; otherwise false. public static bool TryParse(string? address, out ModbusRegion region, out ushort offset, out string? error) { region = default; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusDriverOptions.cs index 150d9c51..04e8c917 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusDriverOptions.cs @@ -84,36 +84,35 @@ public sealed class ModbusDriverOptions /// Reserved / no-op kill-switch for FC23 (Read/Write Multiple Registers). The /// driver does not currently emit FC23 — toggling this option has no observable effect /// today. The slot exists so a future block-read-coalescing enhancement that opts into - /// FC23 can be disabled per-deployment without a code change. Track Driver.Modbus-007 - /// for the wiring follow-up. Default false. + /// FC23 can be disabled per-deployment without a code change. Default false. /// public bool DisableFC23 { get; init; } = false; /// - /// #151 — interval for the background re-probe loop that retries auto-prohibited - /// coalesced ranges (#148). When non-null, every AutoProhibitReprobeInterval + /// Interval for the background re-probe loop that retries auto-prohibited + /// coalesced ranges. When non-null, every AutoProhibitReprobeInterval /// the driver attempts each prohibition's coalesced read once. If the re-probe /// succeeds, the prohibition clears and the planner resumes coalescing across the /// range on the next scan. Default null = re-probe disabled (prohibitions - /// persist until ReinitializeAsync; preserves pre-#151 behaviour). + /// persist until ReinitializeAsync; preserves the previous behaviour). /// public TimeSpan? AutoProhibitReprobeInterval { get; init; } = null; /// - /// Block-read coalescing budget (#143). When non-zero, the read planner combines tags + /// Block-read coalescing budget. When non-zero, the read planner combines tags /// in the same (UnitId, Region) group whose addresses are at most this many registers /// apart into a single FC03/FC04/FC01/FC02 read. The sliced response is then dispatched /// back to per-tag values. Default 0 = no coalescing — every tag gets its own - /// PDU (preserves pre-#143 behaviour). Typical opt-in values are 5..32 — large enough + /// PDU (preserves the previous behaviour). Typical opt-in values are 5..32 — large enough /// to bridge a few unused registers, small enough to avoid trampling protected holes. /// public ushort MaxReadGap { get; init; } = 0; /// - /// PLC family hint that drives the parser's family-native branch (#144). When set to a + /// PLC family hint that drives the parser's family-native branch. When set to a /// non-Generic value, address strings using that family's native syntax (DL205 V2000 / /// MELSEC D100) parse to the right region + offset directly. Defaults to - /// = Modicon-only behaviour preserved from #137. + /// = Modicon-only behaviour preserved by default. /// public ModbusFamily Family { get; init; } = ModbusFamily.Generic; @@ -134,7 +133,7 @@ public sealed class ModbusDriverOptions /// /// /// - /// Driver.Modbus-010 — write-only-tag caveat: the suppression cache is only + /// Write-only-tag caveat: the suppression cache is only /// invalidated by a read that returns a divergent value. A tag that is never /// subscribed or polled (write-only setpoints, command registers) never sees its /// cache entry refreshed — so a value the operator believes was re-asserted is @@ -176,7 +175,7 @@ public sealed class ModbusDriverOptions /// /// Reconnect backoff settings used by the auto-reconnect path. Default is no backoff - /// (immediate retry — preserves the historical pre-#139 behaviour). Set to a non-zero + /// (immediate retry — preserves the historical behaviour). Set to a non-zero /// to sleep before the first reconnect /// attempt; caps the geometric growth. /// @@ -253,8 +252,8 @@ public sealed class ModbusProbeOptions /// character in the low byte instead — see docs/v2/dl205.md §strings. /// /// -/// Per docs/v2/plan.md decisions #44, #45, #143 — flag a tag as safe to replay on -/// write timeout / failure. Default false; writes do not auto-retry. Safe candidates: +/// Flags a tag as safe to replay on write timeout / failure. Default false; writes +/// do not auto-retry. Safe candidates: /// holding-register set-points for analog values and configuration registers where the same /// value can be written again without side-effects. Unsafe: coils that drive edge-triggered /// actions (pulse outputs), counter-increment addresses on PLCs that treat writes as deltas, @@ -273,14 +272,14 @@ public sealed class ModbusProbeOptions /// array tags. Default null = no deadband (every change publishes). /// /// -/// Per-tag UnitId override for multi-slave gateway topology (#142). When non-null this +/// Per-tag UnitId override for multi-slave gateway topology. When non-null this /// UnitId is used in the MBAP header instead of the driver-level ModbusDriverOptions.UnitId. /// Defaults to null = use the driver-level value (preserves single-slave deployments). /// Tags with different UnitIds belong to different physical slaves and the read planner /// must NOT coalesce them across slaves — even at the same address. /// /// -/// Escape hatch for #143 block-read coalescing. When true, the planner reads this +/// Escape hatch for block-read coalescing. When true, the planner reads this /// tag in isolation regardless of ModbusDriverOptions.MaxReadGap. Use when the /// surrounding registers are write-only or fault on read (some Schneider Premium / Siemens /// PNs have protected holes). Default false. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusEquipmentTagParser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusEquipmentTagParser.cs index 373d13cb..88e3b40a 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusEquipmentTagParser.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus.Contracts/ModbusEquipmentTagParser.cs @@ -36,13 +36,12 @@ public static class ModbusEquipmentTagParser var stringLength = (ushort)ReadInt(root, "stringLength"); // Guard: String tags require StringLength >= 1. RegisterCount = (StringLength+1)/2, // so StringLength=0 → 0 registers → spec-illegal FC03/FC04 with quantity=0 → PLC - // returns exception 03. Reject here (analogous to Driver.Modbus-009 / ValidateStringLength + // returns exception 03. Reject here (analogous to ValidateStringLength // in the pre-declared tag path) so the driver surfaces BadNodeIdUnknown rather than a - // misleading BadCommunicationError. (Driver.Modbus.Contracts-002) + // misleading BadCommunicationError. if (dataType == ModbusDataType.String && stringLength < 1) return false; // Guard: BitInRegister tags require BitIndex 0–15. Shift by ≥ 16 on an int overflows // a ushort mask silently — reads always return false, writes have no effect. - // (Driver.Modbus.Contracts-001) if (dataType == ModbusDataType.BitInRegister && bitIndex > 15) return false; // isArray / arrayLength — optional keys authored by the typed Modbus tag editor. // Canonical rule: a tag is an array iff isArray:true AND arrayLength >= 1. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusAutoProhibition.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusAutoProhibition.cs index 1dc14888..3307e0ce 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusAutoProhibition.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusAutoProhibition.cs @@ -1,7 +1,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// -/// #152 — operator-visible snapshot of one auto-prohibited coalesced range. Returned in +/// Operator-visible snapshot of one auto-prohibited coalesced range. Returned in /// bulk by ; consumers (Admin UI, /// dashboards, log-aggregation pipelines) project the list into whatever shape they need. /// @@ -11,8 +11,8 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// Inclusive end of the prohibited range. Equals when bisection has narrowed to a single register. /// Wall-clock time of the most recent failure (record) or re-probe (refresh). /// -/// True when the range still spans > 1 register and the next re-probe will bisect it -/// (per #150). False when the range is single-register or has been pinned permanent. +/// True when the range still spans > 1 register and the next re-probe will bisect it. +/// False when the range is single-register or has been pinned permanent. /// public sealed record ModbusAutoProhibition( byte UnitId, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs index 4d8ad138..16fe1934 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriver.cs @@ -21,7 +21,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; public sealed class ModbusDriver : IDriver, ITagDiscovery, IReadable, IWritable, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable { - // ---- instance fields (Driver.Modbus-011: grouped at top for auditability) ---- + // ---- instance fields (grouped at top for auditability) ---- private readonly ModbusDriverOptions _options; private readonly Func _transportFactory; @@ -42,10 +42,6 @@ public sealed class ModbusDriver // Last-published value per tag, keyed by FullReference. Used by ShouldPublish to apply // the deadband filter. Stored as object so all numeric types share one map; the comparison // does a typed cast inside. - // Driver.Modbus-001: ShouldPublish runs on the PollGroupEngine onChange callback, which - // executes on one background Task per subscription — so a multi-subscription driver mutates - // this map concurrently from several threads. A plain Dictionary corrupts under concurrent - // writes; ConcurrentDictionary makes every TryGetValue / indexer write thread-safe. private readonly ConcurrentDictionary _lastPublishedByRef = new(StringComparer.OrdinalIgnoreCase); // Last-written value per tag for the WriteOnChangeOnly suppression. Invalidated by reads @@ -57,7 +53,6 @@ public sealed class ModbusDriver // per-register lock keeps concurrent bit-write callers from stomping on each other. private readonly ConcurrentDictionary _rmwLocks = new(); - // #148 auto-prohibited coalesce ranges + #150 bisection state (see ProhibitionState below). private readonly Dictionary<(byte Unit, ModbusRegion Region, ushort Start, ushort End), ProhibitionState> _autoProhibited = new(); private readonly object _autoProhibitedLock = new(); @@ -72,14 +67,6 @@ public sealed class ModbusDriver private CancellationTokenSource? _probeCts; private CancellationTokenSource? _reprobeCts; - // Driver.Modbus-003: every read / write / probe path writes to _health from a different - // thread, and GetHealth() reads it without coordination. Reference-assignment on .NET is - // atomic for sealed-record refs (so no tearing), but without a happens-before barrier a - // stale snapshot can persist on another core indefinitely. Volatile.Write / Volatile.Read - // give GetHealth() a defined ordering guarantee: any subsequent read sees at least the - // most recent write any thread has published. The field stays a plain reference (you can't - // mark a record-typed field 'volatile' through the C# keyword on every framework version, - // and the Volatile API is the documented portable form). private DriverHealth _health = new(DriverState.Unknown, null, null); /// Occurs when a subscribed tag value changes. @@ -90,7 +77,7 @@ public sealed class ModbusDriver // ---- nested types ---- /// - /// #150 — per-prohibition state. SplitPending drives the re-probe loop's + /// Per-prohibition state. SplitPending drives the re-probe loop's /// bisection: when true and the range spans > 1 register, the next re-probe /// tries the two halves separately to narrow the actual offending register(s). /// Single-register prohibitions can't be split further; they stay re-probed as-is. @@ -129,20 +116,12 @@ public sealed class ModbusDriver reader: ReadAsync, onChange: (handle, tagRef, snapshot) => { - // #141 deadband filter: when configured on a tag, suppress publishes whose - // numeric distance from the last-published value is below the threshold. if (!ShouldPublish(tagRef, snapshot)) return; OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)); }); } - /// - /// #142 multi-unit-ID gateway support: per-tag UnitId override drives per-slave host - /// name surfacing through this method. The resilience pipeline keys breakers on the - /// returned host string, so a dead RTU slave behind an Ethernet gateway opens its own - /// breaker without tripping siblings on the same TCP socket. - /// - /// Tag reference to resolve the host for. + /// public string ResolveHost(string fullReference) { if (_tagsByName.TryGetValue(fullReference, out var tag)) @@ -179,14 +158,12 @@ public sealed class ModbusDriver return true; } - /// Gets the unique identifier of this driver instance. + /// public string DriverInstanceId => _driverInstanceId; - /// Gets the driver type name. + /// public string DriverType => "Modbus"; - /// Initializes the driver with the specified configuration JSON. - /// JSON configuration string. - /// Cancellation token. + /// public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { WriteHealth(new DriverHealth(DriverState.Initializing, null, null)); @@ -206,7 +183,6 @@ public sealed class ModbusDriver _ = Task.Run(() => ProbeLoopAsync(_probeCts.Token), _probeCts.Token); } - // #151 — start the auto-prohibition re-probe loop when the operator opted in. if (_options.AutoProhibitReprobeInterval is not null) { _reprobeCts = new CancellationTokenSource(); @@ -220,17 +196,14 @@ public sealed class ModbusDriver } } - /// Reinitializes the driver with new configuration. - /// New JSON configuration string. - /// Cancellation token. + /// public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { await ShutdownAsync(cancellationToken); await InitializeAsync(driverConfigJson, cancellationToken); } - /// Shuts down the driver and releases resources. - /// Cancellation token. + /// public async Task ShutdownAsync(CancellationToken cancellationToken) { var lastRead = ReadHealth().LastSuccessfulRead; @@ -238,11 +211,11 @@ public sealed class ModbusDriver WriteHealth(new DriverHealth(DriverState.Unknown, lastRead, null)); } - /// Gets the current driver health status. + /// public DriverHealth GetHealth() => ReadHealth(); /// - /// Driver.Modbus-003: barrier-protected read of the multi-thread _health field. + /// Barrier-protected read of the multi-thread _health field. /// Volatile.Read guarantees GetHealth() and the in-driver self-reads (the /// Degraded paths that retain LastSuccessfulRead) observe the most recently /// published snapshot rather than a per-core cached stale copy. @@ -250,27 +223,20 @@ public sealed class ModbusDriver private DriverHealth ReadHealth() => Volatile.Read(ref _health); /// - /// Driver.Modbus-003: barrier-protected publish of a new _health snapshot. + /// Barrier-protected publish of a new _health snapshot. /// private void WriteHealth(DriverHealth value) => Volatile.Write(ref _health, value); - /// Gets the memory footprint of the driver. + /// public long GetMemoryFootprint() => 0; - /// Flushes optional caches to free memory. - /// Cancellation token. + /// public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; // ---- ITagDiscovery ---- - /// - /// Run-once: emits the complete node set synchronously from - /// the configured tag table in a single pass — nothing fills in asynchronously after - /// connect, so a single discovery pass is sufficient. - /// + /// public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; - /// Discovers tags and builds the OPC UA address space. - /// Address space builder. - /// Cancellation token. + /// public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(builder); @@ -292,9 +258,7 @@ public sealed class ModbusDriver // ---- IReadable ---- - /// Reads the specified tag references from the Modbus device. - /// Tag references to read. - /// Cancellation token. + /// public async Task> ReadAsync( IReadOnlyList fullReferences, CancellationToken cancellationToken) { @@ -302,9 +266,6 @@ public sealed class ModbusDriver var now = DateTime.UtcNow; var results = new DataValueSnapshot[fullReferences.Count]; - // #143 block-read coalescing: when MaxReadGap is non-zero, route eligible tags through - // the coalescing planner first. Tags it can't coalesce (arrays, coils, prohibited, - // unknown) fall through to the per-tag loop below with results[i] still default. var coalesced = _options.MaxReadGap > 0 ? await ReadCoalescedAsync(transport, fullReferences, results, now, cancellationToken).ConfigureAwait(false) : new HashSet(); @@ -405,8 +366,6 @@ public sealed class ModbusDriver /// private static object DecodeBitArray(ReadOnlySpan bitmap, int count, bool isArray) { - // Driver.Modbus-005: guard against empty bitmap (already validated upstream but defensive - // here so the IndexOutOfRangeException path is explicitly closed at decode time too). if (bitmap.IsEmpty) throw new InvalidDataException("Modbus bit response produced an empty bitmap — cannot decode coil value"); if (!isArray) return (bitmap[0] & 0x01) == 1; @@ -511,7 +470,7 @@ public sealed class ModbusDriver } } - /// Resolve the UnitId for a tag — per-tag override (#142) or driver-level fallback. + /// Resolve the UnitId for a tag — per-tag override or driver-level fallback. private byte ResolveUnitId(ModbusTagDefinition tag) => tag.UnitId ?? _options.UnitId; private bool RangeIsAutoProhibited(byte unit, ModbusRegion region, ushort start, ushort end) @@ -545,10 +504,6 @@ public sealed class ModbusDriver }; } - // #152 — structured warning so log-aggregation systems can alert on the event. - // First-time prohibitions get logged; re-fires of the same range stay quiet to avoid - // flooding when a per-tick exception keeps the same range bad. The state visible via - // GetAutoProhibitedRanges shows operators the long-tail picture. if (isNew) _logger.LogWarning( "Modbus coalesced read failed; auto-prohibited range recorded. Driver={DriverInstanceId} Unit={Unit} Region={Region} Start={Start} End={End} Span={Span}", @@ -556,7 +511,7 @@ public sealed class ModbusDriver } /// - /// #153 — info log when a re-probe clears a prohibition. Operators see recovery + /// Info log when a re-probe clears a prohibition. Operators see recovery /// events without having to poll . /// private void LogProhibitionCleared(byte unit, ModbusRegion region, ushort start, ushort end) => @@ -565,12 +520,13 @@ public sealed class ModbusDriver _driverInstanceId, unit, region, start, end); /// - /// #152 — operator-visible snapshot of every range the planner has learned to read + /// Operator-visible snapshot of every range the planner has learned to read /// individually. Exposed through the driver-diagnostics surface; consumers (Admin UI, /// log-aggregation, dashboards) call this to show what's been auto-isolated. Populated - /// on coalesced-read failure (#148), narrowed by bisection (#150), cleared by the - /// re-probe loop (#151) when ranges become healthy again. + /// on coalesced-read failure, narrowed by bisection, cleared by the + /// re-probe loop when ranges become healthy again. /// + /// The current set of auto-prohibited register ranges. public IReadOnlyList GetAutoProhibitedRanges() { lock (_autoProhibitedLock) @@ -588,7 +544,7 @@ public sealed class ModbusDriver } /// - /// #151 — periodic re-probe loop, augmented in #150 with bisection-style narrowing. + /// Periodic re-probe loop, augmented with bisection-style narrowing. /// Each tick processes every prohibition: split-pending multi-register ranges get /// bisected (try left + right halves; replace with whichever halves still fail), /// single-register or non-split-pending ranges get a straight re-probe. Lives for @@ -605,10 +561,6 @@ public sealed class ModbusDriver catch (OperationCanceledException) when (ct.IsCancellationRequested) { return; } catch (ObjectDisposedException) when (ct.IsCancellationRequested) { - // Driver.Modbus-006: ShutdownAsync disposes the transport while we may be - // mid-pass. An ObjectDisposedException from the disposed transport is the - // expected shutdown race — swallow it here so the fire-and-forget task - // exits cleanly rather than faulting with the wrong failure mode. return; } } @@ -621,6 +573,7 @@ public sealed class ModbusDriver /// retry (single-register or already-narrowed). /// /// Cancellation token. + /// A task that represents the asynchronous operation. internal async Task RunReprobeOnceForTestAsync(CancellationToken ct) { var transport = _transport ?? throw new InvalidOperationException("Transport not connected"); @@ -661,7 +614,7 @@ public sealed class ModbusDriver } /// - /// #150 — bisect a multi-register prohibition. Removes the parent entry and re-adds + /// Bisect a multi-register prohibition. Removes the parent entry and re-adds /// whichever halves still fail. Over multiple re-probe ticks the prohibition narrows /// log2(span) times until it pinpoints the actual protected register(s). /// @@ -699,8 +652,6 @@ public sealed class ModbusDriver // and the next normal scan can re-coalesce across the whole original range. } - // #153 — log per-half outcome OUTSIDE the lock (logger calls can be expensive). - // Both halves clear → emit a single combined "fully cleared" line. if (!leftFailed && !rightFailed) LogProhibitionCleared(key.Unit, key.Region, key.Start, key.End); else @@ -726,7 +677,7 @@ public sealed class ModbusDriver } /// - /// #143 block-read coalescing planner. Groups eligible tags by (UnitId, Region), sorts + /// Block-read coalescing planner. Groups eligible tags by (UnitId, Region), sorts /// by start address, and merges adjacent / near-adjacent (gap ≤ MaxReadGap) into single /// FC03/FC04 reads. Per-block: emit one Modbus PDU, slice the response back into per-tag /// values, populate and the WriteOnChangeOnly cache. Returns @@ -777,8 +728,6 @@ public sealed class ModbusDriver var gap = tagStart - last.End - 1; var newEnd = Math.Max(tagEnd, last.End); var newSpan = newEnd - last.Start + 1; - // #148 — skip merges that would re-attempt a known-bad range. The - // per-tag fallback will read each member individually instead. var crossesProhibition = RangeIsAutoProhibited(group.Key.Unit, group.Key.Region, last.Start, (ushort)newEnd); if (gap <= _options.MaxReadGap && newSpan <= cap && !crossesProhibition) { @@ -791,11 +740,11 @@ public sealed class ModbusDriver } // Issue one PDU per block. On a Modbus-level exception (illegal data address / - // protected register), record the range as auto-prohibited (#148), leave the + // protected register), record the range as auto-prohibited, leave the // member indices UNhandled, and let the per-tag fallback in ReadAsync read each // surviving address individually. On transport-level failure (timeout / socket // drop) mark members Bad and short-circuit the per-tag fallback (hitting the - // dead socket again won't help). #150 bisection narrows the prohibition over + // dead socket again won't help). Bisection narrows the prohibition over // subsequent re-probe ticks. foreach (var block in blocks) { @@ -823,13 +772,6 @@ public sealed class ModbusDriver } catch (ModbusException mex) { - // #148 — record the failed range so the planner stops re-coalescing across - // it on subsequent scans. The members are intentionally NOT added to the - // handled-set: ReadAsync's per-tag fallback runs them individually in the - // same scan, so healthy tags around the protected hole keep working without - // operator intervention. Members that ARE the protected register will fail - // again at single-tag granularity and surface the per-tag exception code - // naturally — the block-level mex isn't propagated. RecordAutoProhibition(group.Key.Unit, group.Key.Region, block.Start, block.End); WriteHealth(new DriverHealth(DriverState.Degraded, ReadHealth().LastSuccessfulRead, mex.Message)); } @@ -871,8 +813,6 @@ public sealed class ModbusDriver var resp = await transport.SendAsync(unitId, pdu, ct).ConfigureAwait(false); // resp = [fc][byte-count][data...] — validate before indexing to surface a clean error // rather than an IndexOutOfRangeException when a device returns a truncated PDU. - // Driver.Modbus-005: guard resp.Length >= 2 (fc + byte-count) and that the payload is - // at least as long as the declared byte-count, matching the quantity we requested. if (resp.Length < 2) throw new InvalidDataException( $"Modbus register response too short: expected at least 2 bytes (fc+bytecount), got {resp.Length}"); @@ -894,7 +834,6 @@ public sealed class ModbusDriver var pdu = new byte[] { fc, (byte)(address >> 8), (byte)(address & 0xFF), (byte)(qty >> 8), (byte)(qty & 0xFF) }; var resp = await transport.SendAsync(unitId, pdu, ct).ConfigureAwait(false); - // Driver.Modbus-005: validate the response is structurally sound before indexing. if (resp.Length < 2) throw new InvalidDataException( $"Modbus bit response too short: expected at least 2 bytes (fc+bytecount), got {resp.Length}"); @@ -954,9 +893,7 @@ public sealed class ModbusDriver // ---- IWritable ---- - /// Writes values to the specified tag references on the Modbus device. - /// Write requests to execute. - /// Cancellation token. + /// public async Task> WriteAsync( IReadOnlyList writes, CancellationToken cancellationToken) { @@ -975,8 +912,6 @@ public sealed class ModbusDriver results[i] = new WriteResult(StatusBadNotWritable); continue; } - // #141 WriteOnChangeOnly suppression: skip the wire round-trip when the same value - // was already successfully written and no read since has invalidated the cache. if (_options.WriteOnChangeOnly && IsRedundantWrite(w.FullReference, w.Value)) { results[i] = new WriteResult(0u); @@ -996,10 +931,6 @@ public sealed class ModbusDriver } catch (InvalidDataException) { - // Driver.Modbus-014: malformed/truncated PDU during a write (e.g. the FC03 RMW - // read returning a short response). This is a communication-layer error — surface - // as BadCommunicationError to match the ReadAsync path, not BadInternalError which - // implies a driver code defect. results[i] = new WriteResult(StatusBadCommunicationError); } catch (Exception) @@ -1180,12 +1111,6 @@ public sealed class ModbusDriver try { // FC03 read 1 holding register at tag.Address. - // Driver.Modbus-013: use ReadRegisterBlockAsync so the response is validated before - // indexing (mirrors the fix applied to the normal read path in Driver.Modbus-005). - // Direct transport.SendAsync previously skipped the length checks, letting a truncated - // PDU throw IndexOutOfRangeException from readResp[2]/[3] — now surfaces as a clean - // InvalidDataException, which WriteAsync's communication-error catch arm maps to - // BadCommunicationError (see Driver.Modbus-014). var readRaw = await ReadRegisterBlockAsync(transport, ResolveUnitId(tag), 0x03, tag.Address, 1, ct).ConfigureAwait(false); // readRaw = [hi][lo] — ReadRegisterBlockAsync strips the fc+byte-count header and // validates the payload length, so [0] and [1] are always safe to index here. @@ -1208,17 +1133,12 @@ public sealed class ModbusDriver // ---- ISubscribable (polling overlay via shared engine) ---- - /// Subscribes to value changes on the specified tag references. - /// Tag references to subscribe to. - /// Interval for publishing changes. - /// Cancellation token. + /// public Task SubscribeAsync( IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) => Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval)); - /// Unsubscribes from value changes using the specified handle. - /// Subscription handle. - /// Cancellation token. + /// public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) { _poll.Unsubscribe(handle); @@ -1227,7 +1147,7 @@ public sealed class ModbusDriver // ---- IHostConnectivityProbe ---- - /// Gets the current connectivity status for all hosts. + /// public IReadOnlyList GetHostStatuses() { lock (_probeLock) @@ -1294,6 +1214,7 @@ public sealed class ModbusDriver /// from 2 chars per register). /// /// Tag definition to measure. + /// The number of 16-bit registers the tag occupies. internal static ushort RegisterCount(ModbusTagDefinition tag) => tag.DataType switch { ModbusDataType.Int16 or ModbusDataType.UInt16 or ModbusDataType.BitInRegister or ModbusDataType.Bcd16 => 1, @@ -1354,6 +1275,7 @@ public sealed class ModbusDriver /// Decodes a register value according to the tag's data type. /// Raw register bytes. /// Tag definition specifying the data type. + /// The decoded value, boxed as the CLR type matching the tag's data type. internal static object DecodeRegister(ReadOnlySpan data, ModbusTagDefinition tag) { switch (tag.DataType) @@ -1435,6 +1357,7 @@ public sealed class ModbusDriver /// Encodes a value into register bytes according to the tag's data type. /// Value to encode. /// Tag definition specifying the data type. + /// The encoded register bytes. internal static byte[] EncodeRegister(object? value, ModbusTagDefinition tag) { switch (tag.DataType) @@ -1531,6 +1454,8 @@ public sealed class ModbusDriver /// Map a Modbus logical type to the driver-agnostic used /// by the address-space builder. /// + /// Modbus logical data type to map. + /// The corresponding driver-agnostic data type. internal static DriverDataType MapDataType(ModbusDataType t) => t switch { ModbusDataType.Bool or ModbusDataType.BitInRegister => DriverDataType.Boolean, @@ -1553,6 +1478,7 @@ public sealed class ModbusDriver /// /// Raw BCD value. /// Number of nibbles to decode. + /// The decoded decimal value. internal static uint DecodeBcd(uint raw, int nibbles) { uint result = 0; @@ -1573,6 +1499,7 @@ public sealed class ModbusDriver /// /// Decimal value to encode. /// Number of nibbles to encode. + /// The encoded N-nibble BCD value. internal static uint EncodeBcd(uint value, int nibbles) { uint result = 0; @@ -1607,6 +1534,7 @@ public sealed class ModbusDriver /// extensions. /// /// Modbus exception code. + /// The mapped OPC UA StatusCode value. internal static uint MapModbusExceptionToStatus(byte exceptionCode) => exceptionCode switch { 0x01 => StatusBadNotSupported, // Illegal Function — FC not in supported list @@ -1622,11 +1550,12 @@ public sealed class ModbusDriver public void Dispose() => DisposeAsync().AsTask().GetAwaiter().GetResult(); /// - /// Driver.Modbus-004: DisposeAsync must perform the same teardown as ShutdownAsync so + /// DisposeAsync must perform the same teardown as ShutdownAsync so /// callers that use await using (without an explicit ShutdownAsync) do not /// leak the probe loop, re-probe loop, and poll-engine background tasks. Shares /// with to keep them in sync. /// + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { await TeardownAsync().ConfigureAwait(false); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverFactoryExtensions.cs index 392cc5d1..70606895 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusDriverFactoryExtensions.cs @@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus; /// /// Static factory registration helper for . Server's Program.cs -/// calls once at startup; the bootstrapper (task #248) then +/// calls once at startup; the bootstrapper then /// materialises Modbus DriverInstance rows from the central config DB into live driver /// instances. Mirrors GalaxyProxyDriverFactoryExtensions / FocasDriverFactoryExtensions. /// @@ -33,6 +33,7 @@ public static class ModbusDriverFactoryExtensions /// Public for the Server-side bootstrapper + test consumers (Admin.Tests, etc.). /// The unique identifier for the driver instance. /// The JSON configuration string for the driver. + /// The constructed instance. public static ModbusDriver CreateInstance(string driverInstanceId, string driverConfigJson) => CreateInstance(driverInstanceId, driverConfigJson, loggerFactory: null); @@ -40,6 +41,7 @@ public static class ModbusDriverFactoryExtensions /// The unique identifier for the driver instance. /// The JSON configuration string for the driver. /// Optional logger factory for creating loggers per driver instance. + /// The constructed instance. public static ModbusDriver CreateInstance(string driverInstanceId, string driverConfigJson, ILoggerFactory? loggerFactory) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); @@ -118,11 +120,6 @@ public static class ModbusDriverFactoryExtensions var name = t.Name ?? throw new InvalidOperationException( $"Modbus config for '{driverInstanceId}' has a tag missing Name"); - // Driver.Modbus-009: a String tag with StringLength = 0 yields RegisterCount = 0, which - // turns into an FC03/FC04 with quantity 0 — a spec-illegal request the PLC rejects with - // exception 03. Catch the misconfiguration at bind time with a clear diagnostic instead - // of waiting for the cryptic Illegal Data Value to surface at runtime. - // AddressString takes precedence over the structured fields (Region/Address/DataType/ // ByteOrder/BitIndex/StringLength/ArrayCount). Tags can mix forms freely — newer pasted // rows use the grammar string, legacy rows keep the structured form. Fields not derivable @@ -178,7 +175,7 @@ public static class ModbusDriverFactoryExtensions } /// - /// Driver.Modbus-009: reject StringLength = 0 for String-typed tags. The + /// Rejects StringLength = 0 for String-typed tags. The /// driver computes RegisterCount = (StringLength + 1) / 2 which would emit an /// FC03/FC04 with quantity = 0, a spec-illegal request the PLC rejects with /// exception 03 (Illegal Data Value). Surface as a clear bind-time error. @@ -249,7 +246,7 @@ public static class ModbusDriverFactoryExtensions /// Gets or sets the probe configuration. public ModbusProbeDto? Probe { get; init; } - /// Gets or sets the keep-alive configuration (connection-layer knob #139). + /// Gets or sets the keep-alive configuration (connection-layer knob). public ModbusKeepAliveDto? KeepAlive { get; init; } /// Gets or sets the idle disconnect timeout in milliseconds. public int? IdleDisconnectMs { get; init; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTcpTransport.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTcpTransport.cs index f317af5d..11268eb8 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTcpTransport.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Modbus/ModbusTcpTransport.cs @@ -63,8 +63,7 @@ public sealed class ModbusTcpTransport : IModbusTransport _reconnect = reconnect ?? new ModbusReconnectOptions(); } - /// Connects to the Modbus TCP server with IPv4 preference. - /// The cancellation token for the operation. + /// public async Task ConnectAsync(CancellationToken ct) { // Resolve the host explicitly + prefer IPv4. .NET's TcpClient default-constructor is @@ -101,7 +100,7 @@ public sealed class ModbusTcpTransport : IModbusTransport try { client.Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); - // Driver.Modbus-009: a TimeSpan < 1s previously truncated to 0 via the int cast, + // A TimeSpan < 1s previously truncated to 0 via the int cast, // which Windows / Linux interpret as "use the default" — silently defeating the // configured keep-alive timing. Round up to at least 1 second so a sub-second // configuration still produces a real keep-alive cadence. Negative values are @@ -116,21 +115,19 @@ public sealed class ModbusTcpTransport : IModbusTransport } /// - /// Driver.Modbus-009: cast a to a whole number of seconds with a - /// minimum of 1 — protects callers from the int-cast truncation that turned 500 ms + /// Cast a to a whole number of seconds with a + /// minimum of 1 — protects callers from the int-cast truncation that turned 500 ms /// keep-alive timing into "use the default" on most OSes. /// /// The timespan to clamp to whole seconds. + /// The clamped duration expressed as a whole number of seconds, never less than 1. internal static int ClampToWholeSeconds(TimeSpan ts) { var seconds = (int)Math.Ceiling(ts.TotalSeconds); return seconds < 1 ? 1 : seconds; } - /// Sends a Modbus PDU and returns the response, with automatic retry on socket failure. - /// The Modbus unit/slave ID. - /// The protocol data unit to send. - /// The cancellation token for the operation. + /// public async Task SendAsync(byte unitId, byte[] pdu, CancellationToken ct) { if (_disposed) throw new ObjectDisposedException(nameof(ModbusTcpTransport)); @@ -284,6 +281,7 @@ public sealed class ModbusTcpTransport : IModbusTransport } /// Asynchronously disposes the transport and underlying socket resources. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientBrowseSession.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientBrowseSession.cs index c4493eba..68a670b3 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientBrowseSession.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientBrowseSession.cs @@ -32,25 +32,19 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession _rootNodeId = rootNodeId; } - /// Opaque token identifying this session in the AdminUI registry. + /// public Guid Token { get; } = Guid.NewGuid(); - /// Wall-clock time of the most recent successful browse call; the reaper uses - /// this for idle eviction. + /// public DateTime LastUsedUtc { get; private set; } = DateTime.UtcNow; - /// Browse one level under the configured root node. - /// Cancellation token. + /// public Task> RootAsync(CancellationToken cancellationToken) => BrowseOneLevelAsync(_rootNodeId, cancellationToken); - /// Browse one level under the node identified by , - /// which must be a stable reference produced by - /// (or a plain ns=N;… form). - /// Stable reference string for the parent node. - /// Cancellation token. - /// Thrown when cannot be - /// resolved against the live session's namespace table. + /// + // Throws ArgumentException when nodeId cannot be resolved against the live session's + // namespace table. public Task> ExpandAsync(string nodeId, CancellationToken cancellationToken) { if (!NamespaceMap.TryResolve(_session, nodeId, out var resolved)) @@ -59,12 +53,7 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession return BrowseOneLevelAsync(resolved, cancellationToken); } - /// The OPC UA picker treats variables as terminal leaves and does not surface - /// a per-attribute side-panel, so this always returns empty. - /// is still refreshed to honour the contract and prevent - /// the reaper from evicting an active session that only receives attribute calls. - /// Ignored. - /// Ignored. + /// public Task> AttributesAsync(string nodeId, CancellationToken cancellationToken) { LastUsedUtc = DateTime.UtcNow; @@ -111,8 +100,7 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession var refs = result.References; // Follow browse continuation points so folders larger than the server's per-call - // cap aren't silently truncated (same pattern as runtime - // Driver.OpcUaClient-003). + // cap aren't silently truncated. var cp = result.ContinuationPoint; while (cp is { Length: > 0 }) { @@ -163,6 +151,7 @@ internal sealed class OpcUaClientBrowseSession : IBrowseSession /// Idempotent best-effort dispose: closes the underlying session if it's a /// concrete , disposes it, and disposes the gate. Close errors are /// swallowed because the registry reaper may be racing a remote disconnect. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_disposed) return; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientDriverBrowser.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientDriverBrowser.cs index 9d0e4f2d..69805ce4 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientDriverBrowser.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser/OpcUaClientDriverBrowser.cs @@ -40,13 +40,10 @@ public sealed class OpcUaClientDriverBrowser : IDriverBrowser _logger = logger ?? NullLogger.Instance; } - /// Driver type key — matches the AdminUI's persisted "OpcUaClient" value. + /// public string DriverType => "OpcUaClient"; - /// Opens a transient OPC UA session and returns a browse session over it. - /// Driver options serialized as JSON; same shape the runtime - /// driver would consume. - /// Cancellation for the connect phase only. + /// public async Task OpenAsync(string configJson, CancellationToken cancellationToken) { var opts = JsonSerializer.Deserialize(configJson, JsonOpts) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/NamespaceMap.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/NamespaceMap.cs index ed4cd3ee..ed879916 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/NamespaceMap.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Contracts/NamespaceMap.cs @@ -48,6 +48,7 @@ public sealed class NamespaceMap /// Snapshot the namespace table from a live session. /// The OPC UA session to snapshot. + /// A capturing the session's current namespace table. public static NamespaceMap FromSession(ISession session) { ArgumentNullException.ThrowIfNull(session); @@ -60,6 +61,7 @@ public sealed class NamespaceMap /// a live . /// /// The namespace table to snapshot. + /// A capturing the given namespace table. public static NamespaceMap FromTable(NamespaceTable namespaceUris) { ArgumentNullException.ThrowIfNull(namespaceUris); @@ -71,11 +73,13 @@ public sealed class NamespaceMap /// The namespace URI at the given index, or null if out of range. /// The zero-based index in the namespace table. + /// The namespace URI at , or null if out of range. public string? UriForIndex(int index) => index >= 0 && index < _uris.Length ? _uris[index] : null; /// The index for a namespace URI, or null if the URI is not in the table. /// The namespace URI to look up. + /// The index for , or null if the URI is not in the table. public ushort? IndexForUri(string uri) => _uriToIndex.TryGetValue(uri, out var idx) ? idx : null; @@ -87,6 +91,7 @@ public sealed class NamespaceMap /// address space so it survives a remote namespace-table reorder. /// /// The NodeId to render as a stable reference. + /// The server-stable reference string for . public string ToStableReference(NodeId nodeId) { ArgumentNullException.ThrowIfNull(nodeId); @@ -119,6 +124,7 @@ public sealed class NamespaceMap /// The reference string to resolve (either nsu=… or ns=N;… format). /// On success, the resolved NodeId; on failure, NodeId.Null. /// Use — not == null — to test this value. + /// true if resolved to a non-null NodeId; otherwise false. public static bool TryResolve(ISession? currentSession, string reference, out NodeId nodeId) { nodeId = NodeId.Null; diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs index 6065f072..3484b526 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient/OpcUaClientDriver.cs @@ -32,6 +32,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit { private readonly ILogger _logger; + /// Initializes a new instance of the class. /// Driver configuration. /// Stable logical ID from the config DB. /// Optional logger; defaults to NullLogger when not supplied. @@ -89,6 +90,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// can be exercised deterministically without standing up a live reconnect handler. /// Production code never calls this — the SDK reconnect handler owns the real swap. /// + /// The session instance to install, or null to clear it. internal void SetSessionForTest(ISession? session) => Session = session; private DriverHealth _health = new(DriverState.Unknown, null, null); @@ -99,7 +101,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// Cert-validation delegate wired when /// is true. Stored so / can /// detach it from the (potentially process-shared) - /// and avoid leaking the closure (Driver.OpcUaClient-012). + /// and avoid leaking the closure. /// private CertificateValidationEventHandler? _certValidationHandler; /// The that owns . @@ -107,8 +109,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// /// Approximate count of discovered nodes (folders + variables). Updated by /// and used to report a non-zero - /// to the Core allocation-slope detector - /// (Driver.OpcUaClient-013). + /// to the Core allocation-slope detector. /// private volatile int _discoveredNodeCount; /// @@ -116,7 +117,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// when the session's keep-alive channel reports a bad status. Null outside the /// reconnecting window; constructed lazily inside the keep-alive handler. Guarded by /// — keep-alive callbacks fire from the SDK timer thread and - /// can race a check-then-set if left unsynchronized (Driver.OpcUaClient-005). + /// can race a check-then-set if left unsynchronized. /// private SessionReconnectHandler? _reconnectHandler; @@ -125,19 +126,17 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// Stored NodeIds embed the server-stable namespace URI rather than the /// session-relative ns=N index, so a remote-server namespace-table reorder /// across a restart does not silently re-point stored references at the wrong - /// namespace (driver-specs.md §8 "Namespace Remapping", finding Driver.OpcUaClient-004). + /// namespace (driver-specs.md §8 "Namespace Remapping"). /// Null until returns cleanly. /// private NamespaceMap? _namespaceMap; - /// Gets the stable logical identifier for this driver instance from the config database. + /// public string DriverInstanceId => _driverInstanceId; - /// Gets the driver type identifier. + /// public string DriverType => "OpcUaClient"; - /// Initializes the OPC UA client driver with the given configuration. - /// JSON-serialized driver configuration. - /// Cancellation token for the operation. + /// public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { _health = new DriverHealth(DriverState.Initializing, null, null); @@ -276,7 +275,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit await config.ValidateAsync(ApplicationType.Client, ct).ConfigureAwait(false); // AutoAccept=true is a dev-only escape hatch. Emit a prominent warning so a - // production misconfiguration is immediately visible in logs (Driver.OpcUaClient-012). + // production misconfiguration is immediately visible in logs. if (_options.AutoAcceptCertificates) { _logger.LogWarning( @@ -289,7 +288,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // Accept the full set of certificate-validation error codes: a real dev cert can // fail with BadCertificateChainIncomplete, BadCertificateTimeInvalid, or // BadCertificateHostNameInvalid, not only BadCertificateUntrusted. Only accepting - // the latter would silently fail for those certs (Driver.OpcUaClient-012). + // the latter would silently fail for those certs. CertificateValidationEventHandler handler = (_, e) => e.Accept = true; config.CertificateValidator.CertificateValidation += handler; // Store refs so ShutdownAsync + Dispose can detach the delegate and avoid @@ -312,6 +311,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// existing single-endpoint configs keep working without migration. /// /// Driver options containing endpoint configuration. + /// The ordered list of endpoint URLs to try. internal static IReadOnlyList ResolveEndpointCandidates(OpcUaClientDriverOptions opts) { if (opts.EndpointUrls is { Count: > 0 }) return opts.EndpointUrls; @@ -361,6 +361,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// private key N times, wasteful + keeps the password in memory longer. /// /// Driver options containing authentication configuration. + /// The constructed for session activation. internal static UserIdentity BuildUserIdentity(OpcUaClientDriverOptions options) => options.AuthType switch { @@ -464,6 +465,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// challenge during session activation). /// /// Driver options containing certificate configuration. + /// The certificate-backed . internal static UserIdentity BuildCertificateIdentity(OpcUaClientDriverOptions options) { if (string.IsNullOrWhiteSpace(options.UserCertificatePath)) @@ -491,6 +493,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// Convert a driver to the OPC UA policy URI. /// The driver security policy to map. + /// The corresponding OPC UA security policy URI. internal static string MapSecurityPolicy(OpcUaSecurityPolicy policy) => policy switch { OpcUaSecurityPolicy.None => SecurityPolicies.None, @@ -505,17 +508,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit private static string ShortPolicyName(string policyUri) => policyUri?.Substring(policyUri.LastIndexOf('#') + 1) ?? "(null)"; - /// Reinitializes the driver with new configuration, shutting down and restarting the session. - /// JSON-serialized driver configuration. - /// Cancellation token for the operation. + /// public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { await ShutdownAsync(cancellationToken).ConfigureAwait(false); await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false); } - /// Gracefully shuts down the OPC UA session, unsubscribing all active monitoring items and closing the connection. - /// Cancellation token for the operation. + /// public async Task ShutdownAsync(CancellationToken cancellationToken) { // Tear down remote subscriptions first — otherwise Session.Close will try and may fail @@ -523,7 +523,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // whether or not the wire-side delete succeeds since the local handles are useless // after close anyway. Before deleting each subscription we detach the Notification // handlers we attached at subscribe time so the SDK's invocation list no longer - // holds the driver instance through the closure (Driver.OpcUaClient-014). + // holds the driver instance through the closure. foreach (var rs in _subscriptions.Values) { DetachNotificationHandlers(rs.ItemHandlers); @@ -544,8 +544,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // Abort any in-flight reconnect attempts before touching the session — BeginReconnect's // retry loop holds a reference to the current session and would fight Session.CloseAsync // if left spinning. Take the handler under _probeLock so a keep-alive callback racing - // through OnKeepAlive can't arm a fresh handler after we've torn this one down - // (Driver.OpcUaClient-005). + // through OnKeepAlive can't arm a fresh handler after we've torn this one down. SessionReconnectHandler? handlerToCancel; lock (_probeLock) { @@ -556,9 +555,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit handlerToCancel?.Dispose(); // Take the session reference under _probeLock before touching it, so we can't race - // an OnReconnectComplete that is simultaneously swapping to a new session - // (Driver.OpcUaClient-006). We clear Session to null here so any concurrent caller - // that checks inside _gate sees null immediately after shutdown begins. + // an OnReconnectComplete that is simultaneously swapping to a new session. We clear + // Session to null here so any concurrent caller that checks inside _gate sees null + // immediately after shutdown begins. ISession? sessionToClose; lock (_probeLock) { @@ -578,8 +577,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit _connectedEndpointUrl = null; // Detach the cert-validation handler so the (potentially process-shared) - // CertificateValidator doesn't hold a delegate to a shutting-down driver - // (Driver.OpcUaClient-012). + // CertificateValidator doesn't hold a delegate to a shutting-down driver. if (_certValidationHandler is not null && _certValidatorRef is not null) { try { _certValidatorRef.CertificateValidation -= _certValidationHandler; } catch { } @@ -591,28 +589,21 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null); } - /// Gets the current health status of the OPC UA client driver. + /// public DriverHealth GetHealth() => _health; - /// - /// Returns an approximate in-driver memory footprint for the Core allocation-slope - /// detector. Each discovered node (folder or variable) contributes ~512 bytes to cover - /// the record, the browse-name string, and the stable - /// nsu= reference string stored in the address-space builder. The real number - /// depends on string length + box sizes; the constant is conservative enough that a - /// 10k-node remote server reports ~5 MB — well within the budget and detectable by the - /// Core slope alarm (Driver.OpcUaClient-013). - /// + /// + // Each discovered node (folder or variable) contributes ~512 bytes to cover the + // DriverAttributeInfo record, the browse-name string, and the stable nsu= reference string + // stored in the address-space builder. The real number depends on string length + box sizes; + // the constant is conservative enough that a 10k-node remote server reports ~5 MB — well + // within the budget and detectable by the Core slope alarm. public long GetMemoryFootprint() => _discoveredNodeCount * 512L; - /// - /// Drops the discovered-node count so the Core's cache-budget enforcement can request - /// a flush when footprint budget is breached. The OPC UA Client driver holds no - /// independently-flushable cache beyond what the address-space builder retains — a - /// flush here resets the footprint counter and signals the Core that re-discovery - /// will rebuild it cleanly from the remote server. - /// - /// Cancellation token for the operation. + /// + // The OPC UA Client driver holds no independently-flushable cache beyond what the + // address-space builder retains — a flush here resets the discovered-node footprint + // counter and signals Core that re-discovery will rebuild it cleanly from the remote server. public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) { _discoveredNodeCount = 0; @@ -621,15 +612,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // ---- IReadable ---- - /// Reads the current values of the specified nodes from the remote OPC UA server. - /// Fully-qualified node identifiers to read. - /// Cancellation token for the operation. + /// public async Task> ReadAsync( IReadOnlyList fullReferences, CancellationToken cancellationToken) { // Make sure a session exists before queuing on the gate, but do NOT bind the wire // call to this reference — a reconnect can swap Session while we wait on _gate. The - // session actually used is re-read inside the gate (Driver.OpcUaClient-001/-006). + // session actually used is re-read inside the gate. _ = RequireSession(); var results = new DataValueSnapshot[fullReferences.Count]; var now = DateTime.UtcNow; @@ -712,14 +701,12 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // ---- IWritable ---- - /// Writes values to the specified nodes on the remote OPC UA server. - /// Write requests specifying nodes and values to write. - /// Cancellation token for the operation. + /// public async Task> WriteAsync( IReadOnlyList writes, CancellationToken cancellationToken) { // See ReadAsync — the wire call must use the session current inside the gate, not a - // reference captured before WaitAsync (Driver.OpcUaClient-001/-006). + // reference captured before WaitAsync. _ = RequireSession(); var results = new WriteResult[writes.Count]; @@ -729,9 +716,8 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit var session = Session; if (session is null) { - // Writes are non-idempotent (decision #44/#45) — but here the request never - // reached the wire, so BadCommunicationError ("definitely did not happen") is - // the honest code. + // Writes are non-idempotent — but here the request never reached the wire, so + // BadCommunicationError ("definitely did not happen") is the honest code. for (var i = 0; i < writes.Count; i++) results[i] = new WriteResult(StatusBadCommunicationError); return results; @@ -777,10 +763,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit catch (OperationCanceledException) { // Timeout / cancellation after the wire request may have been dispatched. - // Writes are non-idempotent (decision #44/#45) — BadTimeout ("outcome unknown, - // do not blindly retry") is more honest than BadCommunicationError ("definitely - // did not happen"). Downstream callers that need retry semantics check for - // BadTimeout and can decide whether to re-issue (Driver.OpcUaClient-009). + // Writes are non-idempotent — BadTimeout ("outcome unknown, do not blindly + // retry") is more honest than BadCommunicationError ("definitely did not + // happen"). Downstream callers that need retry semantics check for BadTimeout + // and can decide whether to re-issue. const uint StatusBadTimeout = 0x800A0000u; for (var w = 0; w < indexMap.Count; w++) results[indexMap[w]] = new WriteResult(StatusBadTimeout); @@ -810,6 +796,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// The OPC UA session to resolve the node ID against. /// The full reference string to parse. /// The parsed node ID when successful. + /// true if the reference was parsed successfully; otherwise false. internal static bool TryParseNodeId(ISession session, string fullReference, out NodeId nodeId) => NamespaceMap.TryResolve(session, fullReference, out nodeId); @@ -826,23 +813,19 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // ---- ITagDiscovery ---- - /// - /// Run-once: recursively browses the remote server's address - /// space and registers every variable within the single call (browse + enrich passes are - /// fully awaited) — nothing fills in asynchronously after connect, so a single discovery - /// pass is sufficient. - /// + /// + // Run-once: DiscoverAsync recursively browses the remote server's address space and + // registers every variable within the single call (browse + enrich passes are fully + // awaited) — nothing fills in asynchronously after connect, so a single discovery pass + // is sufficient. public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; - /// Discovers the remote OPC UA server's address space and materializes it through the supplied builder. - /// Address space builder for materializing discovered nodes. - /// Cancellation token for the operation. + /// public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(builder); // Confirm a session exists before queuing; the session actually browsed is re-read - // inside the gate so a reconnect mid-wait can't leave us browsing a closed session - // (Driver.OpcUaClient-001/-006). + // inside the gate so a reconnect mid-wait can't leave us browsing a closed session. _ = RequireSession(); var rootFolder = builder.Folder("Remote", "Remote"); @@ -880,7 +863,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit .ConfigureAwait(false); // Update the footprint counter so GetMemoryFootprint() returns a real estimate - // after each discovery pass (Driver.OpcUaClient-013). + // after each discovery pass. _discoveredNodeCount = discovered; } finally { _gate.Release(); } @@ -937,8 +920,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // per node in a single response; when a folder has more children than the cap, // BrowseResult.ContinuationPoint is non-empty and the remainder must be pulled // with BrowseNext. Without this loop a large remote folder is silently truncated - // and discovered tags go missing from the local address space - // (Driver.OpcUaClient-003). + // and discovered tags go missing from the local address space. var continuationPoint = result.ContinuationPoint; while (continuationPoint is { Length: > 0 }) { @@ -954,8 +936,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit catch (OperationCanceledException) { // Caller cancelled mid-pagination. Release the server-side cursor so it - // doesn't hold resources until session close (Driver.OpcUaClient-016; - // same pattern as Driver.OpcUaClient.Browser-002). Use CancellationToken.None + // doesn't hold resources until session close. Use CancellationToken.None // for the release — the original token is already cancelled so we must not // gate the release on it. Fire-and-forget: if the server is also unreachable, // the release fails silently and the cursor times out server-side anyway. @@ -1132,13 +1113,14 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// StatusCode + timestamps. /// /// The OPC UA data type NodeId to map. + /// The mapped . internal static DriverDataType MapUpstreamDataType(NodeId dataType) { if (dataType == DataTypeIds.Boolean) return DriverDataType.Boolean; // SByte (signed 8-bit) shares Int16 — DriverDataType has no narrower signed type. // Byte (unsigned 8-bit) belongs in the unsigned family → UInt16, not Int16 - // (Driver.OpcUaClient-010: mapping an unsigned 0-255 type onto Int16 misrepresents - // type metadata and confuses range/validation logic keyed off DriverDataType). + // (mapping an unsigned 0-255 type onto Int16 misrepresents type metadata and + // confuses range/validation logic keyed off DriverDataType). if (dataType == DataTypeIds.SByte || dataType == DataTypeIds.Int16) return DriverDataType.Int16; if (dataType == DataTypeIds.Byte || dataType == DataTypeIds.UInt16) return DriverDataType.UInt16; if (dataType == DataTypeIds.Int32) return DriverDataType.Int32; @@ -1160,6 +1142,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// ; read-only as . /// /// The OPC UA access level bitmask. + /// The mapped security classification. internal static SecurityClassification MapAccessLevelToSecurityClass(byte accessLevel) { const byte CurrentWrite = 2; // AccessLevels.CurrentWrite = 0x02 @@ -1170,16 +1153,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // ---- ISubscribable ---- - /// Subscribes to monitored value changes on the specified nodes from the remote OPC UA server. - /// Fully-qualified node identifiers to monitor. - /// Desired minimum interval between publish cycles. - /// Cancellation token for the operation. + /// public async Task SubscribeAsync( IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) { // Fast-fail before queuing if the driver isn't connected, but do NOT bind the // subscription to this reference — a reconnect can swap Session while we wait on _gate. - // The session actually used is re-read inside the gate (Driver.OpcUaClient-001/-006). + // The session actually used is re-read inside the gate. _ = RequireSession(); var id = Interlocked.Increment(ref _nextSubscriptionId); var handle = new OpcUaSubscriptionHandle(id); @@ -1215,9 +1195,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit await subscription.CreateAsync(cancellationToken).ConfigureAwait(false); // Track each (MonitoredItem, handler) pair so UnsubscribeAsync / ShutdownAsync - // can detach the Notification delegate before disposing the session - // (Driver.OpcUaClient-014). The lambda captures `handle`, so we must hold the - // exact delegate instance returned by `+=` to be able to remove it. + // can detach the Notification delegate before disposing the session. The lambda + // captures `handle`, so we must hold the exact delegate instance returned by + // `+=` to be able to remove it. var itemHandlers = new List(); foreach (var fullRef in fullReferences) { @@ -1252,9 +1232,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit return handle; } - /// Unsubscribes from monitored value changes for the specified subscription handle. - /// The subscription handle to unsubscribe. - /// Cancellation token for the operation. + /// public async Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) { if (handle is not OpcUaSubscriptionHandle h) return; @@ -1265,9 +1243,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit { // Detach Notification handlers BEFORE deleting the subscription so the SDK's // MonitoredItem.Notification multicast invocation list no longer holds a - // closure that captures the driver instance (Driver.OpcUaClient-014). The - // delegate stored on RemoteSubscription is the exact instance that was added, - // so `-=` removes it cleanly. + // closure that captures the driver instance. The delegate stored on + // RemoteSubscription is the exact instance that was added, so `-=` removes it + // cleanly. DetachNotificationHandlers(rs.ItemHandlers); try { await rs.Subscription.DeleteAsync(silent: true, cancellationToken).ConfigureAwait(false); } catch { /* best-effort — the subscription may already be gone on reconnect */ } @@ -1305,8 +1283,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// Live data-change subscription bookkeeping. Holds the SDK , /// the local handle, and the per-MonitoredItem (item, handler) pairs so /// / can detach the - /// Notification delegates before the SDK disposes the subscription - /// (Driver.OpcUaClient-014). + /// Notification delegates before the SDK disposes the subscription. /// private sealed record RemoteSubscription( Subscription Subscription, @@ -1326,7 +1303,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit private sealed record OpcUaSubscriptionHandle(long Id) : ISubscriptionHandle { - /// Gets the diagnostic identifier for this subscription. + /// public string DiagnosticId => $"opcua-sub-{Id}"; } @@ -1344,15 +1321,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit private const int AlarmFieldTime = 5; private const int AlarmFieldConditionId = 6; - /// Subscribes to alarm and event notifications from the remote OPC UA server. - /// Source node identifiers to subscribe alarms from. - /// Cancellation token for the operation. + /// public async Task SubscribeAlarmsAsync( IReadOnlyList sourceNodeIds, CancellationToken cancellationToken) { // Fast-fail before queuing if the driver isn't connected, but do NOT bind the alarm // subscription to this reference — a reconnect can swap Session while we wait on _gate. - // The session actually used is re-read inside the gate (Driver.OpcUaClient-001/-006). + // The session actually used is re-read inside the gate. _ = RequireSession(); var id = Interlocked.Increment(ref _nextAlarmSubscriptionId); var handle = new OpcUaAlarmSubscriptionHandle(id); @@ -1424,9 +1399,9 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit Handle = handle, }; // Capture the exact delegate instance so UnsubscribeAlarmsAsync / ShutdownAsync - // can `-=` it later (Driver.OpcUaClient-014). The lambda captures `handle` and - // `sourceFilter`, so without the explicit detach the SDK's invocation list keeps - // the driver instance alive until the session itself is disposed. + // can `-=` it later. The lambda captures `handle` and `sourceFilter`, so without + // the explicit detach the SDK's invocation list keeps the driver instance alive + // until the session itself is disposed. MonitoredItemNotificationEventHandler notifHandler = (mi, args) => OnEventNotification(handle, sourceFilter, mi, args); eventItem.Notification += notifHandler; @@ -1440,9 +1415,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit return handle; } - /// Unsubscribes from alarm and event notifications for the specified alarm subscription handle. - /// The alarm subscription handle to unsubscribe. - /// Cancellation token for the operation. + /// public async Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) { if (handle is not OpcUaAlarmSubscriptionHandle h) return; @@ -1453,7 +1426,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit { // Detach the Notification handler before deleting the subscription so the SDK's // multicast invocation list no longer holds the driver instance through the - // closure (Driver.OpcUaClient-014). + // closure. try { rs.EventItem.Notification -= rs.Handler; } catch { /* best-effort */ } try { await rs.Subscription.DeleteAsync(silent: true, cancellationToken).ConfigureAwait(false); } @@ -1462,9 +1435,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit finally { _gate.Release(); } } - /// Acknowledges multiple alarms by calling the remote OPC UA server's Acknowledge method. - /// List of alarm acknowledgement requests. - /// Cancellation token for the operation. + /// public async Task AcknowledgeAsync( IReadOnlyList acknowledgements, CancellationToken cancellationToken) { @@ -1474,8 +1445,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit if (acknowledgements.Count == 0) return; // Fast-fail before queuing if the driver isn't connected, but do NOT bind the ack calls // (or the namespace-relative ConditionId parse) to this reference — a reconnect can swap - // Session while we wait on _gate. The session actually used is re-read inside the gate - // (Driver.OpcUaClient-001/-006). + // Session while we wait on _gate. The session actually used is re-read inside the gate. _ = RequireSession(); await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -1518,7 +1488,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // Inspect per-ack results — the upstream server can reject individual acks // (BadConditionAlreadyAcked, BadNodeIdUnknown, BadUserAccessDenied) even when // the batch transport succeeds. Operators acking a critical alarm deserve to - // know if the ack didn't take (Driver.OpcUaClient-008). + // know if the ack didn't take. if (resp?.Results is not null) { for (var i = 0; i < resp.Results.Count; i++) @@ -1590,6 +1560,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// guidance: 1-200 Low, 201-500 Medium, 501-800 High, 801-1000 Critical. /// /// The OPC UA severity value (1-1000). + /// The mapped coarse-grained alarm severity bucket. internal static AlarmSeverity MapSeverity(ushort opcSeverity) => opcSeverity switch { <= 200 => AlarmSeverity.Low, @@ -1602,7 +1573,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// Live alarm-event subscription bookkeeping. Holds the SDK , /// the local handle, the single event-MonitoredItem (`Server/Events`), and the exact /// handler delegate instance so unsubscribe / shutdown can detach the Notification - /// event before the SDK disposes the subscription (Driver.OpcUaClient-014). + /// event before the SDK disposes the subscription. /// private sealed record RemoteAlarmSubscription( Subscription Subscription, @@ -1612,18 +1583,13 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit private sealed record OpcUaAlarmSubscriptionHandle(long Id) : IAlarmSubscriptionHandle { - /// Gets the diagnostic identifier for this alarm subscription. + /// public string DiagnosticId => $"opcua-alarm-sub-{Id}"; } // ---- IHistoryProvider (passthrough to upstream server) ---- - /// Reads raw historical data from the remote OPC UA server. - /// Fully-qualified node identifier to read history for. - /// Start time in UTC for the history query. - /// End time in UTC for the history query. - /// Maximum number of values to return. - /// Cancellation token for the operation. + /// public async Task ReadRawAsync( string fullReference, DateTime startUtc, DateTime endUtc, uint maxValuesPerNode, CancellationToken cancellationToken) @@ -1640,13 +1606,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit .ConfigureAwait(false); } - /// Reads processed (aggregated) historical data from the remote OPC UA server. - /// Fully-qualified node identifier to read history for. - /// Start time in UTC for the history query. - /// End time in UTC for the history query. - /// Time interval for aggregation. - /// The aggregation function to apply. - /// Cancellation token for the operation. + /// public async Task ReadProcessedAsync( string fullReference, DateTime startUtc, DateTime endUtc, TimeSpan interval, HistoryAggregateType aggregate, CancellationToken cancellationToken) @@ -1663,10 +1623,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit .ConfigureAwait(false); } - /// Reads historical data at specific timestamps from the remote OPC UA server. - /// Fully-qualified node identifier to read history for. - /// List of specific timestamps to read values at. - /// Cancellation token for the operation. + /// public async Task ReadAtTimeAsync( string fullReference, IReadOnlyList timestampsUtc, CancellationToken cancellationToken) { @@ -1690,8 +1647,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit { // Make sure a session exists before queuing on the gate, but do NOT bind the wire call // (or the namespace-relative NodeId parse) to this reference — a reconnect can swap - // Session while we wait on _gate. The session actually used is re-read inside the gate - // (Driver.OpcUaClient-001/-006). + // Session while we wait on _gate. The session actually used is re-read inside the gate. _ = RequireSession(); await _gate.WaitAsync(ct).ConfigureAwait(false); @@ -1748,6 +1704,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// Map to the OPC UA Part 13 standard aggregate NodeId. /// The aggregation function type to map. + /// The standard aggregate function NodeId. internal static NodeId MapAggregateToNodeId(HistoryAggregateType aggregate) => aggregate switch { HistoryAggregateType.Average => ObjectIds.AggregateFunction_Average, @@ -1847,30 +1804,22 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return 0; } } - /// - /// Forwards OPC UA HistoryReadEvents to the upstream server. Sends the fixed canonical - /// and maps the result onto - /// (the OtOpcUa server projects only those six BaseEventType fields, so a richer client - /// filter would be discarded server-side — the driver supplies the canonical set itself). - /// - /// - /// Upstream event-notifier NodeId to read from (mirrors how fullReference is the - /// upstream NodeId for raw reads). Null/empty → the upstream Server object (i=2253), - /// the standard server-wide event notifier. An unparseable id short-circuits to an empty - /// result, matching the raw path's malformed-NodeId behavior. - /// - /// Inclusive lower bound on event time (UTC). - /// Exclusive upper bound on event time (UTC). - /// Upper cap; <= 0 means "no cap" (NumValuesPerNode = 0). - /// Request cancellation. - /// The historical events plus the upstream continuation point (null when complete). + /// + // Forwards OPC UA HistoryReadEvents to the upstream server. Sends the fixed canonical + // BuildBaseEventFilter and maps the result onto HistoricalEvent (the OtOpcUa server projects + // only those six BaseEventType fields, so a richer client filter would be discarded + // server-side — the driver supplies the canonical set itself). sourceName is the upstream + // event-notifier NodeId to read from (mirrors how fullReference is the upstream NodeId for + // raw reads); null/empty resolves to the upstream Server object (i=2253), the standard + // server-wide event notifier, and an unparseable id short-circuits to an empty result, + // matching the raw path's malformed-NodeId behavior. public async Task ReadEventsAsync( string? sourceName, DateTime startUtc, DateTime endUtc, int maxEvents, CancellationToken cancellationToken) { // Confirm a session exists before queuing; the session actually used — and the // namespace-relative source-NodeId parse — are re-read inside the gate so a reconnect - // mid-wait can't leave us reading from a closed session (Driver.OpcUaClient-001/-006). + // mid-wait can't leave us reading from a closed session. _ = RequireSession(); await _gate.WaitAsync(cancellationToken).ConfigureAwait(false); @@ -1943,7 +1892,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit ?? ResolveEndpointCandidates(_options).FirstOrDefault() ?? _options.EndpointUrl; - /// Gets the current connectivity status of the remote OPC UA server host. + /// public IReadOnlyList GetHostStatuses() { lock (_probeLock) @@ -1971,7 +1920,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // while the channel stays down — the check-then-set must be atomic, otherwise two // callbacks both observe null, both construct a SessionReconnectHandler, and the // second assignment leaks the first (its retry loop keeps running, unreferenced and - // never disposed). Guard with _probeLock (Driver.OpcUaClient-005). + // never disposed). Guard with _probeLock. SessionReconnectHandler handler; lock (_probeLock) { @@ -2008,7 +1957,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // OnReconnectComplete, OnKeepAlive, and ShutdownAsync cannot race each other: // a session swap visible to concurrent ReadAsync/WriteAsync/DiscoverAsync callers // (which re-read Session inside _gate) must be atomic w.r.t. disposal and - // re-arming (Driver.OpcUaClient-006). + // re-arming. ISession? oldSession; lock (_probeLock) { @@ -2051,10 +2000,10 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // maxReconnectPeriod and invoked the callback with a null Session. Without an // explicit Faulted signal the driver is permanently wedged: no session, no live // keep-alive to re-trigger OnKeepAlive, and the Core never learns it must offer an - // operator reinitialize (Driver.OpcUaClient-002). Surface Faulted so the Core fans - // out Bad quality and ReinitializeAsync becomes available, and arm a fresh reconnect - // attempt against the last-known session for an always-on gateway rather than - // abandoning recovery entirely. + // operator reinitialize. Surface Faulted so the Core fans out Bad quality and + // ReinitializeAsync becomes available, and arm a fresh reconnect attempt against + // the last-known session for an always-on gateway rather than abandoning recovery + // entirely. TransitionTo(HostState.Faulted); _health = new DriverHealth( DriverState.Faulted, _health.LastSuccessfulRead, @@ -2127,7 +2076,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit /// handler detach. The async session-close is intentionally skipped — it requires a /// live session + network round-trip and is unsafe to block-on from a potentially /// single-threaded context (OPC UA stack thread). The session will be cleaned up by - /// the SDK's own finalizer on GC (Driver.OpcUaClient-007: no sync-over-async). + /// the SDK's own finalizer on GC (no sync-over-async). /// public void Dispose() { @@ -2154,7 +2103,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // Detach the cert-validation handler registered during InitializeAsync so the // CertificateValidator (which may be process-shared) doesn't hold a reference to - // a disposed driver (Driver.OpcUaClient-012). + // a disposed driver. if (_certValidationHandler is not null && _certValidatorRef is not null) { try { _certValidatorRef.CertificateValidation -= _certValidationHandler; } catch { } @@ -2165,7 +2114,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit // Acquire the gate once so any in-flight gated operation (ReadAsync / WriteAsync / // DiscoverAsync) has definitely released before we dispose the gate. Without this // drain, a background read that calls _gate.Release() after Dispose throws - // ObjectDisposedException (Driver.OpcUaClient-007). + // ObjectDisposedException. try { if (_gate.Wait(TimeSpan.FromSeconds(2))) @@ -2176,6 +2125,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit } /// Asynchronously disposes the driver and releases all associated resources. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_disposed) return; @@ -2183,7 +2133,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit try { await ShutdownAsync(CancellationToken.None).ConfigureAwait(false); } catch { /* disposal is best-effort */ } - // Detach the cert-validation handler (Driver.OpcUaClient-012). + // Detach the cert-validation handler. if (_certValidationHandler is not null && _certValidatorRef is not null) { try { _certValidatorRef.CertificateValidation -= _certValidationHandler; } catch { } @@ -2192,7 +2142,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit } // Drain the gate before disposal so no in-flight _gate.Release() fires after - // Dispose (Driver.OpcUaClient-007). + // Dispose. try { await _gate.WaitAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false); diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs index 1e374cd5..23c8974b 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7.Contracts/S7DriverOptions.cs @@ -82,10 +82,6 @@ public sealed class S7ProbeOptions /// Gets or sets the probe timeout. public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2); - // Driver.S7-012: ProbeAddress was configured and documented but was never read by the - // probe loop. ProbeLoopAsync uses S7.Net's ReadStatusAsync (a CPU-status PDU), not a - // DB/Merker read — it does not consume an explicit address. Rather than ship dead config - // surface, ProbeAddress has been removed. The liveness check is purely ReadStatusAsync-based. } /// @@ -98,7 +94,7 @@ public sealed class S7ProbeOptions /// When true the driver accepts writes for this tag. /// For DataType = String: S7-string max length. Default 254 (S7 max). /// -/// Per docs/v2/plan.md decisions #44, #45, #143 — flag a tag as safe to replay on +/// Flag a tag as safe to replay on /// write timeout / failure. Default false; writes do not auto-retry. Safe candidates /// on S7: DB word/dword set-points holding analog values, configuration DBs where the same /// value can be written again without side-effects. Unsafe: M (merker) bits or Q (output) diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7CpuTypeMap.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7CpuTypeMap.cs index 9f591094..1529ec15 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7CpuTypeMap.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7CpuTypeMap.cs @@ -4,6 +4,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7; internal static class S7CpuTypeMap { + /// Maps the driver's enum to the S7.Net library's equivalent CPU type. + /// The driver-configured S7 CPU type. + /// The corresponding S7.Net CpuType value. public static S7NetCpuType ToS7Net(S7CpuType type) => type switch { S7CpuType.S7200 => S7NetCpuType.S7200, diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs index c135b1f1..717da6ad 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7Driver.cs @@ -62,7 +62,6 @@ public sealed class S7Driver /// Handle to the in-flight probe loop. Tracked (rather than fire-and-forget) so /// can await it after cancelling — otherwise a probe /// iteration still inside the would race a disposed semaphore. - /// See code-review finding Driver.S7-006. /// private Task? _probeTask; @@ -101,7 +100,7 @@ public sealed class S7Driver /// /// Active driver configuration. Seeded from the constructor argument, then replaced by /// whatever / parse out of - /// the supplied driverConfigJson — see code-review finding Driver.S7-011. The + /// the supplied driverConfigJson. The /// constructor value is the fallback used when the caller passes an empty / placeholder /// JSON document (e.g. the "{}" some unit tests pass). /// @@ -125,22 +124,19 @@ public sealed class S7Driver private DriverHealth _health = new(DriverState.Unknown, null, null); private bool _disposed; - /// Gets the unique driver instance identifier. + /// public string DriverInstanceId => _driverInstanceId; - /// Gets the driver type name. + /// public string DriverType => "S7"; - /// Initializes the driver with the provided configuration. - /// JSON configuration string. - /// Cancellation token. - /// A task representing the asynchronous operation. + /// public async Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { _health = new DriverHealth(DriverState.Initializing, null, null); try { // Re-parse the supplied DriverConfig JSON so a config change delivered through the - // IDriver contract is honoured (Driver.S7-011). An empty / placeholder document + // IDriver contract is honoured. An empty / placeholder document // (e.g. the "{}" some unit tests pass) keeps the constructor-supplied options. if (HasConfigBody(driverConfigJson)) _options = S7DriverFactoryExtensions.ParseOptions(_driverInstanceId, driverConfigJson); @@ -152,7 +148,7 @@ public sealed class S7Driver // so a config typo fails fast at init instead of as a misleading per-read fault. RejectUnsupportedTagConfigs(); - // Legacy data-type seam (Driver.S7-013). The UnimplementedDataTypes set is now + // Legacy data-type seam. The UnimplementedDataTypes set is now // empty (Phase 4d unblocked all five wide types), so this rejects nothing — kept as // the single grep target / future seam should a type ever need re-gating at init. RejectUnsupportedTagDataTypes(); @@ -194,7 +190,7 @@ public sealed class S7Driver { _probeCts = new CancellationTokenSource(); // Track the probe Task (not fire-and-forget) so ShutdownAsync can await it - // before disposing _gate / _probeCts (Driver.S7-006). Pass None to Task.Run so + // before disposing _gate / _probeCts. Pass None to Task.Run so // the delegate always runs and the handle is always awaitable; the loop's own // token check handles cancellation. _probeTask = Task.Run(() => ProbeLoopAsync(_probeCts.Token), CancellationToken.None); @@ -212,27 +208,22 @@ public sealed class S7Driver } } - /// Reinitializes the driver with a new configuration. - /// JSON configuration string. - /// Cancellation token. - /// A task representing the asynchronous operation. + /// public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { // InitializeAsync re-parses driverConfigJson, so a config change delivered here is - // applied in place rather than silently discarded (Driver.S7-011). + // applied in place rather than silently discarded. await ShutdownAsync(cancellationToken).ConfigureAwait(false); await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false); } - /// Shuts down the driver and releases resources. - /// Cancellation token. - /// A task representing the asynchronous operation. + /// public async Task ShutdownAsync(CancellationToken cancellationToken) { // Signal cancellation to the probe + poll loops first, collect their Task handles, // then await all of them with a bounded timeout BEFORE disposing the shared semaphore // and CTS objects. Without the drain, a loop iteration mid-_gate would call Release() - // on (or WaitAsync against) a disposed semaphore — see code-review finding Driver.S7-006. + // on (or WaitAsync against) a disposed semaphore. var drain = new List(); var probeCts = _probeCts; @@ -271,13 +262,10 @@ public sealed class S7Driver _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null); } - /// Gets the current driver health. - /// The current health state. + /// public DriverHealth GetHealth() => _health; - /// Flushes optional caches to free memory. - /// Cancellation token. - /// A task representing the asynchronous operation. + /// public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; /// @@ -379,8 +367,8 @@ public sealed class S7Driver "Timer/Counter are read-only this phase; set Writable=false."); } - // (e) Array tag declared Writable — WriteOneAsync has no WriteArrayAsync path yet - // (Driver.S7-015). Without this guard a writable array node is discovered and + // (e) Array tag declared Writable — WriteOneAsync has no WriteArrayAsync path yet. + // Without this guard a writable array node is discovered and // accepted, then every write returns BadCommunicationError (InvalidCastException from // BoxValueForWrite receiving a typed array) instead of the informative BadNotSupported // the caller could act on. Wide-type arrays are already rejected above (guard-a); this @@ -401,7 +389,7 @@ public sealed class S7Driver /// list — types / /// throw for, which /// would otherwise create live OPC UA nodes via that return - /// BadNotSupported on every access (code-review finding Driver.S7-013). + /// BadNotSupported on every access. /// The set is now empty (Phase 4d unblocked all five wide types), so this rejects /// nothing — kept as the seam / grep target should a type ever need re-gating at init. /// @@ -430,25 +418,20 @@ public sealed class S7Driver /// private static readonly HashSet UnimplementedDataTypes = new(); - /// - /// Approximate memory footprint. The Plc instance + one 240-960 byte PDU buffer is - /// under 4 KB; return 0 because the contract asks for a - /// driver-attributable growth number and S7.Net doesn't expose one. - /// + /// + // The Plc instance + one 240-960 byte PDU buffer is under 4 KB; returns 0 because the + // IDriver contract asks for a driver-attributable growth number and S7.Net doesn't expose one. public long GetMemoryFootprint() => 0; // ---- IReadable ---- - /// Reads values from the specified tag references. - /// Tag references to read. - /// Cancellation token. - /// A task representing the asynchronous operation returning a list of data value snapshots. + /// public async Task> ReadAsync( IReadOnlyList fullReferences, CancellationToken cancellationToken) { // Validate the list before RequirePlc() so a null argument produces an // ArgumentNullException (consistent with DiscoverAsync) rather than an - // InvalidOperationException from the not-initialized check — Driver.S7-003. + // InvalidOperationException from the not-initialized check. ArgumentNullException.ThrowIfNull(fullReferences); var plc = RequirePlc(); var now = DateTime.UtcNow; @@ -480,8 +463,7 @@ public sealed class S7Driver // PUT/GET-disabled (S7-1200/1500) / access-protection — a permanent // configuration fault, NOT a transient one. Blind retry is wasted effort, // so map it to BadNotSupported and flag the driver as a config alert - // (Faulted) rather than Degraded — per driver-specs.md §5 and - // code-review finding Driver.S7-007. + // (Faulted) rather than Degraded — per driver-specs.md §5. results[i] = new DataValueSnapshot(null, StatusBadNotSupported, null, now); _health = new DriverHealth(DriverState.Faulted, _health.LastSuccessfulRead, "S7 access denied — enable PUT/GET communication in TIA Portal " + @@ -900,7 +882,7 @@ public sealed class S7Driver /// unsigned type: bool, byte, ushort, uint) into the /// SEMANTIC type declared by the tag's . No network I/O. /// Factored out of so it can be exercised in unit tests - /// without a live PLC (Driver.S7-014). + /// without a live PLC. /// /// Tag definition containing type information. /// Parsed tag address. @@ -935,15 +917,12 @@ public sealed class S7Driver // ---- IWritable ---- - /// Writes values to the specified tags. - /// Write requests containing tag references and values. - /// Cancellation token. - /// A task representing the asynchronous operation returning a list of write results. + /// public async Task> WriteAsync( IReadOnlyList writes, CancellationToken cancellationToken) { // Same as ReadAsync — validate before RequirePlc() so a null argument is a - // typed argument error, not the "not initialized" surface (Driver.S7-003). + // typed argument error, not the "not initialized" surface. ArgumentNullException.ThrowIfNull(writes); var plc = RequirePlc(); var results = new WriteResult[writes.Count]; @@ -987,7 +966,7 @@ public sealed class S7Driver } catch (OperationCanceledException) { - // Driver.S7-008: let cancellation propagate rather than turning it into + // Let cancellation propagate rather than turning it into // a status code — the gate is still held so Release() runs in finally. throw; } @@ -998,7 +977,7 @@ public sealed class S7Driver catch (PlcException pex) when (IsAccessDenied(pex)) { // PUT/GET-disabled / access-protection on write — same permanent - // configuration fault as on read (Driver.S7-007). BadNotSupported + + // configuration fault as on read. BadNotSupported + // a config-alert health state, not a transient device failure. results[i] = new WriteResult(StatusBadNotSupported); _health = new DriverHealth(DriverState.Faulted, _health.LastSuccessfulRead, @@ -1009,14 +988,14 @@ public sealed class S7Driver { // Genuine device-layer fault — degrade health so a PLC-down-during-writes // scenario is visible to the operator (previously health was never updated - // on write failure — Driver.S7-008). + // on write failure). results[i] = new WriteResult(StatusBadDeviceFailure); _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, pex.Message); } catch (Exception ex) { // Socket/timeout/conversion failure. Map to BadCommunicationError (not - // BadInternalError) for transport faults; degrade health — Driver.S7-008. + // BadInternalError) for transport faults; degrade health. results[i] = new WriteResult(StatusBadCommunicationError); _health = new DriverHealth(DriverState.Degraded, _health.LastSuccessfulRead, ex.Message); } @@ -1028,7 +1007,7 @@ public sealed class S7Driver private async Task WriteOneAsync(Plc plc, S7TagDefinition tag, object? value, CancellationToken ct) { - // Defence-in-depth guard for Driver.S7-015: authored array tags are rejected at init + // Defence-in-depth guard: authored array tags are rejected at init // (RejectUnsupportedTagConfigs guard-e), but a transient equipment-tag ref resolved by // _resolver bypasses that path. Both should fail with NotSupportedException → BadNotSupported, // not InvalidCastException → BadCommunicationError from BoxValueForWrite receiving an array. @@ -1080,7 +1059,7 @@ public sealed class S7Driver /// S7.Net's Plc.WriteAsync expects for each address size (bool → bool, byte /// → byte, short → ushort, int → uint, float → uint-bits). No network I/O. /// Factored out of so it can be exercised in unit tests - /// without a live PLC (Driver.S7-014). + /// without a live PLC. /// /// Target S7 data type. /// Value to box. @@ -1116,7 +1095,7 @@ public sealed class S7Driver /// the response-code validator throws a plain for the S7 /// AccessingObjectNotAllowed status, which lands as the inner exception. There is /// no typed error code for it, so the inner message is the only discriminator - /// S7.Net exposes — see code-review finding Driver.S7-007. + /// S7.Net exposes. /// private static bool IsAccessDenied(PlcException pex) { @@ -1133,17 +1112,13 @@ public sealed class S7Driver // ---- ITagDiscovery ---- - /// - /// Run-once: emits the complete node set synchronously from - /// the configured tag table in a single pass — nothing fills in asynchronously after - /// connect, so a single discovery pass is sufficient. - /// + /// + // Run-once: DiscoverAsync emits the complete node set synchronously from the configured tag + // table in a single pass — nothing fills in asynchronously after connect, so a single + // discovery pass is sufficient. public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; - /// Discovers tags and builds the OPC UA address space. - /// Address space builder. - /// Cancellation token. - /// A task representing the asynchronous operation. + /// public Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(builder); @@ -1171,7 +1146,7 @@ public sealed class S7Driver { S7DataType.Bool => DriverDataType.Boolean, S7DataType.Byte => DriverDataType.Int32, // no 8-bit in DriverDataType yet - // Driver.S7-002: UInt32 values > int.MaxValue (2^31-1) wrap negative when surfaced as + // UInt32 values > int.MaxValue (2^31-1) wrap negative when surfaced as // Int32. That residual lossy note now applies ONLY to UInt32 — Int64/UInt64 map to // their own DriverDataType members below (Phase 4d), so they are no longer lossy. S7DataType.Int16 or S7DataType.UInt16 or S7DataType.Int32 or S7DataType.UInt32 => DriverDataType.Int32, @@ -1186,11 +1161,7 @@ public sealed class S7Driver // ---- ISubscribable (polling overlay) ---- - /// Subscribes to changes on the specified tag references. - /// Tag references to subscribe to. - /// Polling interval. - /// Cancellation token. - /// A task representing the asynchronous operation returning a subscription handle. + /// public Task SubscribeAsync( IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) { @@ -1205,15 +1176,12 @@ public sealed class S7Driver var state = new SubscriptionState(handle, [.. fullReferences], interval, cts); _subscriptions[id] = state; // Track the poll Task so ShutdownAsync can await it after cancelling — a poll - // iteration mid-_gate would otherwise race the semaphore's disposal (Driver.S7-006). + // iteration mid-_gate would otherwise race the semaphore's disposal. state.PollTask = Task.Run(() => PollLoopAsync(state, cts.Token), CancellationToken.None); return Task.FromResult(handle); } - /// Unsubscribes from a subscription. - /// Subscription handle. - /// Cancellation token. - /// A task representing the asynchronous operation. + /// public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) { if (handle is S7SubscriptionHandle h && _subscriptions.TryRemove(h.Id, out var state)) @@ -1227,7 +1195,7 @@ public sealed class S7Driver /// /// Upper bound on the poll-loop backoff window. After enough consecutive failures the /// loop waits this long between retries instead of , - /// so a subscription against a dropped / uninitialised driver doesn't spin (Driver.S7-009). + /// so a subscription against a dropped / uninitialised driver doesn't spin. /// private static readonly TimeSpan PollBackoffCap = TimeSpan.FromSeconds(30); @@ -1235,7 +1203,7 @@ public sealed class S7Driver /// Number of consecutive poll failures before the loop transitions the driver's /// health to . One stray failure can be transient; /// a sustained run indicates the operator should see it. Threshold of 1 because the - /// first failure already lives in the LastError surface — see Driver.S7-009. + /// first failure already lives in the LastError surface. /// private const int PollFailureHealthThreshold = 1; @@ -1283,7 +1251,7 @@ public sealed class S7Driver /// /// Logs the swallowed poll exception and, once /// consecutive failures have accumulated, degrades the driver health so the failure - /// surfaces on the dashboard — see Driver.S7-009. The probe loop owns Running/Stopped + /// surfaces on the dashboard. The probe loop owns Running/Stopped /// transitions for the host-connectivity surface, so we touch /// rather than the probe state. /// @@ -1353,14 +1321,14 @@ public sealed class S7Driver /// /// Handle to this subscription's poll loop. Tracked so - /// can await it after cancelling — see code-review finding Driver.S7-006. + /// can await it after cancelling. /// public Task PollTask { get; set; } = Task.CompletedTask; } private sealed record S7SubscriptionHandle(long Id) : ISubscriptionHandle { - /// Gets the diagnostic identifier for this subscription. + /// public string DiagnosticId => $"s7-sub-{Id}"; } @@ -1373,8 +1341,7 @@ public sealed class S7Driver /// public string HostName => $"{_options.Host}:{_options.Port}"; - /// Gets the host connectivity statuses. - /// A list containing the current host status. + /// public IReadOnlyList GetHostStatuses() { lock (_probeLock) @@ -1433,7 +1400,7 @@ public sealed class S7Driver /// Disposes the driver and releases resources. public void Dispose() { - // Driver.S7-010: avoid the sync-over-async DisposeAsync().AsTask().GetAwaiter().GetResult() + // Avoid the sync-over-async DisposeAsync().AsTask().GetAwaiter().GetResult() // pattern (a known deadlock surface even when currently safe here). ShutdownAsync's // body is effectively synchronous apart from waiting on probe/poll Tasks; do the same // teardown directly, blocking only on the drain — and only with a bounded timeout so @@ -1458,8 +1425,7 @@ public sealed class S7Driver /// /// Synchronous teardown — mirrors but blocks (with a bounded /// timeout) on the probe + poll Tasks instead of awaiting them. Used by the sync - /// path so we don't sync-over-async - /// (Driver.S7-010). + /// path so we don't sync-over-async . /// private void SynchronousTeardown() { diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverFactoryExtensions.cs index 0ca03032..16b2f45f 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.S7/S7DriverFactoryExtensions.cs @@ -6,9 +6,9 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.S7; /// /// Static factory registration helper for . Server's Program.cs -/// calls once at startup; the bootstrapper (task #248) then -/// materialises S7 DriverInstance rows from the central config DB into live driver -/// instances. Mirrors GalaxyProxyDriverFactoryExtensions. +/// calls once at startup; the bootstrapper then materialises S7 +/// DriverInstance rows from the central config DB into live driver instances. Mirrors +/// GalaxyProxyDriverFactoryExtensions. /// public static class S7DriverFactoryExtensions { @@ -37,7 +37,7 @@ public static class S7DriverFactoryExtensions /// Parse a driver-config JSON document into a strongly-typed . /// Shared by the factory (instance creation) and by /// / so a config change delivered through the - /// IDriver contract is actually applied — see code-review finding Driver.S7-011. + /// IDriver contract is actually applied. /// /// The unique identifier for the driver instance. /// The JSON configuration for the driver. @@ -72,7 +72,7 @@ public static class S7DriverFactoryExtensions Enabled = dto.Probe?.Enabled ?? true, Interval = TimeSpan.FromMilliseconds(dto.Probe?.IntervalMs ?? 5_000), Timeout = TimeSpan.FromMilliseconds(dto.Probe?.TimeoutMs ?? 2_000), - // Driver.S7-012: ProbeAddress removed — probe uses ReadStatusAsync, not a tag read. + // ProbeAddress removed — probe uses ReadStatusAsync, not a tag read. }, }; } @@ -159,8 +159,8 @@ public static class S7DriverFactoryExtensions public int? IntervalMs { get; init; } /// Gets the probe timeout in milliseconds. public int? TimeoutMs { get; init; } - // Driver.S7-012: ProbeAddress removed from the configurable surface — the probe uses - // ReadStatusAsync (CPU status), not a tag-address read. Config documents that previously - // set probeAddress are safely ignored (unknown JSON fields are tolerated by the deserialiser). + // ProbeAddress removed from the configurable surface — the probe uses ReadStatusAsync + // (CPU status), not a tag-address read. Config documents that previously set + // probeAddress are safely ignored (unknown JSON fields are tolerated by the deserialiser). } } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/TwinCATDataType.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/TwinCATDataType.cs index ee2b0290..31c79615 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/TwinCATDataType.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/TwinCATDataType.cs @@ -20,11 +20,10 @@ public enum TwinCATDataType String, // ASCII string WString,// UTF-16 string // IEC 61131-3 / TwinCAT temporal types — all stored on the wire as 32-bit unsigned (UDINT) - // raw counters. The driver surfaces them as DriverDataType.UInt32 (Driver.TwinCAT-002), so + // raw counters. The driver surfaces them as DriverDataType.UInt32, so // operators see the raw counter, not a decoded date/duration. Proper decoding to // DriverDataType.DateTime is a future enhancement; until then comments accurately describe - // the on-wire encoding so the next implementer doesn't re-derive it wrong - // (Driver.TwinCAT-004). + // the on-wire encoding so the next implementer doesn't re-derive it wrong. Time, // TIME — duration in milliseconds, stored as UDINT (32-bit unsigned) Date, // DATE — seconds since 1970-01-01 truncated to a day boundary, stored as UDINT DateTime, // DT (DATE_AND_TIME) — seconds since 1970-01-01, stored as UDINT diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/TwinCATDriverOptions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/TwinCATDriverOptions.cs index dbd33435..ec6a6a37 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/TwinCATDriverOptions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.Contracts/TwinCATDriverOptions.cs @@ -45,7 +45,7 @@ public sealed class TwinCATDriverOptions /// up to this delay before pushing them. 0 (default) = no batching, push /// immediately. Useful for high-churn signals where the OPC UA subscriber tolerates a /// small delay in exchange for fewer wire round-trips. Listed in docs/v2/driver-specs.md - /// section 6 — was previously hard-coded to 0 (Driver.TwinCAT-014). + /// section 6 — was previously hard-coded to 0. /// public int NotificationMaxDelayMs { get; init; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs index 16ca0f5f..385b37e6 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/AdsTwinCATClient.cs @@ -22,10 +22,6 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; /// internal sealed class AdsTwinCATClient : ITwinCATClient { - // Bounded so a slow downstream consumer cannot back the AMS router thread up — the - // router thread enqueues and returns immediately (Driver.TwinCAT-008). 50k matches the - // Galaxy EventPump default; ~500 notifications/connection is the ADS ceiling so this is - // generous headroom against bursty change storms. private const int NotificationQueueCapacity = 50_000; private readonly AdsClient _client = new(); @@ -57,9 +53,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient private readonly record struct PendingNotification(NotificationRegistration Registration, object? Value); - /// - /// Gets a value indicating whether the client is connected. - /// + /// public bool IsConnected => _client.IsConnected; /// @@ -75,13 +69,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient return TwinCATStatusMapper.MapAdsError(adsError); } - /// - /// Connects to the specified ADS target asynchronously. - /// - /// The AMS address to connect to. - /// The connection timeout. - /// The cancellation token. - /// A task representing the asynchronous connection operation. + /// public Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken) { if (_client.IsConnected) return Task.CompletedTask; @@ -91,16 +79,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient return Task.CompletedTask; } - /// - /// Reads a value from the specified symbol path asynchronously. - /// - /// The ADS symbol path to read from. - /// The TwinCAT data type. - /// Optional bit index for BOOL values within larger containers. - /// When non-null, read a 1-D array of this many - /// elements; the boxed value is the element-typed CLR array (e.g. int[]). Phase 4c. - /// The cancellation token. - /// A tuple containing the value and OPC UA status code. + /// public async Task<(object? value, uint status)> ReadValueAsync( string symbolPath, TwinCATDataType type, @@ -170,17 +149,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient : symbolPath; } - /// - /// Writes a value to the specified symbol path asynchronously. - /// - /// The ADS symbol path to write to. - /// The TwinCAT data type. - /// Optional bit index for BOOL values. BOOL-within-word writes are handled - /// upstream by as a parent-word read-modify-write, so a - /// bit index does not reach this method on the write path. - /// The value to write. - /// The cancellation token. - /// The OPC UA status code of the write operation. + /// public async Task WriteValueAsync( string symbolPath, TwinCATDataType type, @@ -203,11 +172,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient } } - /// - /// Probes the connection to verify the ADS target is reachable. - /// - /// The cancellation token. - /// True if the probe succeeds; otherwise false. + /// public async Task ProbeAsync(CancellationToken cancellationToken) { try @@ -221,17 +186,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient } } - /// - /// Adds a notification (subscription) for changes to the specified symbol. - /// - /// The ADS symbol path to monitor. - /// The TwinCAT data type. - /// Optional bit index for BOOL values. - /// The minimum cycle time between notifications. - /// The maximum delay before delivering a batched notification. - /// The callback to invoke when the value changes. - /// The cancellation token. - /// A handle to manage the notification subscription. + /// public async Task AddNotificationAsync( string symbolPath, TwinCATDataType type, @@ -242,13 +197,6 @@ internal sealed class AdsTwinCATClient : ITwinCATClient CancellationToken cancellationToken) { var clrType = MapToClrType(type); - // NotificationSettings takes cycle + max-delay in milliseconds (Beckhoff InfoSys - // tcadsnetref/7313319051 — "The unit is 1ms"). AdsTransMode.OnChange fires when - // the value differs; OnCycle fires every cycle. OnChange is the right default for - // OPC UA data-change semantics — the PLC already has the best view of "has this - // changed" so we let it decide. maxDelayMs > 0 lets TwinCAT batch notifications up - // to that delay before pushing them — exposed via TwinCATDriverOptions - // (Driver.TwinCAT-014). var cycleMs = (int)Math.Max(1, cycleTime.TotalMilliseconds); var settings = new NotificationSettings(AdsTransMode.OnChange, cycleMs, Math.Max(0, maxDelayMs)); @@ -270,8 +218,8 @@ internal sealed class AdsTwinCATClient : ITwinCATClient /// /// Runs on the AMS router thread. Does the cheap bit-extraction /// decode then enqueues — no driver logic, no consumer callbacks — so a slow consumer - /// can never stall ADS notification delivery for the rest of the process - /// (Driver.TwinCAT-008). Drops the notification (DropWrite) if the queue is saturated. + /// can never stall ADS notification delivery for the rest of the process. + /// Drops the notification (DropWrite) if the queue is saturated. /// private void OnAdsNotificationEx(object? sender, AdsNotificationExEventArgs args) { @@ -311,11 +259,7 @@ internal sealed class AdsTwinCATClient : ITwinCATClient catch { /* best-effort tear-down; target may already be gone */ } } - /// - /// Browses all available symbols on the connected ADS target asynchronously. - /// - /// The cancellation token. - /// An async enumerable of discovered symbols. + /// public async IAsyncEnumerable BrowseSymbolsAsync( [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -341,11 +285,6 @@ internal sealed class AdsTwinCATClient : ITwinCATClient foreach (var ds in TwinCATSymbolExpander.ExpandLeaves( loader.Symbols.Select(s => new AdsSymbolNode(s)))) { - // ThrowIfCancellationRequested — not yield break — so a cancelled browse propagates - // as OperationCanceledException rather than a silent clean completion. DiscoverAsync - // has an explicit catch(OperationCanceledException){ throw; } to surface this - // distinctly from a genuine browse failure; a yield break would let a partial - // symbol set appear as a fully successful discovery (Driver.TwinCAT-010). cancellationToken.ThrowIfCancellationRequested(); yield return ds; } @@ -368,6 +307,11 @@ internal sealed class AdsTwinCATClient : ITwinCATClient private readonly IReadOnlyList _children; private readonly bool _readOnly; + /// + /// Initializes a new instance of the class, computing its + /// struct/mapped-type/children/read-only state eagerly from the wrapped . + /// + /// The Beckhoff ADS symbol to adapt. public AdsSymbolNode(ISymbol symbol) { _symbol = symbol; @@ -379,14 +323,19 @@ internal sealed class AdsTwinCATClient : ITwinCATClient _readOnly = !IsSymbolWritable(symbol); } + /// public string InstancePath => _symbol.InstancePath; + /// public bool IsStruct => _isStruct; + /// public (TwinCATDataType? Type, int? ArrayLength) Mapped => _mapped; + /// public IReadOnlyList Children => _children; + /// public bool ReadOnly => _readOnly; } @@ -577,9 +526,6 @@ internal sealed class AdsTwinCATClient : ITwinCATClient /// Default — one per call. internal sealed class AdsTwinCATClientFactory : ITwinCATClientFactory { - /// - /// Creates a new instance. - /// - /// A new instance. + /// public ITwinCATClient Create() => new AdsTwinCATClient(); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ITwinCATClient.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ITwinCATClient.cs index 03c59232..0e992dfe 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ITwinCATClient.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/ITwinCATClient.cs @@ -17,6 +17,7 @@ public interface ITwinCATClient : IDisposable /// The target AMS address. /// The connection timeout. /// Cancellation token for the connection attempt. + /// A task that represents the asynchronous operation. Task ConnectAsync(TwinCATAmsAddress address, TimeSpan timeout, CancellationToken cancellationToken); /// True when the AMS router + target both accept commands. @@ -28,7 +29,7 @@ public interface ITwinCATClient : IDisposable /// 1809 / 0x0711) on any read / write / notification — the signal that a PLC program /// re-download has invalidated every symbol + notification handle. The driver forwards /// this to so Core - /// rebuilds the address space subtree (docs/v2/driver-specs.md §6, Driver.TwinCAT-013). + /// rebuilds the address space subtree (docs/v2/driver-specs.md §6). /// event EventHandler? OnSymbolVersionChanged; @@ -44,6 +45,7 @@ public interface ITwinCATClient : IDisposable /// elements; the boxed value is the element-typed CLR array (int[] / float[] / /// bool[] / string[] / …). When null, read a scalar (Phase 4c). /// Cancellation token for the read operation. + /// The boxed value read (or null) together with the mapped OPC UA status. Task<(object? value, uint status)> ReadValueAsync( string symbolPath, TwinCATDataType type, @@ -60,6 +62,7 @@ public interface ITwinCATClient : IDisposable /// Optional bit index for bit manipulation within a word. /// The value to write. /// Cancellation token for the write operation. + /// The mapped OPC UA status for the write (0 = Good). Task WriteValueAsync( string symbolPath, TwinCATDataType type, @@ -72,6 +75,7 @@ public interface ITwinCATClient : IDisposable /// Used by 's probe loop. /// /// Cancellation token for the probe operation. + /// true when the target is reachable; otherwise false. Task ProbeAsync(CancellationToken cancellationToken); /// @@ -86,9 +90,10 @@ public interface ITwinCATClient : IDisposable /// Minimum interval between change notifications (native-floor depends on target). /// Maximum batching delay in milliseconds — TwinCAT may coalesce /// notifications up to this delay before pushing them. 0 = no batching, push - /// immediately (Driver.TwinCAT-014). + /// immediately. /// Invoked with (symbolPath, boxedValue) per notification. /// Cancels the initial registration; does not tear down an established notification. + /// A handle whose disposal tears the registered notification down. Task AddNotificationAsync( string symbolPath, TwinCATDataType type, @@ -109,6 +114,7 @@ public interface ITwinCATClient : IDisposable /// and never see struct-typed entries — expansion is performed here, not in the caller. /// /// Cancellation token for the enumeration operation. + /// An asynchronous stream of the target's atomic-leaf symbols. IAsyncEnumerable BrowseSymbolsAsync(CancellationToken cancellationToken); } @@ -138,5 +144,6 @@ public sealed record TwinCATDiscoveredSymbol( public interface ITwinCATClientFactory { /// Creates a new TwinCAT client instance. + /// A new, not-yet-connected . ITwinCATClient Create(); } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs index cc5ed79d..9a85b14e 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriver.cs @@ -13,16 +13,15 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT; public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable, IHostConnectivityProbe, IPerCallHostResolver, IRediscoverable, IDisposable, IAsyncDisposable { - // Mutable so ReinitializeAsync can apply a new config generation (Driver.TwinCAT-001). - // The constructor seeds it; InitializeAsync re-parses driverConfigJson over the top of it. + // Mutable so ReinitializeAsync can apply a new config generation. The constructor seeds + // it; InitializeAsync re-parses driverConfigJson over the top of it. private TwinCATDriverOptions _options; private readonly string _driverInstanceId; private readonly ITwinCATClientFactory _clientFactory; private readonly ILogger _logger; private readonly PollGroupEngine _poll; // ConcurrentDictionary so ShutdownAsync (Clear) and ReadAsync/WriteAsync/SubscribeAsync - // (TryGetValue) don't race — plain Dictionary is not safe for concurrent read+write - // (Driver.TwinCAT-009). + // (TryGetValue) don't race — plain Dictionary is not safe for concurrent read+write. private readonly ConcurrentDictionary _devices = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary _tagsByName = @@ -73,23 +72,20 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot))); } - /// Gets the unique driver instance identifier. + /// public string DriverInstanceId => _driverInstanceId; - /// Gets the driver type name. + /// public string DriverType => "TwinCAT"; - /// Initializes the driver with configuration and establishes device connections. - /// JSON configuration string for the driver. - /// Cancellation token. - /// Completion task. + /// public Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { _health = new DriverHealth(DriverState.Initializing, null, null); try { - // Apply the supplied config generation (Driver.TwinCAT-001). A blank or content-free - // document keeps the constructor-seeded options — that path covers callers that have - // already materialised options up front (the factory passes both, in agreement). + // Apply the supplied config generation. A blank or content-free document keeps the + // constructor-seeded options — that path covers callers that have already + // materialised options up front (the factory passes both, in agreement). if (!string.IsNullOrWhiteSpace(driverConfigJson)) { var parsed = TwinCATDriverFactoryExtensions.ParseOptions(driverConfigJson, _driverInstanceId); @@ -112,7 +108,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery { state.ProbeCts = new CancellationTokenSource(); var ct = state.ProbeCts.Token; - // Store the task so ShutdownAsync can await it (Driver.TwinCAT-009). + // Store the task so ShutdownAsync can await it. state.ProbeTask = Task.Run(() => ProbeLoopAsync(state, ct), ct); } } @@ -126,19 +122,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery return Task.CompletedTask; } - /// Reinitializes the driver by shutting down and reinitializing with new configuration. - /// JSON configuration string for the driver. - /// Cancellation token. - /// Completion task. + /// public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) { await ShutdownAsync(cancellationToken).ConfigureAwait(false); await InitializeAsync(driverConfigJson, cancellationToken).ConfigureAwait(false); } - /// Shuts down the driver and releases all device connections and subscriptions. - /// Cancellation token. - /// Completion task. + /// public async Task ShutdownAsync(CancellationToken cancellationToken) { // Native subs first — disposing the handles is cheap + lets the client close its @@ -150,8 +141,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery await _poll.DisposeAsync().ConfigureAwait(false); // Cancel every probe loop and await its task before disposing the client + gate so the - // loop can never touch a disposed object. (Driver.TwinCAT-009: ShutdownAsync previously - // cancelled ProbeCts but did not await the task before calling DisposeClient.) + // loop can never touch a disposed object. foreach (var state in _devices.Values) { try { state.ProbeCts?.Cancel(); } catch { } @@ -176,27 +166,20 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery _health = new DriverHealth(DriverState.Unknown, _health.LastSuccessfulRead, null); } - /// Gets the current driver health status. - /// Driver health information. + /// public DriverHealth GetHealth() => _health; - /// - /// Estimated bytes attributable to this driver instance (Driver.TwinCAT-012). - /// This driver holds no flushable symbol cache — BrowseSymbolsAsync streams and - /// discards; the footprint reflects live allocations only: - /// ~256 bytes per pre-declared tag (tag-definition record + dictionary overhead) and - /// ~512 bytes per active native subscription. - /// + /// + // This driver holds no flushable symbol cache — BrowseSymbolsAsync streams and discards; the + // footprint reflects live allocations only: ~256 bytes per pre-declared tag (tag-definition + // record + dictionary overhead) and ~512 bytes per active native subscription. public long GetMemoryFootprint() => (_tagsByName.Count * 256L) + (_nativeSubs.Count * 512L); - /// - /// No flushable cache exists in this driver — the symbol table is streamed fresh on - /// every call. This is a no-op but is deliberately present - /// so Core's cache-budget enforcement sees a compliant Tier-A driver. - /// - /// Cancellation token. - /// Completion task. + /// + // No flushable cache exists in this driver — the symbol table is streamed fresh on every + // DiscoverAsync call. This is a no-op but is deliberately present so Core's cache-budget + // enforcement sees a compliant Tier-A driver. public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) => Task.CompletedTask; /// Gets the count of configured devices. @@ -209,10 +192,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery // ---- IReadable ---- - /// Reads values for the specified tag references from ADS devices. - /// The full tag references to read. - /// Cancellation token. - /// Data value snapshots for each reference. + /// public async Task> ReadAsync( IReadOnlyList fullReferences, CancellationToken cancellationToken) { @@ -272,10 +252,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery // ---- IWritable ---- - /// Writes values to the specified tags on ADS devices. - /// The write requests to execute. - /// Cancellation token. - /// Write results for each request. + /// public async Task> WriteAsync( IReadOnlyList writes, CancellationToken cancellationToken) { @@ -377,18 +354,14 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery // ---- ITagDiscovery ---- - /// - /// Run-once: emits pre-declared tags and (when - /// EnableControllerBrowse is set) fully awaits the controller symbol browse within the - /// single call, streaming the complete node set in one pass — nothing fills in - /// asynchronously after connect, so a single discovery pass is sufficient. - /// + /// + // Run-once: DiscoverAsync emits pre-declared tags and (when EnableControllerBrowse is set) + // fully awaits the controller symbol browse within the single call, streaming the complete + // node set in one pass — nothing fills in asynchronously after connect, so a single + // discovery pass is sufficient. public DiscoveryRediscoverPolicy RediscoverPolicy => DiscoveryRediscoverPolicy.Once; - /// Discovers devices and tags from ADS configuration and optionally controller symbols. - /// Address space builder for adding discovered nodes. - /// Cancellation token. - /// Completion task. + /// public async Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(builder); @@ -468,17 +441,11 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery private readonly ConcurrentDictionary _nativeSubs = new(); private long _nextNativeSubId; - /// - /// Subscribe via native ADS notifications when - /// is true, otherwise fall through to the shared . - /// Native path registers one per tag against the - /// target's PLC runtime — the PLC pushes changes on its own cycle so we skip the poll - /// loop entirely. Unsub path disposes the handles. - /// - /// The full tag references to subscribe to. - /// The publishing interval for updates. - /// Cancellation token. - /// Subscription handle for managing the subscription. + /// + // Subscribe via native ADS notifications when TwinCATDriverOptions.UseNativeNotifications is + // true, otherwise fall through to the shared PollGroupEngine. Native path registers one + // ITwinCATNotificationHandle per tag against the target's PLC runtime — the PLC pushes + // changes on its own cycle so we skip the poll loop entirely. Unsub path disposes the handles. public async Task SubscribeAsync( IReadOnlyList fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) { @@ -529,10 +496,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery return handle; } - /// Unsubscribes from a native or poll-based subscription. - /// The subscription handle to unsubscribe. - /// Cancellation token. - /// Completion task. + /// public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) { if (handle is NativeSubscriptionHandle native && _nativeSubs.TryRemove(native.Id, out var sub)) @@ -546,7 +510,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery private sealed record NativeSubscriptionHandle(long Id) : ISubscriptionHandle { - /// Gets the diagnostic identifier for the subscription. + /// public string DiagnosticId => $"twincat-native-sub-{Id}"; } @@ -556,8 +520,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery // ---- IHostConnectivityProbe ---- - /// Gets the connectivity status for all configured devices. - /// List of host connectivity statuses. + /// public IReadOnlyList GetHostStatuses() => [.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))]; @@ -569,7 +532,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery try { // Probe-initiated connects honor TwinCATProbeOptions.Timeout — distinct from - // the driver-wide _options.Timeout used by reads/writes (Driver.TwinCAT-014). + // the driver-wide _options.Timeout used by reads/writes. var client = await EnsureConnectedAsync(state, ct, _options.Probe.Timeout) .ConfigureAwait(false); success = await client.ProbeAsync(ct).ConfigureAwait(false); @@ -614,26 +577,23 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery /// host is an ads://… URI), so it cleanly signals "unresolved" without colliding /// with a real host key. Used to be , which is a logical /// config-DB identifier — that collided with consumers who expected the resolver and the - /// connectivity-status table to share keys (Driver.TwinCAT-006). + /// connectivity-status table to share keys. /// public const string UnresolvedHostSentinel = ""; - /// Resolves the device host address for the specified tag reference. - /// The full tag reference. - /// The host address or if not found. + /// public string ResolveHost(string fullReference) { if (_tagsByName.TryGetValue(fullReference, out var def)) return def.DeviceHostAddress; // First device's HostAddress when one exists; otherwise the unresolved sentinel — - // intentionally NOT DriverInstanceId, which is a config-DB key, not a host address - // (Driver.TwinCAT-006). + // intentionally NOT DriverInstanceId, which is a config-DB key, not a host address. return _options.Devices.FirstOrDefault()?.HostAddress ?? UnresolvedHostSentinel; } /// /// Lazily connect a device's client, serialized per device by - /// (Driver.TwinCAT-007). Without the gate, a + /// . Without the gate, a /// concurrent read / write / probe could each create + connect a separate client and /// leak all-but-one, or dispose a client another thread is mid-connect on. The S7 and /// AB-CIP drivers serialize device access the same way; single-connection-per-PLC is @@ -661,8 +621,8 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery var client = _clientFactory.Create(); client.OnSymbolVersionChanged += HandleSymbolVersionChanged; // timeoutOverride lets the probe loop use TwinCATProbeOptions.Timeout for probe- - // initiated connects rather than the driver-level _options.Timeout - // (Driver.TwinCAT-014). Reads / writes pass null and get the driver default. + // initiated connects rather than the driver-level _options.Timeout. Reads / writes + // pass null and get the driver default. var effectiveTimeout = timeoutOverride ?? _options.Timeout; try { @@ -692,7 +652,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery /// /// Routes a wire-detected ADS symbol-version-changed (DeviceSymbolVersionInvalid 1809 / - /// 0x0711) to Core as an invocation (Driver.TwinCAT-013). + /// 0x0711) to Core as an invocation. /// A PLC re-download invalidates every symbol + notification handle, so the address /// space must be rebuilt — this is the documented TwinCAT failure mode, not a transient /// connection error. @@ -705,8 +665,8 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery /// /// Synchronous teardown — no await, no captured sync context. The OPC UA stack /// thread can call ; routing through DisposeAsync().GetResult() - /// can deadlock on a single-threaded sync context (Driver.TwinCAT-015, - /// docs/v2/driver-stability.md). The operations here are all genuinely synchronous — + /// can deadlock on a single-threaded sync context (see docs/v2/driver-stability.md). + /// The operations here are all genuinely synchronous — /// cancel tokens, wait on task handles with a hard timeout, dispose clients — so a /// synchronous path does the right thing without re-entering the scheduler. /// @@ -759,7 +719,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery public ITwinCATClient? Client { get; set; } /// Serializes connect / reconnect so concurrent callers never race a client - /// create-or-dispose for this device (Driver.TwinCAT-007). + /// create-or-dispose for this device. public SemaphoreSlim ConnectGate { get; } = new(1, 1); /// Gets the lock object for synchronizing host state transitions. @@ -771,7 +731,7 @@ public sealed class TwinCATDriver : IDriver, IReadable, IWritable, ITagDiscovery /// Gets or sets the cancellation token source for the probe loop. public CancellationTokenSource? ProbeCts { get; set; } /// The running probe-loop task — awaited by - /// so the loop cannot touch a disposed client (Driver.TwinCAT-009). + /// so the loop cannot touch a disposed client. public Task? ProbeTask { get; set; } /// Disposes the active ADS client if any. diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriverFactoryExtensions.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriverFactoryExtensions.cs index 53c9ef48..a628087e 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriverFactoryExtensions.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATDriverFactoryExtensions.cs @@ -24,6 +24,7 @@ public static class TwinCATDriverFactoryExtensions /// Creates a TwinCAT driver instance from the provided configuration. /// The driver instance identifier. /// The driver configuration as JSON. + /// The constructed instance. internal static TwinCATDriver CreateInstance(string driverInstanceId, string driverConfigJson) { ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); @@ -35,10 +36,11 @@ public static class TwinCATDriverFactoryExtensions /// Parse a TwinCAT driver-config JSON document into a . /// Shared by (constructor-time) and /// / - /// so a config generation pushed via Reinitialize is actually applied (Driver.TwinCAT-001). + /// so a config generation pushed via Reinitialize is actually applied. /// /// The JSON configuration string. /// The driver instance identifier. + /// The parsed . internal static TwinCATDriverOptions ParseOptions(string driverConfigJson, string driverInstanceId) { ArgumentException.ThrowIfNullOrWhiteSpace(driverConfigJson); @@ -79,6 +81,7 @@ public static class TwinCATDriverFactoryExtensions /// /// The JSON configuration string. /// The driver instance identifier. + /// The parsed . public static TwinCATDriverOptions ParseOptionsForTests(string driverConfigJson, string driverInstanceId) => ParseOptions(driverConfigJson, driverInstanceId); @@ -86,13 +89,6 @@ public static class TwinCATDriverFactoryExtensions { var dataType = ParseEnum(t.DataType, t.Name, driverInstanceId, "DataType"); - // Driver.TwinCAT-003: Structure-typed pre-declared tags are not supported. The driver's - // atomic surface cannot read/write UDT blobs — MapToClrType falls through to typeof(int) - // and ConvertForWrite throws NotSupportedException, producing garbage reads or late - // runtime failures. BrowseSymbolsAsync expands a Structure/UDT/FB symbol into its atomic - // member leaves (the container itself is never surfaced as a writable Structure node), so the - // supported way to address members is discovery — not a pre-declared Structure tag. Reject here - // with a clear error so operators get a configuration-time failure, not a silent wrong value. if (dataType == TwinCATDataType.Structure) throw new InvalidOperationException( $"TwinCAT tag '{t.Name ?? ""}' in '{driverInstanceId}' specifies " + @@ -110,9 +106,6 @@ public static class TwinCATDriverFactoryExtensions DataType: dataType, Writable: t.Writable ?? true, WriteIdempotent: t.WriteIdempotent ?? false, - // Driver.TwinCAT-017: thread arrayLength through so JSON-authored pre-declared tags can - // declare 1-D arrays (Phase 4c). Only positive values are honoured; null / non-positive - // keeps the scalar default (ArrayLength: null on TwinCATTagDefinition). ArrayLength: t.ArrayLength is > 0 ? t.ArrayLength : null); } @@ -192,7 +185,7 @@ public static class TwinCATDriverFactoryExtensions /// /// Optional 1-D array element count. When positive, the tag is a 1-D array of this /// many elements — drives IsArray/ArrayDim at - /// discovery and a native ADS array read at runtime (Phase 4c, Driver.TwinCAT-017). + /// discovery and a native ADS array read at runtime (Phase 4c). /// null or non-positive = scalar (the default). /// public int? ArrayLength { get; init; } diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATStatusMapper.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATStatusMapper.cs index 4c479b85..2ec92749 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATStatusMapper.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATStatusMapper.cs @@ -30,11 +30,11 @@ public static class TwinCATStatusMapper /// Raised by the runtime after a PLC program re-download: every symbol handle and /// notification handle the driver holds is now stale. The driver treats this as an /// trigger, not a connection error - /// (docs/v2/driver-specs.md §6, Driver.TwinCAT-013). + /// (docs/v2/driver-specs.md §6). /// /// Note: legacy Beckhoff InfoSys documentation sometimes cites this as "0x0702"; that /// is a transcription error — 0x0702 is DeviceInvalidGroup (1794). The SDK enum - /// value 1809 (0x0711) is authoritative (Driver.TwinCAT-011). + /// value 1809 (0x0711) is authoritative. /// /// public const uint AdsSymbolVersionChanged = 1809u; // DeviceSymbolVersionInvalid = 0x0711 diff --git a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSystemSymbolFilter.cs b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSystemSymbolFilter.cs index 7db22b59..7aed3914 100644 --- a/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSystemSymbolFilter.cs +++ b/src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.TwinCAT/TwinCATSystemSymbolFilter.cs @@ -10,6 +10,7 @@ public static class TwinCATSystemSymbolFilter { /// Gets a value indicating whether a symbol is a system or infrastructure symbol. /// The symbol instance path to check. + /// if the symbol is a system/infrastructure symbol that should be filtered out; otherwise . public static bool IsSystemSymbol(string instancePath) { if (string.IsNullOrWhiteSpace(instancePath)) return true; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/BrowseSessionReaper.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/BrowseSessionReaper.cs index cffe77a6..251d9df8 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/BrowseSessionReaper.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/BrowseSessionReaper.cs @@ -38,6 +38,8 @@ public sealed class BrowseSessionReaper( /// Evicts every session whose /// is older than . Internal so tests can drive a tick directly. + /// Cancellation token for the reap pass. + /// A task that represents the asynchronous operation. internal async Task ReapOnceAsync(CancellationToken ct) { var now = DateTime.UtcNow; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/BrowseSessionRegistry.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/BrowseSessionRegistry.cs index ca6feee9..d92a4a2b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/BrowseSessionRegistry.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/BrowseSessionRegistry.cs @@ -14,17 +14,25 @@ public sealed class BrowseSessionRegistry private readonly ConcurrentDictionary _sessions = new(); /// Adds (or replaces) a session in the registry keyed by its token. + /// The session to register. public void Register(IBrowseSession session) => _sessions[session.Token] = session; /// Looks up a session by token without removing it. + /// The token identifying the session to look up. + /// The found session, if any. + /// true if a session with the given token was found. public bool TryGet(Guid token, out IBrowseSession session) => _sessions.TryGetValue(token, out session!); /// Atomically removes a session from the registry, returning it for disposal. + /// The token identifying the session to remove. + /// The removed session, if found. + /// true if a session with the given token was found and removed. public bool TryRemove(Guid token, out IBrowseSession session) => _sessions.TryRemove(token, out session!); /// Returns a point-in-time snapshot of all currently registered sessions. + /// The registered sessions and their tokens at the time of the call. public IReadOnlyList<(Guid Token, IBrowseSession Session)> Snapshot() => _sessions.Select(kv => (kv.Key, kv.Value)).ToList(); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/IBrowserSessionService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/IBrowserSessionService.cs index b7b171bd..45886b89 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/IBrowserSessionService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Browsing/IBrowserSessionService.cs @@ -21,21 +21,38 @@ public interface IBrowserSessionService { /// Opens a session against the named driver type using the given JSON config. /// Never throws — all errors are surfaced via . + /// The driver type to open a browse session against. + /// The driver configuration as JSON. + /// Cancellation token for the operation. + /// The result of opening the session, including the registry token on success. Task OpenAsync(string driverType, string configJson, CancellationToken ct); /// Returns the root nodes of an open session. Throws /// if the token is unknown. + /// Registry handle for the open browse session. + /// Cancellation token for the operation. + /// The top-level browse nodes. Task> RootAsync(Guid token, CancellationToken ct); /// Returns the direct children of in an open session. /// Throws if the token is unknown. + /// Registry handle for the open browse session. + /// The node whose direct children are returned. + /// Cancellation token for the operation. + /// The direct children of the node. Task> ExpandAsync(Guid token, string nodeId, CancellationToken ct); /// Returns the attributes of in an open session. Throws /// if the token is unknown. + /// Registry handle for the open browse session. + /// The node whose attributes are returned. + /// Cancellation token for the operation. + /// The attributes of the node. Task> AttributesAsync(Guid token, string nodeId, CancellationToken ct); /// Removes the session from the registry and disposes it. No-op for unknown tokens. + /// Registry handle for the browse session to close. + /// A task that represents the asynchronous operation. Task CloseAsync(Guid token); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Certificates/CertificateStoreManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Certificates/CertificateStoreManager.cs index 062b0584..23b156c1 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Certificates/CertificateStoreManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Certificates/CertificateStoreManager.cs @@ -10,8 +10,11 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Certificates; public sealed record CertActionResult(bool Success, string? Error) { /// A successful result. + /// A indicating success. public static CertActionResult Ok() => new(true, null); /// A failed result carrying the given error message. + /// The friendly error message describing the failure. + /// A indicating failure with . public static CertActionResult Fail(string error) => new(false, error); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminOperationsClient.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminOperationsClient.cs index c2a29096..c33d1d62 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminOperationsClient.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminOperationsClient.cs @@ -25,10 +25,7 @@ public sealed class AdminOperationsClient : IAdminOperationsClient _proxy = registry.Get(); } - /// Starts a deployment via the admin operations actor. - /// The username of who initiated the deployment. - /// The cancellation token. - /// The deployment start result. + /// public async Task StartDeploymentAsync(string createdBy, CancellationToken ct) { var msg = new StartDeployment(createdBy, CorrelationId.NewId()); @@ -37,12 +34,7 @@ public sealed class AdminOperationsClient : IAdminOperationsClient return await _proxy.Ask(msg, AskTimeout, linked.Token); } - /// Acknowledges one alarm via the admin singleton. - /// The alarm's ScriptedAlarmId. - /// The acting operator's name. - /// Optional free-text comment; null when none. - /// The cancellation token. - /// The acknowledge result. + /// public async Task AcknowledgeAlarmAsync( string alarmId, string user, string? comment, CancellationToken ct) { @@ -52,14 +44,7 @@ public sealed class AdminOperationsClient : IAdminOperationsClient return await _proxy.Ask(msg, AskTimeout, linked.Token); } - /// Shelves or unshelves one alarm via the admin singleton. - /// The alarm's ScriptedAlarmId. - /// The acting operator's name. - /// Which shelve action to perform. - /// For a timed shelve, when it expires; null otherwise. - /// Optional free-text comment; null when none. - /// The cancellation token. - /// The shelve result. + /// public async Task ShelveAlarmAsync( string alarmId, string user, ShelveKind kind, DateTime? unshelveAtUtc, string? comment, CancellationToken ct) { @@ -69,11 +54,7 @@ public sealed class AdminOperationsClient : IAdminOperationsClient return await _proxy.Ask(msg, AskTimeout, linked.Token); } - /// - /// Generic Ask — forwards any message to the AdminOperationsActor singleton proxy. - /// Uses the caller-supplied for cancellation; does not impose an - /// additional internal timeout beyond what the proxy itself enforces. - /// + /// public Task AskAsync(object message, CancellationToken ct) => _proxy.Ask(message, cancellationToken: ct); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminProbeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminProbeService.cs index 49415b66..7db8de61 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminProbeService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/AdminProbeService.cs @@ -25,6 +25,7 @@ public sealed class AdminProbeService /// Driver config as JSON (same shape as DriverInstance.DriverConfig). /// Per-probe timeout; actor clamps to [1, 60]. /// Optional cancellation token from the caller. + /// The probe result, or a timeout failure if no reply arrives in time. public async Task TestAsync( string driverType, string configJson, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/FleetDiagnosticsClient.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/FleetDiagnosticsClient.cs index 1780d899..81a0b6cc 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/FleetDiagnosticsClient.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/FleetDiagnosticsClient.cs @@ -31,10 +31,7 @@ public sealed class FleetDiagnosticsClient : IFleetDiagnosticsClient _systemName = options.Value.SystemName; } - /// Gets diagnostics for a cluster node. - /// The node identifier to query. - /// Cancellation token. - /// Diagnostics snapshot for the node, or an empty snapshot if the query fails. + /// public async Task GetDiagnosticsAsync(NodeId nodeId, CancellationToken ct) { var selection = _system.ActorSelection($"akka.tcp://{_systemName}@{nodeId.Value}/user/driver-host"); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/ServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/ServiceCollectionExtensions.cs index c0d947a2..dc951fe1 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/ServiceCollectionExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Clients/ServiceCollectionExtensions.cs @@ -10,6 +10,7 @@ public static class ServiceCollectionExtensions /// Registers the Admin UI client services in the dependency injection container. /// /// The service collection to register clients into. + /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaAdminClients(this IServiceCollection services) { services.AddScoped(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressBuilder.cs index 03137eaf..5cb84c2b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressBuilder.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/AbLegacyAddressBuilder.cs @@ -7,6 +7,11 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; /// public static class AbLegacyAddressBuilder { + /// Builds the canonical AB Legacy address string from its file-type, file-number, and element parts. + /// The file-type prefix (e.g. N). + /// The file number. + /// The element index within the file. + /// The canonical address string (e.g. N7:0). public static string Build(string fileType, int fileNumber, int element) => $"{fileType}{fileNumber}:{element}"; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FocasAddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FocasAddressBuilder.cs index b5ebd94c..94e2708d 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FocasAddressBuilder.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/FocasAddressBuilder.cs @@ -7,6 +7,10 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; /// public static class FocasAddressBuilder { + /// Builds the canonical FOCAS address string from a parameter group and parameter ID. + /// The FOCAS parameter group (e.g. "axis"). + /// The parameter ID within the group. + /// The canonical address string in group:parameterId form (e.g. axis:5). public static string Build(string group, int parameterId) => $"{group}:{parameterId}"; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressBuilder.cs index 13b90164..739b3807 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressBuilder.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/ModbusAddressBuilder.cs @@ -7,6 +7,11 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; /// public static class ModbusAddressBuilder { + /// Builds the canonical Modbus address string for a register type, offset, and length. + /// The register type (e.g. Coil, DiscreteInput, Input, Holding). + /// The zero-based register/coil offset. + /// The number of registers/coils spanned. + /// The canonical address string (e.g. 4x00001-1). public static string Build(string regType, int offset, int length) { var prefix = regType switch diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressBuilder.cs index 6280681d..520cd700 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressBuilder.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/S7AddressBuilder.cs @@ -7,10 +7,12 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers; /// public static class S7AddressBuilder { + /// Builds the canonical S7 address string for the given area, DB number, offset, and data type. /// DB / M / I / Q /// Only relevant when area == "DB". /// Byte offset (decimal). /// X / B / W / D / REAL + /// The canonical S7 address string (e.g. DB10.DBD20:REAL). public static string Build(string area, int dbNumber, int offset, string s7Type) { if (area == "DB") diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/ResilienceFormModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/ResilienceFormModel.cs index 729b6ab3..6d70c4d0 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/ResilienceFormModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/ResilienceFormModel.cs @@ -14,19 +14,28 @@ public sealed class ResilienceFormModel public static readonly string[] Capabilities = ["Read", "Write", "Discover", "Subscribe", "Probe", "AlarmSubscribe", "AlarmAcknowledge", "HistoryRead"]; + /// Gets or sets the bulkhead max-concurrency override; null = use the tier default. public int? BulkheadMaxConcurrent { get; set; } + /// Gets or sets the bulkhead max-queue-length override; null = use the tier default. public int? BulkheadMaxQueue { get; set; } + /// Gets or sets the driver recycle-interval override, in seconds; null = use the tier default. public int? RecycleIntervalSeconds { get; set; } // capability name -> (timeout, retry, breaker), each nullable. + /// Gets or sets the per-capability resilience overrides, keyed by capability name. public Dictionary Policies { get; set; } = Capabilities.ToDictionary(c => c, _ => new CapabilityRow(), StringComparer.OrdinalIgnoreCase); + /// Per-capability timeout/retry/breaker override row; null fields fall back to the tier default. public sealed class CapabilityRow { + /// Gets or sets the timeout override, in seconds; null = use the tier default. public int? TimeoutSeconds { get; set; } + /// Gets or sets the retry-count override; null = use the tier default. public int? RetryCount { get; set; } + /// Gets or sets the circuit-breaker failure-threshold override; null = use the tier default. public int? BreakerFailureThreshold { get; set; } + /// Gets a value indicating whether all fields in this row are unset. public bool IsEmpty => TimeoutSeconds is null && RetryCount is null && BreakerFailureThreshold is null; } @@ -37,6 +46,9 @@ public sealed class ResilienceFormModel DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, }; + /// Parses the override JSON into a form model; malformed or blank JSON yields an empty (all-default) form. + /// The raw resilience-override JSON, or null/blank if there is no override. + /// A populated form model, or an all-default one when is blank or malformed. public static ResilienceFormModel FromJson(string? json) { var model = new ResilienceFormModel(); @@ -62,6 +74,7 @@ public sealed class ResilienceFormModel } /// Emit only the non-null overrides; returns null when nothing is overridden. + /// The serialized override JSON, or null when no field in this form is overridden. public string? ToJson() { var caps = Policies @@ -87,18 +100,27 @@ public sealed class ResilienceFormModel return JsonSerializer.Serialize(shape, WriteOpts); } + /// Wire shape of the resilience-override JSON, as consumed by DriverResilienceOptionsParser. private sealed class Shape { + /// Gets or sets the bulkhead max-concurrency override. public int? BulkheadMaxConcurrent { get; set; } + /// Gets or sets the bulkhead max-queue-length override. public int? BulkheadMaxQueue { get; set; } + /// Gets or sets the driver recycle-interval override, in seconds. public int? RecycleIntervalSeconds { get; set; } + /// Gets or sets the per-capability overrides, keyed by capability name. public Dictionary? CapabilityPolicies { get; set; } } + /// Wire shape of a single capability's timeout/retry/breaker override. private sealed class PolicyShape { + /// Gets or sets the timeout override, in seconds. public int? TimeoutSeconds { get; set; } + /// Gets or sets the retry-count override. public int? RetryCount { get; set; } + /// Gets or sets the circuit-breaker failure-threshold override. public int? BreakerFailureThreshold { get; set; } } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs index 1b5fda01..d8a1eb98 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/EndpointRouteBuilderExtensions.cs @@ -19,6 +19,7 @@ public static class EndpointRouteBuilderExtensions /// /// The root component type for Razor pages. /// The endpoint route builder. + /// The same endpoint route builder, for chaining. public static IEndpointRouteBuilder MapAdminUI(this IEndpointRouteBuilder app) where TApp : IComponent { @@ -34,6 +35,7 @@ public static class EndpointRouteBuilderExtensions /// Adds AdminUI services to the dependency injection container. /// /// The service collection. + /// The same service collection, for chaining. public static IServiceCollection AddAdminUI(this IServiceCollection services) { services.AddRazorComponents().AddInteractiveServerComponents(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/AlertSignalRBridge.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/AlertSignalRBridge.cs index 572c8bf1..dfd1a4b1 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/AlertSignalRBridge.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/AlertSignalRBridge.cs @@ -25,6 +25,7 @@ public sealed class AlertSignalRBridge : ReceiveActor /// /// The SignalR hub context to send alerts to. /// In-process fan-out read directly by the Blazor Server Alerts page. + /// The Akka for creating this actor. public static Props Props(IHubContext hub, IInProcessBroadcaster broadcaster) => Akka.Actor.Props.Create(() => new AlertSignalRBridge(hub, broadcaster)); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverStatusHub.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverStatusHub.cs index 88a70ee7..ad7ec49e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverStatusHub.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverStatusHub.cs @@ -28,6 +28,8 @@ public sealed class DriverStatusHub : Hub /// the current snapshot from the in-memory store so the client renders state without /// waiting for the next change event. /// + /// Identifier of the driver instance to join. + /// A task that represents the asynchronous operation. public async Task JoinDriver(string driverInstanceId) { var groupName = GroupName(driverInstanceId); @@ -40,5 +42,7 @@ public sealed class DriverStatusHub : Hub } /// Builds the SignalR group name for a given driver instance. + /// Identifier of the driver instance. + /// The SignalR group name for the driver instance. public static string GroupName(string driverInstanceId) => $"driver:{driverInstanceId}"; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverStatusSignalRBridge.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverStatusSignalRBridge.cs index 6fc52c9b..556d337b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverStatusSignalRBridge.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/DriverStatusSignalRBridge.cs @@ -24,6 +24,7 @@ public sealed class DriverStatusSignalRBridge : ReceiveActor /// Creates actor props for a . /// The SignalR hub context for pushing snapshots to grouped clients. /// Snapshot store updated before each SignalR push. + /// An Akka.NET instance for spawning the bridge actor. public static Props Props(IHubContext hub, IDriverStatusSnapshotStore store) => Akka.Actor.Props.Create(() => new DriverStatusSignalRBridge(hub, store)); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubRouteBuilderExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubRouteBuilderExtensions.cs index f30b5a55..1013b021 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubRouteBuilderExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubRouteBuilderExtensions.cs @@ -10,6 +10,7 @@ public static class HubRouteBuilderExtensions /// Maps all OtOpcUa Admin UI SignalR hubs to their configured endpoints. /// /// The endpoint route builder to register hubs on. + /// The same endpoint route builder, for chaining. public static IEndpointRouteBuilder MapOtOpcUaHubs(this IEndpointRouteBuilder app) { app.MapHub(FleetStatusHub.Endpoint); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs index 3e5a2fff..6867d222 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/HubServiceCollectionExtensions.cs @@ -24,6 +24,7 @@ public static class HubServiceCollectionExtensions /// /// /// The service collection. + /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaDriverStatusServices(this IServiceCollection services) { services.AddSingleton(); @@ -46,6 +47,7 @@ public static class HubServiceCollectionExtensions /// /// /// The Akka configuration builder. + /// The same builder, for chaining. public static AkkaConfigurationBuilder WithOtOpcUaSignalRBridges(this AkkaConfigurationBuilder builder) { builder.WithActors((system, registry, resolver) => diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IDriverStatusSnapshotStore.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IDriverStatusSnapshotStore.cs index a4d0da15..b37b7bd4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IDriverStatusSnapshotStore.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IDriverStatusSnapshotStore.cs @@ -11,10 +11,18 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs; /// public interface IDriverStatusSnapshotStore { + /// Stores or replaces the last-known health snapshot for a driver instance. + /// The driver health snapshot to store. void Upsert(DriverHealthChanged snapshot); + + /// Attempts to retrieve the last-known health snapshot for a driver instance. + /// The driver instance's stable logical ID. + /// The stored snapshot, if found. + /// if a snapshot was found; otherwise . bool TryGet(string driverInstanceId, out DriverHealthChanged snapshot); /// Returns a point-in-time snapshot of every driver instance's last-known health. + /// A read-only collection of the last-known health snapshot for each driver instance. IReadOnlyCollection GetAll(); /// diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IInProcessBroadcaster.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IInProcessBroadcaster.cs index ba0533ce..23d4e764 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IInProcessBroadcaster.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/IInProcessBroadcaster.cs @@ -25,6 +25,7 @@ public interface IInProcessBroadcaster event Action? Received; /// Fan the item out to all current subscribers. + /// The item to publish to subscribers. void Publish(T item); /// diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/ScriptLogSignalRBridge.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/ScriptLogSignalRBridge.cs index 69ae2790..650465ba 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/ScriptLogSignalRBridge.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Hubs/ScriptLogSignalRBridge.cs @@ -21,6 +21,7 @@ public sealed class ScriptLogSignalRBridge : ReceiveActor /// Creates a Props instance for the ScriptLogSignalRBridge. /// The SignalR hub context for sending messages to clients. /// In-process fan-out read directly by the Blazor Server Script log page. + /// A configured for creating the actor. public static Props Props(IHubContext hub, IInProcessBroadcaster broadcaster) => Akka.Actor.Props.Create(() => new ScriptLogSignalRBridge(hub, broadcaster)); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs index 810febe3..fe607acc 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/IScriptTagCatalog.cs @@ -14,9 +14,13 @@ public interface IScriptTagCatalog /// ctx.GetTag/SetVirtualTag), optionally filtered by a literal prefix. /// Case-insensitive StartsWith prefix; null/empty returns all (bounded). /// Cancellation token. + /// The matching configured tag + virtual-tag paths. Task> GetPathsAsync(string? filter, CancellationToken ct); /// Exact (case-sensitive, Ordinal) lookup of a resolvable tag path; null when not a known configured path. + /// The exact tag/virtual-tag path to look up. + /// Cancellation token. + /// The resolved tag info, or when is not a known configured path. Task GetTagInfoAsync(string path, CancellationToken ct); /// Distinct attribute leaf names — the substring after the first dot of each diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisEndpoints.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisEndpoints.cs index 0d4f48db..7c801db9 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisEndpoints.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisEndpoints.cs @@ -8,6 +8,9 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.ScriptAnalysis; public static class ScriptAnalysisEndpoints { + /// Maps the /api/script-analysis endpoint group (diagnostics, completions, hover, signature help, format), gated to the Administrator/Designer roles. + /// The route builder to map the endpoints onto. + /// The same route builder, for chaining. public static IEndpointRouteBuilder MapScriptAnalysisEndpoints(this IEndpointRouteBuilder endpoints) { // Require Administrator or Designer — matches the Script page gate and the /deployments gate. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisService.cs index bff62966..32331a0e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/ScriptAnalysis/ScriptAnalysisService.cs @@ -33,6 +33,9 @@ public sealed class ScriptAnalysisService private readonly IScriptTagCatalog? _catalog; private readonly ILogger? _logger; + /// Creates the script-analysis service. + /// The tag catalog used to resolve tag-path completions/hover, or null to disable tag-path features. + /// The logger used to record analysis failures, or null to disable logging. public ScriptAnalysisService(IScriptTagCatalog? catalog = null, ILogger? logger = null) { _catalog = catalog; @@ -81,6 +84,10 @@ public sealed class ScriptAnalysisService private static string Normalize(string? s) => (s ?? "").Replace("\r\n", "\n").Replace("\r", "\n"); + /// Compiles the script and returns compile-error diagnostics plus sandbox-policy violations + /// (forbidden types, disallowed dynamic tag paths). + /// The diagnose request carrying the script source. + /// The list of diagnostic markers, empty when the script is clean or analysis fails. public DiagnoseResponse Diagnose(DiagnoseRequest req) { if (string.IsNullOrEmpty(req.Code)) return new DiagnoseResponse(Array.Empty()); @@ -149,6 +156,9 @@ public sealed class ScriptAnalysisService if (code[i] == '\n') { line++; col = 1; } else col++; return (line, col); } + /// Computes completion items (tag-path, dot-member, or in-scope symbol completions) at the caret position. + /// The completions request carrying the script source and caret position. + /// The resolved list of completion items, empty when none apply. public async Task CompleteAsync(CompletionsRequest req) { if (string.IsNullOrEmpty(req.CodeText)) return new CompletionsResponse(Array.Empty()); @@ -245,6 +255,9 @@ public sealed class ScriptAnalysisService return new CompletionItem(symbol.Name, symbol.Name, symbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), kind); } + /// Computes hover information (tag-path info or C# symbol info) for the position under the caret. + /// The hover request carrying the script source and caret position. + /// The resolved hover markdown, or a response with a null value when nothing resolves. public async Task Hover(HoverRequest req) { if (string.IsNullOrWhiteSpace(req.CodeText)) return new HoverResponse(null); @@ -339,6 +352,9 @@ public sealed class ScriptAnalysisService catch { return null; } } + /// Computes signature help for the method invocation at the caret position. + /// The signature-help request carrying the script source and caret position. + /// The resolved signature and active-parameter index, or an empty response when none applies. public SignatureHelpResponse SignatureHelp(SignatureHelpRequest req) { var empty = new SignatureHelpResponse(null, null, 0); @@ -373,7 +389,10 @@ public sealed class ScriptAnalysisService } catch (Exception ex) { _logger?.LogWarning(ex, "Script signature help failed."); return empty; } } - public FormatResponse Format(FormatRequest req) // Task 8 + /// Formats a script's source code using Roslyn's syntax-tree whitespace normalization. + /// The format request carrying the script source to format. + /// The formatted source, or the original source unchanged if parsing/formatting fails. + public FormatResponse Format(FormatRequest req) { if (string.IsNullOrEmpty(req.Code)) return new FormatResponse(req.Code); try @@ -389,5 +408,9 @@ public sealed class ScriptAnalysisService return new FormatResponse(req.Code); } } - public InlayHintsResponse InlayHints(InlayHintsRequest req) => new(Array.Empty()); // Task 8 (stays empty) + /// Returns editor inlay hints for the script. Currently a no-op (always empty) — the endpoint + /// exists for future use but the feature is not implemented. + /// The inlay-hints request (unused). + /// An always-empty inlay-hints response. + public InlayHintsResponse InlayHints(InlayHintsRequest req) => new(Array.Empty()); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentInput.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentInput.cs index bccea960..4a39f767 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentInput.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentInput.cs @@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns; /// Parameter object carrying the operator-editable fields for an equipment create or update, /// so and /// avoid an unwieldy positional signature. -/// The EquipmentId and EquipmentUuid are system-generated (decision #125) and are +/// The EquipmentId and EquipmentUuid are system-generated and are /// therefore not part of this input. Optional string fields that arrive whitespace-only are /// collapsed to null by the service. /// diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs index 4c9fcb84..1faecc18 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs @@ -28,7 +28,7 @@ public sealed record LineEditDto(string UnsLineId, string UnsAreaId, string Name /// An equipment projected for editing: its system-generated id, the operator-editable identity and /// OPC 40010 identification fields, plus the concurrency token the edit modal must echo back on save. /// -/// The system-generated stable id (read-only — never operator-edited, decision #125). +/// The system-generated stable id (read-only — never operator-edited). /// UNS level-5 segment name. /// Operator colloquial id; unique fleet-wide. /// The owning line id (the UNS-line selection). @@ -54,7 +54,7 @@ public sealed record EquipmentEditDto(string EquipmentId, string Name, string Ma /// /// An equipment-bound tag projected for editing: its operator-editable fields, the owning equipment /// (so the host can scope the candidate-driver list and refresh the right node), plus the concurrency -/// token the edit modal must echo back on save. Tree tags are always equipment-bound (decision #110), +/// token the edit modal must echo back on save. Tree tags are always equipment-bound, /// so FolderPath never surfaces here. /// /// The tag's stable id (read-only on edit). @@ -73,7 +73,7 @@ public sealed record TagEditDto(string TagId, string EquipmentId, string Name, s /// /// An equipment-bound virtual tag projected for editing: its operator-editable fields, the owning /// equipment (so the host can refresh the right node), plus the concurrency token the edit modal must -/// echo back on save. Virtual tags are always scoped to an equipment (plan decision #2), so the modal +/// echo back on save. Virtual tags are always scoped to an equipment, so the modal /// never offers an equipment-change control. /// /// The virtual tag's stable id (read-only on edit). @@ -92,7 +92,7 @@ public sealed record VirtualTagEditDto(string VirtualTagId, string EquipmentId, /// /// The outcome of a bulk equipment CSV import: how many rows were inserted, how many were skipped /// (existing MachineCode — the importer is additive-only, never an update), and a per-row error list -/// for rows that could not be inserted (unknown line, unknown driver, or a decision-#122 cluster +/// for rows that could not be inserted (unknown line, unknown driver, or a cluster /// mismatch). Skipped rows never appear in . /// /// The count of new Equipment rows added. @@ -221,7 +221,7 @@ public interface IUnsTreeService /// /// Updates a UNS area's name, notes, and owning cluster. When the cluster changes, the - /// decision-#122 reassignment guard blocks the move if any driver-bound equipment under the + /// reassignment guard blocks the move if any driver-bound equipment under the /// area is bound to a driver in a different cluster than the target. Uses last-write-wins /// optimistic concurrency on . /// @@ -231,7 +231,7 @@ public interface IUnsTreeService /// The target cluster (may equal the current one). /// The concurrency token the caller last read. /// A token to cancel the operation. - /// Success, a missing-row failure, a #122 guard failure, or a concurrency failure. + /// Success, a missing-row failure, a cluster-guard failure, or a concurrency failure. Task UpdateAreaAsync(string unsAreaId, string name, string? notes, string newClusterId, byte[] rowVersion, CancellationToken ct = default); /// @@ -283,21 +283,21 @@ public interface IUnsTreeService /// /// Creates a new equipment under a UNS line. The EquipmentId is system-generated - /// (decision #125: EQ- + the first 12 hex chars of a fresh EquipmentUuid). + /// (EQ- + the first 12 hex chars of a fresh EquipmentUuid). /// Fails if the line is unset, if the MachineCode is already used fleet-wide, or if the - /// decision-#122 driver-cluster guard trips. Whitespace-only DriverInstanceId/ZTag/SAPID + /// driver-cluster guard trips. Whitespace-only DriverInstanceId/ZTag/SAPID /// collapse to null. /// /// The operator-editable equipment fields. /// A token to cancel the operation. - /// Success, a missing-line failure, a duplicate-MachineCode failure, or a #122 guard failure. + /// Success, a missing-line failure, a duplicate-MachineCode failure, or a cluster-guard failure. Task CreateEquipmentAsync(EquipmentInput input, CancellationToken ct = default); /// /// Bulk-imports equipment from a parsed set of rows in a single /// context, applying the same rules as the single-add path: a row whose UnsLineId does not /// exist is an error; a row whose DriverInstanceId is set but does not resolve is an error; - /// a driver-bound row whose driver is in a different cluster than its line fails the decision-#122 + /// a driver-bound row whose driver is in a different cluster than its line fails the cluster /// guard; and a row whose MachineCode already exists in the DB or earlier in the /// same batch is silently skipped (additive-only — never an update). Inserted rows get a /// system-generated EQ- id and a fresh EquipmentUuid. All inserts are saved once at @@ -310,7 +310,7 @@ public interface IUnsTreeService /// /// Updates an equipment's mutable fields (driver binding, line, name, MachineCode, external - /// ids, and the OPC 40010 identification fields). The decision-#122 driver-cluster guard blocks + /// ids, and the OPC 40010 identification fields). The driver-cluster guard blocks /// binding to a driver in a different cluster than the equipment's line. Uses last-write-wins /// optimistic concurrency on . /// @@ -318,7 +318,7 @@ public interface IUnsTreeService /// The new operator-editable equipment fields. /// The concurrency token the caller last read. /// A token to cancel the operation. - /// Success, a missing-row failure, a #122 guard failure, or a concurrency failure. + /// Success, a missing-row failure, a cluster-guard failure, or a concurrency failure. Task UpdateEquipmentAsync(string equipmentId, EquipmentInput input, byte[] rowVersion, CancellationToken ct = default); /// @@ -361,7 +361,7 @@ public interface IUnsTreeService /// /// Loads the drivers eligible to back a tag on the given equipment: drivers in the equipment's /// cluster (Equipment.UnsLine → UnsArea.ClusterId) whose namespace is Equipment-kind - /// (decision #110 — tree tags are equipment-bound). Ordered by DriverInstanceId. Returns + /// (tree tags are equipment-bound). Ordered by DriverInstanceId. Returns /// an empty list when the equipment cannot be resolved to a cluster. /// /// The equipment whose candidate drivers to load. @@ -373,10 +373,10 @@ public interface IUnsTreeService Task> LoadTagDriversForEquipmentAsync(string equipmentId, CancellationToken ct = default); /// - /// Creates a new equipment-bound tag. FolderPath is always null (decision #110 — - /// the tree only edits equipment-bound tags). Fails on a duplicate TagId, invalid + /// Creates a new equipment-bound tag. FolderPath is always null (the tree + /// only edits equipment-bound tags). Fails on a duplicate TagId, invalid /// TagConfig JSON, an unknown driver, a driver whose namespace is not Equipment-kind, a - /// driver in a different cluster than the equipment (decision #122), or a name already used on + /// driver in a different cluster than the equipment, or a name already used on /// the equipment. Whitespace-only PollGroupId collapses to null. /// /// The owning equipment. @@ -388,8 +388,8 @@ public interface IUnsTreeService /// /// Updates an equipment-bound tag's driver binding, name, data type, access level, write-retry /// flag, poll group, and config. The owning EquipmentId and the null - /// FolderPath are preserved. Re-runs the JSON-validity, namespace-kind, and decision-#122 - /// cluster guards against the tag's existing equipment, and enforces name uniqueness on that + /// FolderPath are preserved. Re-runs the JSON-validity, namespace-kind, and cluster + /// guards against the tag's existing equipment, and enforces name uniqueness on that /// equipment excluding this tag. Uses last-write-wins optimistic concurrency on /// . /// @@ -432,7 +432,7 @@ public interface IUnsTreeService Task CreateScriptAsync(string name, CancellationToken ct = default); /// - /// Creates a new equipment-bound virtual tag (plan decision #2 — virtual tags are always scoped + /// Creates a new equipment-bound virtual tag (virtual tags are always scoped /// to an equipment). Fails if the equipment does not exist, if no script is chosen, if neither a /// change trigger nor a timer is set, if the timer is below the 50 ms minimum, on a duplicate /// VirtualTagId, or on a name already used on the equipment. diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/AbCipTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/AbCipTagConfigModel.cs index 449d38da..efb64254 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/AbCipTagConfigModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/AbCipTagConfigModel.cs @@ -20,6 +20,8 @@ public sealed class AbCipTagConfigModel /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining /// every original key (so fields this editor doesn't expose survive a load→save). + /// The TagConfig JSON string to parse, or null/empty for a new tag. + /// The populated model. public static AbCipTagConfigModel FromJson(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -34,6 +36,7 @@ public sealed class AbCipTagConfigModel /// Serialises this model back to a TagConfig JSON string, writing the exposed fields /// (enum as its name string; the optional host address omitted when blank) over the preserved key bag. + /// The serialised TagConfig JSON string. public string ToJson() { TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim()); @@ -43,6 +46,7 @@ public sealed class AbCipTagConfigModel } /// Validation hook; returns an error message or null when the model is valid. + /// An error message describing the validation failure, or null when valid. public string? Validate() => string.IsNullOrWhiteSpace(TagPath) ? "Tag path is required." : null; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/AbLegacyTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/AbLegacyTagConfigModel.cs index e5d81829..cf0d2dd1 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/AbLegacyTagConfigModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/AbLegacyTagConfigModel.cs @@ -20,6 +20,8 @@ public sealed class AbLegacyTagConfigModel /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining /// every original key (so fields this editor doesn't expose survive a load→save). + /// The TagConfig JSON string to parse, or null/empty for a new tag. + /// The populated model. public static AbLegacyTagConfigModel FromJson(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -34,6 +36,7 @@ public sealed class AbLegacyTagConfigModel /// Serialises this model back to a TagConfig JSON string, writing the exposed fields /// (enum as its name string; the optional host address omitted when blank) over the preserved key bag. + /// The serialised TagConfig JSON string. public string ToJson() { TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim()); @@ -43,6 +46,7 @@ public sealed class AbLegacyTagConfigModel } /// Validation hook; returns an error message or null when the model is valid. + /// An error message describing the validation failure, or null when valid. public string? Validate() { if (string.IsNullOrWhiteSpace(Address)) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/FocasTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/FocasTagConfigModel.cs index 1cd44d8e..ac159401 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/FocasTagConfigModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/FocasTagConfigModel.cs @@ -20,6 +20,8 @@ public sealed class FocasTagConfigModel /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining /// every original key (so fields this editor doesn't expose survive a load→save). + /// The raw TagConfig JSON string, or null/empty for a fresh model. + /// The populated model, defaulted where fields are absent from . public static FocasTagConfigModel FromJson(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -34,6 +36,7 @@ public sealed class FocasTagConfigModel /// Serialises this model back to a TagConfig JSON string, writing the exposed fields /// (enum as its name string; the optional host address omitted when blank) over the preserved key bag. + /// The serialised TagConfig JSON string. public string ToJson() { TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim()); @@ -43,6 +46,7 @@ public sealed class FocasTagConfigModel } /// Validation hook; returns an error message or null when the model is valid. + /// An error message describing the validation failure, or null when the model is valid. public string? Validate() => string.IsNullOrWhiteSpace(Address) ? "Address is required." : null; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/ModbusTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/ModbusTagConfigModel.cs index 9fb33ded..127da898 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/ModbusTagConfigModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/ModbusTagConfigModel.cs @@ -29,6 +29,8 @@ public sealed class ModbusTagConfigModel /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining /// every original key (so fields this editor doesn't expose survive a load→save). + /// The raw TagConfig JSON string, or null for a new/empty config. + /// The populated . public static ModbusTagConfigModel FromJson(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -46,6 +48,7 @@ public sealed class ModbusTagConfigModel /// Serialises this model back to a TagConfig JSON string, writing the six exposed fields /// (enums as their name strings) over the preserved key bag. + /// The serialised TagConfig JSON string. public string ToJson() { TagConfigJson.Set(_bag, "region", Region); @@ -58,5 +61,6 @@ public sealed class ModbusTagConfigModel } /// Validation hook; returns an error message or null when the model is valid. + /// An error message describing the validation failure, or null when the model is valid. public string? Validate() => null; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/NativeAlarmModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/NativeAlarmModel.cs index 864581c5..7ac2e5b4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/NativeAlarmModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/NativeAlarmModel.cs @@ -43,6 +43,8 @@ public sealed class NativeAlarmModel /// Loads a model from a TagConfig JSON string. When no alarm object is present the /// model is flagged non-alarm and the alarm fields keep their defaults. Retains every original key /// at the root and inside alarm so a load→save preserves fields this editor doesn't expose. + /// The tag's raw TagConfig JSON, or null/empty for a fresh config. + /// The populated model. public static NativeAlarmModel FromJson(string? json) { var root = TagConfigJson.ParseOrNew(json); @@ -63,6 +65,7 @@ public sealed class NativeAlarmModel /// Serialises this model back to a TagConfig JSON string. When is set, /// writes the alarm fields over the preserved alarm sub-object (creating it if absent); a /// null REMOVES the key so the absent ⇒ historize default holds. + /// The serialised TagConfig JSON string. public string ToJson() { if (IsAlarm) @@ -92,6 +95,7 @@ public sealed class NativeAlarmModel } /// Validation hook; returns an error message or null when the model is valid. + /// An error message describing the validation failure, or null when the model is valid. public string? Validate() => null; /// Default alarmType seeded when an alarm-bearing attribute is picked but the @@ -111,6 +115,8 @@ public sealed class NativeAlarmModel /// preserved: when an alarm object already exists this returns the input unchanged (never /// overwrites an authored alarm). Returns the (possibly unchanged) TagConfig JSON string. /// + /// The tag's raw TagConfig JSON, or null/empty for a fresh config. + /// The (possibly unchanged) TagConfig JSON string with the default alarm seeded when applicable. public static string SeedDefaultAlarm(string? json) { var root = TagConfigJson.ParseOrNew(json); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs index 7066e7ad..82a6348b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs @@ -24,6 +24,7 @@ public sealed class OpcUaClientTagConfigModel /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining /// every original key (so fields this editor doesn't expose survive a load→save). /// The tag's TagConfig JSON (null/blank/malformed ⇒ defaults). + /// The populated model. public static OpcUaClientTagConfigModel FromJson(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -38,6 +39,7 @@ public sealed class OpcUaClientTagConfigModel /// FullName is written PascalCase (the composer/walker contract key); any history keys merged /// by the TagModal (isHistorized / historianTagname) are carried through untouched as /// preserved unknown keys. + /// The serialised TagConfig JSON string. public string ToJson() { TagConfigJson.Set(_bag, "FullName", FullName.Trim()); @@ -45,6 +47,7 @@ public sealed class OpcUaClientTagConfigModel } /// Validation hook; returns an error message or null when the model is valid. + /// An error message describing the validation failure, or null when valid. public string? Validate() => string.IsNullOrWhiteSpace(FullName) ? "An upstream node reference (FullName) is required." : null; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/S7TagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/S7TagConfigModel.cs index 611aa4df..7d561a71 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/S7TagConfigModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/S7TagConfigModel.cs @@ -20,6 +20,8 @@ public sealed class S7TagConfigModel /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining /// every original key (so fields this editor doesn't expose survive a load→save). + /// The raw TagConfig JSON string, or null/empty for a fresh model. + /// The populated model, defaulted where fields are absent from . public static S7TagConfigModel FromJson(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -34,6 +36,7 @@ public sealed class S7TagConfigModel /// Serialises this model back to a TagConfig JSON string, writing the exposed fields /// (enum as its name string) over the preserved key bag. + /// The serialised TagConfig JSON string. public string ToJson() { TagConfigJson.Set(_bag, "address", Address.Trim()); @@ -43,6 +46,7 @@ public sealed class S7TagConfigModel } /// Validation hook; returns an error message or null when the model is valid. + /// An error message describing the validation failure, or null when the model is valid. public string? Validate() => string.IsNullOrWhiteSpace(Address) ? "Address is required." : null; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagArrayConfig.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagArrayConfig.cs index 9609a187..eae0a254 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagArrayConfig.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagArrayConfig.cs @@ -27,6 +27,8 @@ public static class TagArrayConfig /// Reads the array-shape keys from a TagConfig JSON, defaulting any absent key /// (null/blank/malformed input ⇒ (false, null)). A negative or non-numeric arrayLength /// reads as null. + /// The TagConfig JSON to read from, or null. + /// The parsed array-shape state. public static ArrayState Read(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -39,6 +41,10 @@ public static class TagArrayConfig /// (driver-specific or unknown) key. isArray is dropped when false (absent ⇒ false at the /// materialiser); arrayLength is dropped when is false or /// is null (never leaves an orphan length behind a cleared isArray). + /// The existing TagConfig JSON to merge into, or null for a new config. + /// Whether the tag should be materialised as a 1-D array. + /// The array length to store; ignored when is false. + /// The merged TagConfig JSON string. public static string Set(string? json, bool isArray, uint? arrayLength) { var o = TagConfigJson.ParseOrNew(json); @@ -52,6 +58,9 @@ public static class TagArrayConfig /// Validates the array-shape intent: when is set, a positive /// is required. Returns an error string when invalid, or null /// when valid (a non-array is always valid regardless of length). + /// Whether the tag is configured as a 1-D array. + /// The configured array length, or null when unset. + /// An error message when the combination is invalid; otherwise null. public static string? Validate(bool isArray, uint? arrayLength) => isArray && (arrayLength is null or 0) ? "Array length must be a positive number when 'This tag is an array' is checked." diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs index c6c9f153..835953d2 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs @@ -20,6 +20,8 @@ public static class TagConfigEditorMap }; /// Returns the editor component type for a driver type, or null if none is registered. + /// The driver type name to look up an editor component for. + /// The editor component , or null if no typed editor is registered. public static Type? Resolve(string? driverType) => driverType is not null && Map.TryGetValue(driverType, out var t) ? t : null; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs index 66bd823e..69d6ac4b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigJson.cs @@ -11,6 +11,8 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors; public static class TagConfigJson { /// Parses into a mutable object; returns a fresh empty object on null/blank/malformed/non-object input. + /// The JSON string to parse. + /// The parsed , or a fresh empty object on invalid input. public static JsonObject ParseOrNew(string? json) { if (string.IsNullOrWhiteSpace(json)) { return new JsonObject(); } @@ -19,25 +21,46 @@ public static class TagConfigJson } /// Serialises the object to compact JSON (JsonNode.ToJsonString() defaults to non-indented). + /// The JSON object to serialise. + /// The compact JSON string. public static string Serialize(JsonObject obj) => obj.ToJsonString(); /// Reads a string value, or null if absent/null/non-string. + /// The JSON object to read from. + /// The property name to read. + /// The string value, or null if absent, null, or non-string. public static string? GetString(JsonObject o, string name) => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue(out var s) ? s : null; /// Reads an int value, or if absent/null/non-numeric (incl. object/array nodes). + /// The JSON object to read from. + /// The property name to read. + /// The value to return if the property is absent, null, or non-numeric. + /// The int value, or . public static int GetInt(JsonObject o, string name, int fallback = 0) => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue(out var i) ? i : fallback; /// Reads a bool value, or if absent/null/non-boolean (incl. object/array/number/string nodes). + /// The JSON object to read from. + /// The property name to read. + /// The value to return if the property is absent, null, or non-boolean. + /// The bool value, or . public static bool GetBool(JsonObject o, string name, bool fallback = false) => o.TryGetPropertyValue(name, out var n) && n is JsonValue v && v.TryGetValue(out var b) ? b : fallback; /// Reads an enum by its serialised name, or if absent/unparseable. + /// The enum type to parse. + /// The JSON object to read from. + /// The property name to read. + /// The value to return if the property is absent or unparseable. + /// The parsed enum value, or . public static TEnum GetEnum(JsonObject o, string name, TEnum fallback) where TEnum : struct, Enum => GetString(o, name) is { } s && Enum.TryParse(s, ignoreCase: true, out var v) ? v : fallback; /// Sets a string/number/enum-name value (enums via ToString()). A null value REMOVES the key, so it is omitted from the serialised JSON. + /// The JSON object to mutate. + /// The property name to set. + /// The value to set, or null to remove the property. public static void Set(JsonObject o, string name, object? value) { if (value is null) { o.Remove(name); return; } @@ -52,6 +75,9 @@ public static class TagConfigJson /// alarm object (and root history/array intent, scaling, etc.). Null/blank/malformed input starts /// from a bare {"FullName":...} object. /// + /// The existing TagConfig JSON string, or null/blank/malformed to start fresh. + /// The newly-picked Galaxy tag_name.AttributeName reference. + /// The updated TagConfig serialised back to a JSON string. public static string SetFullName(string? json, string fullName) { // A blank reference is never a valid Galaxy bind — surface it loudly rather than persisting diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs index 3037d2cd..3c7e28b0 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs @@ -25,6 +25,9 @@ public static class TagConfigValidator /// Validates a tag's for the given . /// Returns an error string to block save, or null when valid / no typed validator is registered. /// + /// The driver type name to look up a typed validator for. + /// The tag's config JSON to validate. + /// An error string to block save, or null when the config is valid or unmapped. public static string? Validate(string? driverType, string? configJson) => driverType is not null && Validators.TryGetValue(driverType, out var v) ? v(configJson) : null; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagHistorizeConfig.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagHistorizeConfig.cs index 9935e679..fa0483b5 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagHistorizeConfig.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagHistorizeConfig.cs @@ -24,6 +24,8 @@ public static class TagHistorizeConfig /// Reads the history-intent keys from a TagConfig JSON, defaulting any absent key /// (null/blank/malformed input ⇒ (false, "")). + /// The tag's TagConfig JSON, or null/empty/malformed input. + /// The parsed history-intent state, defaulted to (false, "") when keys are absent. public static HistorizeState Read(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -36,6 +38,10 @@ public static class TagHistorizeConfig /// (driver-specific or unknown) key. isHistorized is dropped when false (absent ⇒ false at the /// composer); historianTagname is dropped when null/blank (absent ⇒ defaults to FullName) and /// trimmed otherwise. + /// The tag's existing TagConfig JSON, or null/empty to start from a new object. + /// Whether the server should expose OPC UA HistoryRead over this tag's node. + /// Optional historian tagname override; null/blank omits the key. + /// The merged TagConfig JSON with the two history-intent keys applied. public static string Set(string? json, bool isHistorized, string? historianTagname) { var o = TagConfigJson.ParseOrNew(json); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TwinCATTagConfigModel.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TwinCATTagConfigModel.cs index 69507a31..b4756ed4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TwinCATTagConfigModel.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TwinCATTagConfigModel.cs @@ -20,6 +20,8 @@ public sealed class TwinCATTagConfigModel /// Loads a model from a TagConfig JSON string, defaulting any absent field and retaining /// every original key (so fields this editor doesn't expose survive a load→save). + /// The TagConfig JSON string to parse, or null/empty for a fresh model. + /// A populated . public static TwinCATTagConfigModel FromJson(string? json) { var o = TagConfigJson.ParseOrNew(json); @@ -34,6 +36,7 @@ public sealed class TwinCATTagConfigModel /// Serialises this model back to a TagConfig JSON string, writing the exposed fields /// (enum as its name string; the optional host address omitted when blank) over the preserved key bag. + /// The serialised TagConfig JSON string. public string ToJson() { TagConfigJson.Set(_bag, "deviceHostAddress", string.IsNullOrWhiteSpace(DeviceHostAddress) ? null : DeviceHostAddress.Trim()); @@ -43,6 +46,7 @@ public sealed class TwinCATTagConfigModel } /// Validation hook; returns an error message or null when the model is valid. + /// An error message describing the validation failure, or null when the model is valid. public string? Validate() => string.IsNullOrWhiteSpace(SymbolPath) ? "Symbol path is required." : null; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagInput.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagInput.cs index 0c969109..d75de6b7 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagInput.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagInput.cs @@ -14,7 +14,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns; /// The bound driver; must resolve to an Equipment-kind namespace in the equipment's cluster. /// OPC UA built-in type name (Boolean / Int32 / Float / etc.). /// Tag-level OPC UA access baseline. -/// Whether writes are safe to retry (decisions #44–45). +/// Whether writes are safe to retry. /// Optional poll-group batching key; whitespace/empty collapses to null. /// Schemaless per-driver-type JSON config; validated for JSON well-formedness. public sealed record TagInput( diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsNode.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsNode.cs index 3265202d..fc5893b4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsNode.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsNode.cs @@ -96,6 +96,11 @@ public static class UnsTreeAssembly /// (and enterprises whose clusters have no areas) are retained. Ordering is /// deterministic and ordinal at every level. /// + /// The flat set of cluster rows to nest under the enterprise root. + /// The flat set of area rows to nest under their owning clusters. + /// The flat set of line rows to nest under their owning areas. + /// The flat set of equipment rows to nest under their owning lines. + /// The assembled read-only list of top-level UNS tree nodes. public static IReadOnlyList Build( IReadOnlyList clusters, IReadOnlyList areas, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs index 469e7f3d..1150a9b0 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs @@ -293,7 +293,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion; - // Decision #122: a cluster move must not orphan driver-bound equipment from its driver's + // A cluster move must not orphan driver-bound equipment from its driver's // cluster. Any equipment under this area that is bound to a driver in a different cluster // than the target blocks the move. if (newClusterId != entity.ClusterId) @@ -416,7 +416,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF db.Entry(entity).Property(e => e.RowVersion).OriginalValue = rowVersion; - // Decision #122: a reparent to a different area must not orphan driver-bound equipment + // A reparent to a different area must not orphan driver-bound equipment // from its driver's cluster. Resolve the new area's cluster and check every bound // equipment item under this line against it. if (newUnsAreaId != entity.UnsAreaId) @@ -580,8 +580,8 @@ public sealed class UnsTreeService(IDbContextFactory dbF continue; } - // Reuse the #122 guard: it reports an unknown DriverInstanceId and a driver/line cluster - // mismatch, and is a no-op for driver-less rows. + // Reuse the driver/line cluster guard: it reports an unknown DriverInstanceId and a + // driver/line cluster mismatch, and is a no-op for driver-less rows. var guard = await CheckDriverClusterGuardAsync(db, row, ct); if (guard is not null) { @@ -838,7 +838,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF return Array.Empty<(string, string, string, string)>(); } - // Drivers in the equipment's cluster whose namespace is Equipment-kind (decision #110). + // Drivers in the equipment's cluster whose namespace is Equipment-kind. // GalaxyMxGateway is an ordinary Equipment-kind driver post-de-split, so it surfaces here // alongside the PLC drivers; its DriverConfig feeds the Galaxy live-browse address picker. var equipmentNamespaceIds = await db.Namespaces @@ -1142,7 +1142,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF var equipGuard = await ValidateEquipTokenAsync(db, entity.EquipmentId, input.ScriptId, ct); if (equipGuard is not null) return equipGuard.Value; - // EquipmentId is preserved — virtual tags are always equipment-bound (plan decision #2). + // EquipmentId is preserved — virtual tags are always equipment-bound. entity.Name = input.Name; entity.DataType = input.DataType; entity.ScriptId = input.ScriptId; @@ -1283,7 +1283,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF /// Returns true if parses as a well-formed JSON document. /// Null/blank input is treated as invalid (not well-formed JSON) so every caller gets the friendly /// "not valid JSON" result rather than an unhandled from - /// JsonDocument.Parse(null) (AdminUI-002). + /// JsonDocument.Parse(null). private static bool IsValidJson(string? json) { if (string.IsNullOrWhiteSpace(json)) @@ -1335,7 +1335,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF /// /// Validates a tree tag's driver binding: the driver must exist, live in an Equipment-kind - /// namespace (decision #110), and be in the same cluster as the owning equipment (decision #122). + /// namespace, and be in the same cluster as the owning equipment. /// Returns null when the binding is allowed, or a populated failure otherwise. /// private static async Task CheckTagDriverGuardAsync( @@ -1367,7 +1367,7 @@ public sealed class UnsTreeService(IDbContextFactory dbF } /// - /// Decision #122: an equipment may only bind to a driver in the same cluster as its line. + /// An equipment may only bind to a driver in the same cluster as its line. /// Policy: /// /// Driver-less (DriverInstanceId empty) → always allowed (returns null). diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/VirtualTagInput.cs b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/VirtualTagInput.cs index fed11be7..de18e6af 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/VirtualTagInput.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/VirtualTagInput.cs @@ -2,7 +2,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns; /// /// Parameter object carrying the operator-editable fields for an equipment-bound VirtualTag create -/// or update via the UNS tree. Virtual tags are always scoped to an equipment (plan decision #2), so +/// or update via the UNS tree. Virtual tags are always scoped to an equipment, so /// the owning EquipmentId is supplied separately and never changes on update. A virtual tag /// must have at least one evaluation trigger: or a non-null /// (which, when set, must be at least 50 ms). diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs index 7367e42d..7448ed8d 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/AdminOperationsActor.cs @@ -175,7 +175,7 @@ public sealed class AdminOperationsActor : ReceiveActor var draft = await DraftSnapshotFactory.FromConfigDbAsync(db); var errors = DraftValidator.Validate(draft).ToList(); - // Cluster-topology guard (decision #91 / task #148 part 2). The SQL + // Cluster-topology guard. The SQL // CK_ServerCluster_RedundancyMode_NodeCount CHECK enforces the (NodeCount, RedundancyMode) // pair on the row itself, but it cannot see the per-node ClusterNode.Enabled flag — an // operator can disable a node (effective enabled-count = 1) while leaving RedundancyMode at diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/ConfigComposer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/ConfigComposer.cs index b5c637c9..9ef81752 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/ConfigComposer.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/AdminOperations/ConfigComposer.cs @@ -25,6 +25,7 @@ public static class ConfigComposer /// Reads the current configuration and returns a deterministic snapshot blob with revision hash. /// The configuration database context. /// The cancellation token for the operation. + /// A deterministic containing the serialised snapshot bytes and its revision hash. public static async Task SnapshotAndFlattenAsync( OtOpcUaConfigDbContext db, CancellationToken ct = default) { @@ -53,6 +54,7 @@ public static class ConfigComposer /// Returns the SHA-256 hex digest of the supplied artifact bytes (lowercase, no prefix). /// The bytes to hash. + /// The lowercase hex-encoded SHA-256 digest. public static string HashOf(ReadOnlySpan blob) => Convert.ToHexStringLower(SHA256.HashData(blob)); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Audit/AuditWriterActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Audit/AuditWriterActor.cs index 9f1d3f4a..2ca3026c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Audit/AuditWriterActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Audit/AuditWriterActor.cs @@ -39,6 +39,7 @@ public sealed class AuditWriterActor : ReceiveActor, IWithTimers, IAuditWriter /// Creates a Props factory for the AuditWriterActor. /// The database context factory for creating ConfigDb connections. + /// The used to create an . public static Props Props(IDbContextFactory dbFactory) => Akka.Actor.Props.Create(() => new AuditWriterActor(dbFactory)); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs index 79438ca6..635e89df 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Coordinators/ConfigPublishCoordinator.cs @@ -16,7 +16,7 @@ namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Coordinators; /// over DistributedPubSub on the deployments topic, gathers /// per-node replies, and seals the deployment when every expected node /// has acked Applied. Per-node ACKs are persisted in NodeDeploymentState so a failover of -/// this singleton (Task 31) can recover in-flight state from the DB. +/// this singleton can recover in-flight state from the DB. /// /// Discovery of the "expected ACK set" comes from Akka.Cluster.State.Members filtered by /// the driver role — the DB does not own per-node role assignment. @@ -46,6 +46,7 @@ public sealed class ConfigPublishCoordinator : ReceiveActor, IWithTimers /// Creates actor props for the ConfigPublishCoordinator with the specified configuration. /// The database context factory for accessing configuration data. /// The timeout for waiting for apply acknowledgments from cluster nodes. + /// The Akka.NET used to spawn this actor. public static Props Props( IDbContextFactory dbFactory, TimeSpan? applyDeadline = null) => @@ -68,14 +69,12 @@ public sealed class ConfigPublishCoordinator : ReceiveActor, IWithTimers Receive(_ => { /* DPS subscribe confirmation */ }); } - /// - /// On startup recover any deployment that was mid-flight when a prior singleton instance - /// died. We re-derive _expectedAcks from NodeDeploymentState, replay the ACKs - /// that already landed in the DB, and resume the deadline timer. - /// /// protected override void PreStart() { + // On startup recover any deployment that was mid-flight when a prior singleton instance + // died. We re-derive _expectedAcks from NodeDeploymentState, replay the ACKs + // that already landed in the DB, and resume the deadline timer. // Subscribe to per-node ApplyAck broadcasts so DriverHostActors on remote members can // route their ACKs to whichever node currently hosts this singleton. DistributedPubSub.Get(Context.System).Mediator.Tell(new Subscribe(DeploymentAcksTopic, Self)); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Fleet/FleetStatusBroadcaster.cs b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Fleet/FleetStatusBroadcaster.cs index 1ca85343..b56520f8 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Fleet/FleetStatusBroadcaster.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.ControlPlane/Fleet/FleetStatusBroadcaster.cs @@ -10,7 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.ControlPlane.Fleet; /// /// Admin-role cluster singleton that maintains an in-memory /// snapshot from cluster membership + reachability events plus per-node heartbeats, and pushes -/// diffs on the fleet-status DistributedPubSub topic. The SignalR hub layer (Task 49) +/// diffs on the fleet-status DistributedPubSub topic. The SignalR hub layer /// subscribes to that topic and forwards to browser clients. /// /// Heartbeat staleness: a node we haven't heard from in flips diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs index a24eb392..0619af3e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Drivers/DriverFactoryBootstrap.cs @@ -36,6 +36,7 @@ public static class DriverFactoryBootstrap /// from DI when spawning DriverHostActor. /// /// The service collection to register driver factories with. + /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaDriverFactories(this IServiceCollection services) { services.AddSingleton(sp => @@ -73,6 +74,7 @@ public static class DriverFactoryBootstrap /// /// /// The service collection to register driver probes with. + /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaDriverProbes(this IServiceCollection services) { services.TryAddEnumerable(ServiceDescriptor.Singleton()); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynVirtualTagEvaluator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynVirtualTagEvaluator.cs index 3bf96df3..d9e70a8c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynVirtualTagEvaluator.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Engines/RoslynVirtualTagEvaluator.cs @@ -43,11 +43,7 @@ public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IDisposabl _runTimeout = runTimeout ?? TimeSpan.FromSeconds(2); } - /// Evaluates a virtual tag expression against a set of dependencies. - /// The virtual tag identifier, used for logging. - /// The C# expression to evaluate. - /// Dictionary of tag names to values available in the expression context. - /// The evaluation result, either successful with a value or failed with an error message. + /// public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary dependencies) { if (_disposed) return VirtualTagEvalResult.Failure("evaluator disposed"); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs index 5e29f6c7..a2abe173 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Health/HealthEndpoints.cs @@ -14,6 +14,8 @@ public static class HealthEndpoints /// Registers the shared ZB.MOM.WW health probes. Tier semantics preserved: configdb + akka on /// ready+active; admin-leader on active only. /// + /// The service collection to register the health checks on. + /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaHealth(this IServiceCollection services) { services.AddHealthChecks() @@ -40,6 +42,7 @@ public static class HealthEndpoints /// Maps the OtOpcUa health check endpoints to the route builder. /// The endpoint route builder. + /// The same endpoint route builder, for chaining. public static IEndpointRouteBuilder MapOtOpcUaHealth(this IEndpointRouteBuilder app) { app.MapZbHealth(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Observability/ObservabilityExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Observability/ObservabilityExtensions.cs index 9fb7572e..2c89fd2f 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Observability/ObservabilityExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Observability/ObservabilityExtensions.cs @@ -21,6 +21,7 @@ public static class ObservabilityExtensions /// Otlp; OtOpcUa:Telemetry:OtlpEndpoint sets the OTLP endpoint. With no /// config the exporter stays Prometheus (the default). /// + /// The service collection, for chaining. public static IServiceCollection AddOtOpcUaObservability(this IServiceCollection services, IConfiguration configuration) { return services.AddZbTelemetry(o => @@ -44,6 +45,7 @@ public static class ObservabilityExtensions /// MapZbHealth, which also marks its endpoints anonymous. /// /// The endpoint route builder. + /// The endpoint route builder, for chaining. public static IEndpointRouteBuilder MapOtOpcUaMetrics(this IEndpointRouteBuilder app) { app.MapZbMetrics().AllowAnonymous(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/LdapOpcUaUserAuthenticator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/LdapOpcUaUserAuthenticator.cs index 62858193..4a03e34a 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/LdapOpcUaUserAuthenticator.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/LdapOpcUaUserAuthenticator.cs @@ -26,10 +26,7 @@ public sealed class LdapOpcUaUserAuthenticator( ILogger logger) : IOpcUaUserAuthenticator { - /// Authenticates an OPC UA UserName token via LDAP, resolving roles through the mapper. - /// The username to authenticate. - /// The password to authenticate. - /// Cancellation token. + /// public async Task AuthenticateUserNameAsync(string username, string password, CancellationToken ct) { try diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs index 46bcd267..c9b16310 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/OpcUa/OtOpcUaServerHostedService.cs @@ -98,6 +98,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl /// Starts the OPC UA server asynchronously. /// /// Cancellation token. + /// A task that represents the asynchronous operation. public async Task StartAsync(CancellationToken cancellationToken) { _server = new OtOpcUaSdkServer(); @@ -236,6 +237,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl /// Stops the OPC UA server asynchronously. /// /// Cancellation token. + /// A task that represents the asynchronous operation. public Task StopAsync(CancellationToken cancellationToken) { // Revert to Null adapters so any in-flight writes from a poison-pilled actor don't hit a @@ -254,6 +256,7 @@ public sealed class OtOpcUaServerHostedService : IHostedService, IAsyncDisposabl /// /// Disposes the hosted service and its resources asynchronously. /// + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_appHost is not null) await _appHost.DisposeAsync(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs index eb083b0b..78e9707e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Host/Program.cs @@ -88,7 +88,7 @@ Directory.SetCurrentDirectory(AppContext.BaseDirectory); builder.AddZbSerilog(o => o.ServiceName = "otopcua"); // Windows-service registration is handled at install time by scripts/install/Install-Services.ps1 -// (Task 62) rather than in-process, so the binary stays cross-platform-compilable. +// rather than in-process, so the binary stays cross-platform-compilable. // Shared services — always registered regardless of role. ConfigDb is required for everything. builder.Services.AddOtOpcUaConfigDb(builder.Configuration); @@ -222,8 +222,8 @@ if (hasDriver) // Script-log fan-out (Layer 0). The DPS publisher resolves the ActorSystem lazily so it never // races Akka startup. ScriptRootLogger wraps the composed pipeline (rolling scripts-*.log + - // error mirror to the main log + script-logs DPS topic) for unambiguous DI resolution; Task 3 - // injects it into the Roslyn evaluators above. + // error mirror to the main log + script-logs DPS topic) for unambiguous DI resolution; it + // injects into the Roslyn evaluators above. var scriptLogFilePath = builder.Configuration["Scripting:LogFilePath"] ?? "logs/scripts-.log"; var scriptLogTopicMinLevel = Enum.TryParse( builder.Configuration["Scripting:LogTopicMinLevel"], ignoreCase: true, out var parsedLevel) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs index 4c97c570..3fba3d99 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs @@ -20,7 +20,7 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer; /// tear-down + rebuild. /// /// -/// This is the side-effecting layer Task 47 deferred to F14. It stays pure-of-SDK so +/// This is the side-effecting layer deferred to F14. It stays pure-of-SDK so /// production binds a real SDK sink, dev/Mac binds , /// and tests can capture every call. /// @@ -101,7 +101,7 @@ public sealed class AddressSpaceApplier plan.ChangedEquipment.Count + plan.ChangedDrivers.Count + plan.ChangedAlarms.Count + plan.ChangedEquipmentTags.Count + plan.ChangedEquipmentVirtualTags.Count + - // OpcUaServer-001: a UNS Area/Line rename is an in-place change to an existing folder node. + // A UNS Area/Line rename is an in-place change to an existing folder node. plan.RenamedFolders.Count; var addedCount = plan.AddedEquipment.Count + plan.AddedDrivers.Count + plan.AddedAlarms.Count + @@ -122,7 +122,7 @@ public sealed class AddressSpaceApplier // respawn (DriverHostActor → ApplyVirtualTags), so it skips the rebuild and PRESERVES every // client's server-wide subscriptions. Any structural / node-affecting vtag change (Name/ // FolderPath/DataType) — or any non-vtag change anywhere — still forces a full rebuild. - // F10b + FB-7 (surgical tag write): a CHANGED equipment tag whose ONLY differences are Writable / + // F10b (surgical tag write): a CHANGED equipment tag whose ONLY differences are Writable / // IsHistorized / HistorianTagname / DataType / IsArray / ArrayLength (a plain value variable — no // alarm condition node) can be updated IN PLACE on the existing node via // ISurgicalAddressSpaceSink.UpdateTagAttributes (see TagDeltaIsSurgicalEligible), avoiding the full @@ -140,7 +140,7 @@ public sealed class AddressSpaceApplier plan.ChangedEquipmentVirtualTags.Any(d => !VtagDeltaIsNodeIrrelevant(d)); var surgicalTagDeltas = plan.ChangedEquipmentTags.Where(TagDeltaIsSurgicalEligible).ToList(); - // OpcUaServer-001 — UNS Area / Line renames are surgically applicable (in-place DisplayName swap) + // UNS Area / Line renames are surgically applicable (in-place DisplayName swap) // when no structural rebuild fires. When a rebuild DOES fire (any add/remove/structural change), // MaterialiseHierarchy re-creates every folder with the new display names, so the renames are // covered for free and need no separate surgical pass. @@ -383,7 +383,7 @@ public sealed class AddressSpaceApplier } /// - /// #85 — build the UNS Area/Line/Equipment folder hierarchy in the address space from a + /// Build the UNS Area/Line/Equipment folder hierarchy in the address space from a /// composition snapshot. Called by OpcUaPublishActor after a rebuild so OPC UA /// clients browsing the server see proper folder structure instead of flat tag ids. /// Idempotent: each EnsureFolder call returns the existing folder if already @@ -420,19 +420,13 @@ public sealed class AddressSpaceApplier /// ensure its optional FolderPath sub-folder under the existing equipment folder, then /// ensure a Variable (NodeId = FullName, the driver-side ref) inside it. Variables /// start BadWaitingForInitialData; the driver fills live values in a later milestone. - /// Idempotent. - /// - /// Task 0 architecture decisions (recorded per the equipment-namespace-structure - /// plan). Decision #1 = A — a sink-based pass, NOT a reuse of - /// EquipmentNodeWalker: no sink-backed IAddressSpaceBuilder adapter exists - /// (GenericDriverNodeManager.CapturingBuilder decorates another builder, not the - /// sink), and the walker re-creates the whole Area/Line/Equipment tree with browse-path - /// NodeIds — incompatible with this path's logical-Id NodeIds (decision #3) and the - /// already-materialised equipment folders (decision #4). Decision #4 = this pass adds - /// ONLY variables (and any per-tag sub-folder); owns - /// the equipment folders and this pass never re-creates them. The sink's - /// EnsureVariable takes a plain string dataType (not a DriverAttributeInfo). - /// + /// Idempotent. This is a sink-based pass, NOT a reuse of EquipmentNodeWalker: no + /// sink-backed IAddressSpaceBuilder adapter exists, and the walker re-creates the + /// whole Area/Line/Equipment tree with browse-path NodeIds, incompatible with this path's + /// logical-Id NodeIds and the already-materialised equipment folders. This pass adds ONLY + /// variables (and any per-tag sub-folder); owns the + /// equipment folders and this pass never re-creates them. The sink's EnsureVariable + /// takes a plain string dataType (not a DriverAttributeInfo). /// /// The composition result containing the equipment tags to materialise. public void MaterialiseEquipmentTags(AddressSpaceComposition composition) @@ -442,7 +436,7 @@ public sealed class AddressSpaceApplier // Sub-folders first — a tag's FolderPath becomes one folder UNDER its equipment folder // (deduped per distinct equipment+path). Tags with no FolderPath hang directly under the - // equipment folder, which MaterialiseHierarchy already created (decision #4: never re-create + // equipment folder, which MaterialiseHierarchy already created (never re-create // the equipment folder here). var foldersCreated = new HashSet(StringComparer.Ordinal); foreach (var tag in composition.EquipmentTags) @@ -641,7 +635,7 @@ public sealed class AddressSpaceApplier Historize = d.Current.Historize, }).Equals(d.Current); - // F10b + FB-7: a CHANGED equipment tag whose ONLY differences are Writable / IsHistorized / + // F10b: a CHANGED equipment tag whose ONLY differences are Writable / IsHistorized / // HistorianTagname / DataType / IsArray / ArrayLength (a plain value variable — no alarm condition node) // can be updated IN PLACE on the existing node via ISurgicalAddressSpaceSink.UpdateTagAttributes, // avoiding a full rebuild (preserving subscriptions). The presentation-shape fields (DataType / IsArray / @@ -650,7 +644,7 @@ public sealed class AddressSpaceApplier // differences still fall through to a rebuild — FullName/DriverInstanceId re-route the node to a different // driver point, Name re-derives the NodeId, and an alarm flip turns the node into a Part 9 condition. The // override-unequal default also covers any future field. - // REACH (live-verified FB-7): the shape path only fires for drivers whose TagConfig carries a stable + // REACH (live-verified): the shape path only fires for drivers whose TagConfig carries a stable // top-level "FullName" (Galaxy = tag_name.AttributeName; OpcUaClient = the node id) — there a DataType/array // edit leaves FullName untouched ⇒ surgical. For structured-TagConfig protocol drivers (Modbus/S7/AbCip/…) // AddressSpaceComposer.ExtractTagFullName falls back to the RAW TagConfig blob as FullName, so a DataType/ diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs index 4a4a503e..4ef942e9 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceComposer.cs @@ -171,11 +171,11 @@ public sealed record EquipmentVirtualTagPlan( IReadOnlyList DependencyRefs, bool Historize = false) { - /// Structural equality: the auto-generated record equality would compare - /// (an interface-typed list) BY REFERENCE, flagging every - /// VirtualTag as "changed" on every parse (fresh list instances). Compare it element-wise - /// so a no-op redeploy diffs empty. is included so a Historize-only - /// toggle is detected as a change. + /// + // Structural equality: the auto-generated record equality would compare DependencyRefs (an + // interface-typed list) BY REFERENCE, flagging every VirtualTag as "changed" on every parse + // (fresh list instances). Compare it element-wise so a no-op redeploy diffs empty. Historize + // is included so a Historize-only toggle is detected as a change. public bool Equals(EquipmentVirtualTagPlan? other) => other is not null && VirtualTagId == other.VirtualTagId && @@ -187,6 +187,7 @@ public sealed record EquipmentVirtualTagPlan( Historize == other.Historize && DependencyRefs.SequenceEqual(other.DependencyRefs, StringComparer.Ordinal); + /// public override int GetHashCode() { var hash = new HashCode(); @@ -238,17 +239,15 @@ public sealed record EquipmentScriptedAlarmPlan( bool Retain, bool Enabled) { - /// Structural equality: the auto-generated record equality would compare - /// (an interface-typed list) BY REFERENCE, flagging every alarm as - /// "changed" on every parse (fresh list instances). Compare it element-wise so a no-op redeploy - /// diffs empty (mirrors ). - /// - /// DependencyRefs equality is order-sensitive (SequenceEqual). - /// is the canonical, deterministic - /// producer of that order (predicate ctx.GetTag reads first, then first-seen message - /// template tokens). Downstream byte-parity between the live composer and the artifact-decode - /// mirror depends on both sides calling ExtractAlarmDependencyRefs with identical inputs. - /// + /// + // Structural equality: the auto-generated record equality would compare DependencyRefs (an + // interface-typed list) BY REFERENCE, flagging every alarm as "changed" on every parse (fresh + // list instances). Compare it element-wise so a no-op redeploy diffs empty (mirrors + // EquipmentVirtualTagPlan). DependencyRefs equality is order-sensitive (SequenceEqual). + // EquipmentScriptPaths.ExtractAlarmDependencyRefs is the canonical, deterministic producer of + // that order (predicate ctx.GetTag reads first, then first-seen message template tokens). + // Downstream byte-parity between the live composer and the artifact-decode mirror depends on + // both sides calling ExtractAlarmDependencyRefs with identical inputs. public bool Equals(EquipmentScriptedAlarmPlan? other) => other is not null && ScriptedAlarmId == other.ScriptedAlarmId && @@ -280,9 +279,9 @@ public sealed record EquipmentScriptedAlarmPlan( /// /// Pure composer that flattens the live-edit DB tables into the address-space build plan a /// driver-role host needs. Same inputs → same outputs, no logging, no DB writes. The driver-role -/// startup (Task 53) consumes the result and hands it to the node-manager factory. +/// startup consumes the result and hands it to the node-manager factory. /// -/// #85 — the composer now carries UNS topology ( + +/// The composer also carries UNS topology ( + /// ) so AddressSpaceApplier can build the /// Area/Line/Equipment folder hierarchy in the SDK's address space. The legacy /// EquipmentNodeWalker integration that did this server-side is fully replaced by the @@ -597,6 +596,8 @@ public static class AddressSpaceComposer /// Parses the optional alarm object from a tag's TagConfig JSON. Returns null /// when absent, non-object, or non-JSON (the tag is then a plain variable). Never throws. The /// artifact-decode side (DeploymentArtifact.ExtractTagAlarm) MUST parse identically (byte-parity). + /// The tag's raw TagConfig JSON (nullable/blank tolerated). + /// The parsed native-alarm intent, or when the tag has no alarm object. internal static EquipmentTagAlarmInfo? ExtractTagAlarm(string? tagConfig) { if (string.IsNullOrWhiteSpace(tagConfig)) return null; @@ -627,6 +628,8 @@ public static class AddressSpaceComposer /// resolved later). The raw string value is used — not trimmed — matching ExtractTagFullName / /// ExtractTagAlarm. Never throws. The artifact-decode side /// (DeploymentArtifact.ExtractTagHistorize) MUST parse identically (byte-parity). + /// The tag's raw TagConfig JSON (nullable/blank tolerated). + /// The parsed historize flag and optional historian tagname override. internal static (bool IsHistorized, string? HistorianTagname) ExtractTagHistorize(string? tagConfig) { if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null); @@ -657,6 +660,8 @@ public static class AddressSpaceComposer /// exactly in structure + null/blank/non-object/malformed-JSON /// tolerance. Never throws. The artifact-decode side /// (DeploymentArtifact.ExtractTagArray) MUST parse identically (byte-parity). + /// The tag's raw TagConfig JSON (nullable/blank tolerated). + /// The parsed array flag and optional array length. internal static (bool IsArray, uint? ArrayLength) ExtractTagArray(string? tagConfig) { if (string.IsNullOrWhiteSpace(tagConfig)) return (false, null); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpacePlan.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpacePlan.cs index 815b1590..b75901a8 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpacePlan.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpacePlan.cs @@ -53,7 +53,7 @@ public sealed record AddressSpacePlan( public IReadOnlyList ChangedEquipmentVirtualTags { get; init; } = Array.Empty(); /// - /// OpcUaServer-001 — UNS Area / Line folder renames: a folder whose stable id is unchanged but + /// UNS Area / Line folder renames: a folder whose stable id is unchanged but /// whose DisplayName differs between the previous + next composition. A deploy whose ONLY /// change is an Area or Line rename produces NO Equipment / Driver / Alarm / Tag / VirtualTag /// delta, so without this set the plan would be and @@ -84,7 +84,7 @@ public sealed record AddressSpacePlan( public sealed record EquipmentTagDelta(EquipmentTagPlan Previous, EquipmentTagPlan Current); public sealed record EquipmentVirtualTagDelta(EquipmentVirtualTagPlan Previous, EquipmentVirtualTagPlan Current); - /// OpcUaServer-001 — one renamed UNS Area / Line folder: the stable folder + /// One renamed UNS Area / Line folder: the stable folder /// (the area's UnsAreaId or line's UnsLineId, the exact /// NodeId MaterialiseHierarchy placed the folder at) and the /// to apply in place. @@ -103,6 +103,7 @@ public static class AddressSpacePlanner /// /// The previous composition result. /// The new composition result. + /// The diff plan between the two compositions. public static AddressSpacePlan Compute(AddressSpaceComposition previous, AddressSpaceComposition next) { ArgumentNullException.ThrowIfNull(previous); @@ -138,7 +139,7 @@ public static class AddressSpacePlanner t => t.VirtualTagId, (a, b) => new AddressSpacePlan.EquipmentVirtualTagDelta(a, b)); - // OpcUaServer-001 — UNS Area / Line renames: a folder whose stable id is unchanged but whose + // UNS Area / Line renames: a folder whose stable id is unchanged but whose // DisplayName differs. Diffed by stable id (UnsAreaId / UnsLineId) so an Area/Line whose ONLY // change is its friendly name is no longer a silent no-op at the IsEmpty gate. The folder NodeId // IS the area/line id (the exact scheme MaterialiseHierarchy uses), so the rename carries it @@ -163,7 +164,7 @@ public static class AddressSpacePlanner } /// - /// OpcUaServer-001 — emit a for every folder present in + /// Emit a for every folder present in /// BOTH snapshots (matched by stable ) whose /// differs (ordinal). Added/removed folders are NOT renames — they're handled by the equipment / /// hierarchy rebuild path — so this pass only flags an in-place display-name change on a surviving diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs index a591d281..8bd0c3a5 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OpcUaApplicationHost.cs @@ -122,6 +122,7 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable /// Starts the OPC UA application and server. /// The standard server instance to start. /// A cancellation token for the operation. + /// A task that represents the asynchronous operation. public async Task StartAsync(StandardServer server, CancellationToken cancellationToken) { _server = server; @@ -377,6 +378,7 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable /// listening endpoints — the misconfiguration is logged and very visible. /// /// The security profiles to build policies for. + /// The SDK security policies for the requested profiles. internal static IEnumerable BuildSecurityPolicies(IEnumerable profiles) { var seen = new HashSet(); @@ -424,6 +426,7 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable /// project supplies the LDAP adapter, and denies all /// UserName logins when none is registered. /// + /// The Anonymous and UserName token policies. internal static IEnumerable BuildUserTokenPolicies() { yield return new UserTokenPolicy(UserTokenType.Anonymous) @@ -439,6 +442,7 @@ public sealed class OpcUaApplicationHost : IAsyncDisposable } /// Disposes the application host and cleans up resources. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { if (_impersonateHandler is not null && _server?.CurrentInstance?.SessionManager is { } sessionManager) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs index a358d7d7..23c08e95 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs @@ -58,7 +58,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// Keyed by NodeId → the actual so can /// pass the folder to RemoveRootNotifier on teardown. private readonly Dictionary _notifierFolders = new(); - /// Phase C (Task 4): event-notifier folder NodeId-identifier → the event-history source + /// Phase C: event-notifier folder NodeId-identifier → the event-history source /// name passed to . The equipment-folder NodeId /// identifier IS the equipment id, which IS the sourceName, so key and value are the same string; /// the map's presence (not its value) is what makes a folder an event-history source. Populated by @@ -168,7 +168,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// this source — so a single registered historian (e.g. Wonderware) serves many drivers' nodes, /// independent of any driver's lifecycle. /// - /// Set by the Host at StartAsync (Task 5). The + /// Set by the Host at StartAsync. The /// default (assigning null restores it) means "no historian wired" → every read /// returns empty, so a historized node's HistoryRead surfaces GoodNoData rather than /// faulting. Backed by a volatile field (auto-properties can't be volatile) to make @@ -261,7 +261,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) { ArgumentException.ThrowIfNullOrEmpty(nodeId); - EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started. + EnsureAddressSpaceCreated(); lock (Lock) { @@ -297,7 +297,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 { ArgumentException.ThrowIfNullOrEmpty(alarmNodeId); ArgumentNullException.ThrowIfNull(state); - EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started. + EnsureAddressSpaceCreated(); // Look up + project under a SINGLE Lock so a concurrent RebuildAddressSpace can't clear // _alarmConditions / detach the condition node between the lookup and the Set* calls. @@ -573,6 +573,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// Human-readable condition name (BrowseName / DisplayName / Message / ConditionName). /// Domain alarm type — maps to the SDK condition subtype (see remarks). /// Domain severity (treated as an OPC UA 1..1000 severity); mapped to . + /// True when the condition is a NATIVE (driver-fed, e.g. Galaxy) alarm rather than a scripted one. /// /// AlarmType → SDK subtype mapping. Script-driven alarms have no OPC limit / /// setpoint values, so any limit-style subtype would have unset limit children. We therefore @@ -586,7 +587,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 { ArgumentException.ThrowIfNullOrEmpty(alarmNodeId); ArgumentException.ThrowIfNullOrEmpty(displayName); - EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started. + EnsureAddressSpaceCreated(); lock (Lock) { @@ -693,7 +694,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // (ScriptedAlarmEngine.EnableAsync/DisableAsync, dispatched by ScriptedAlarmHostActor on the // "Enable"/"Disable" AlarmCommand operations), so a scripted condition routes through the same // AlarmAck-gated HandleAlarmCommand as the other handlers. NATIVE (driver-fed) conditions have no - // engine enable/disable surface (Phase 3 decision #2) — they short-circuit to BadNotSupported. + // engine enable/disable surface — they short-circuit to BadNotSupported. alarm.OnEnableDisable = (context, condition, enabling) => { if (IsNativeAlarmNode(alarmNodeId)) @@ -720,6 +721,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// (driver-fed) alarm rather than a scripted one. A later task uses this to route a native condition's /// inbound Acknowledge to the driver instead of the scripted engine. /// The alarm condition node id. + /// True when the condition is native (driver-fed); false when it is scripted or not found. internal bool IsNativeAlarmNode(string alarmNodeId) { // _nativeAlarmNodeIds is a plain HashSet mutated only under Lock (in MaterialiseAlarmCondition / @@ -811,7 +813,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// /// The attached to a writable equipment-tag variable by - /// (Task 11). The OPC UA SDK invokes it when a client writes the + /// . The OPC UA SDK invokes it when a client writes the /// node's Value. It resolves the calling principal off the SDK the /// SAME way does, gates on the /// role + the gateway being wired @@ -1230,7 +1232,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// register it as a root notifier (idempotent — guarded by ) so the /// alarm condition has a notifier path to the Server object for T16's event propagation. /// - /// Phase C (Task 4): when a real historian is wired at promotion time (the source is NOT the + /// Phase C: when a real historian is wired at promotion time (the source is NOT the /// ), the folder ALSO gets the /// bit OR-ed in (keeping SubscribeToEvents) and registers /// its NodeId identifier as an event-history source so the override @@ -1272,7 +1274,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// /// Ensure a folder node exists at with the given display /// name, parented under (or the namespace root when null). - /// #85 — used by to materialise the UNS Area/Line/Equipment + /// Used by to materialise the UNS Area/Line/Equipment /// folder hierarchy. Idempotent: the second call with the same id returns the cached /// folder so adding child variables under it still works. /// @@ -1283,7 +1285,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 { ArgumentException.ThrowIfNullOrEmpty(folderNodeId); ArgumentException.ThrowIfNullOrEmpty(displayName); - EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started. + EnsureAddressSpaceCreated(); if (_folders.ContainsKey(folderNodeId)) return; @@ -1310,7 +1312,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// /// Update an EXISTING folder node's in place and notify /// subscribers, WITHOUT a rebuild — the in-place counterpart of for a - /// UNS Area / Line rename (OpcUaServer-001). early-returns for an + /// UNS Area / Line rename. early-returns for an /// already-present folder id and never touches an existing folder's display name, so a /// rename-only deploy would otherwise be invisible until a full /// cleared _folders. The NodeId + BrowseName are unchanged (a rename only touches the @@ -1351,7 +1353,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// The OPC UA data type name (e.g., "Boolean", "Int32", "String"). /// When true the node is created CurrentReadWrite (an authored /// ReadWrite equipment tag) and the inbound-write handler is attached - /// to its OnWriteValue (Task 11) so a client write gates on the WriteOperate role + routes + /// to its OnWriteValue so a client write gates on the WriteOperate role + routes /// to the backing driver; when false it stays CurrentRead (read-only) with no write handler. /// Phase C: null ⇒ the node is NOT historized (Historizing=false, no /// HistoryRead bit, not registered). Non-null ⇒ the node is created Historizing with the @@ -1368,7 +1370,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 { ArgumentException.ThrowIfNullOrEmpty(variableNodeId); ArgumentException.ThrowIfNullOrEmpty(displayName); - EnsureAddressSpaceCreated(); // OpcUaServer-004: fail legibly if called before the server started. + EnsureAddressSpaceCreated(); // If already present, leave it alone (idempotent re-applies). if (_variables.ContainsKey(variableNodeId)) return; @@ -1401,7 +1403,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 StatusCode = StatusCodes.BadWaitingForInitialData, Timestamp = DateTime.MinValue, }; - // Task 11: a writable equipment tag owns an inbound-write handler. The SDK invokes + // A writable equipment tag owns an inbound-write handler. The SDK invokes // OnWriteValue on a client write; it gates on the WriteOperate role and routes to the backing // driver via NodeWriteGateway. Read-only nodes leave the handler unattached so a write is // rejected by the SDK's own AccessLevel check before it ever reaches a handler. @@ -1437,7 +1439,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// in place and notify subscribers, WITHOUT a rebuild — so client MonitoredItems on the node survive. /// Returns false when the node id is unknown (caller falls back to a full rebuild). /// - /// FB-7: when the shape (DataType / ValueRank / ArrayDimensions) ACTUALLY changes, two extra things + /// When the shape (DataType / ValueRank / ArrayDimensions) ACTUALLY changes, two extra things /// happen. (1) The node's value is reset to BadWaitingForInitialData (Value=null) so no value /// still typed to the OLD DataType is exposed between this update and the driver's next publish — the /// same fresh-node state creates, which closes the "brief value-type @@ -1476,7 +1478,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 if (historized) _historizedTagnames[variableNodeId] = historianTagname!; else _historizedTagnames.TryRemove(variableNodeId, out _); - // FB-7: swap DataType + ValueRank + ArrayDimensions in place, but only when they actually differ + // Swap DataType + ValueRank + ArrayDimensions in place, but only when they actually differ // from the live node — a Writable/Historizing-only change leaves the shape untouched (no value // reset, no model-change event), preserving the original surgical behaviour byte-for-byte. var newDataType = ResolveBuiltInDataType(dataType); @@ -1727,7 +1729,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // Drop the notifier-folder guard so re-materialised alarms re-promote their (rebuilt) // equipment folders to event notifiers. _notifierFolders.Clear(); - // Phase C (Task 4): drop the event-history source registrations alongside the notifier folders + // Phase C: drop the event-history source registrations alongside the notifier folders // they map; re-materialised alarms re-register them (with the HistoryRead bit) on re-promotion. _eventNotifierSources.Clear(); } @@ -1740,7 +1742,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 return _folders.TryGetValue(parentNodeId, out var existing) ? existing : _root!; } - /// OpcUaServer-004: guard the address-space mutators against a too-early call. _root + /// Guard the address-space mutators against a too-early call. _root /// is only assigned in , which the SDK invokes during /// StandardServer start; every public mutator (, /// , , , @@ -1766,7 +1768,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // that carry the HistoryRead access bit), validates the timestamp args, handles // `releaseContinuationPoints`, and dispatches by `details` runtime type to the per-details // protected virtuals below. We override all four arms: the three variable-history virtuals - // (Raw/Processed/AtTime) and the event-history arm (HistoryReadEvents, Task 4). Each override + // (Raw/Processed/AtTime) and the event-history arm (HistoryReadEvents). Each override // receives the pre-filtered handles and fills results[handle.Index] / errors[handle.Index] — // handle.Index is the original index into the service-level results/errors lists, seeded by the // base. The base pre-seeds every handle's error to BadHistoryOperationUnsupported, so a handle @@ -1778,18 +1780,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 // backend throw) never throws out of the batch. // --------------------------------------------------------------------------------------------- - /// - /// Serve a HistoryRead-Raw request over the pre-filtered historized variable handles, dispatching - /// each to . Modified-history reads - /// (IsReadModified) are unsupported — we don't serve a modified-value history surface. - /// - /// Raw is the only arm that pages server-side: ReadRawModifiedDetails carries a client - /// count cap (NumValuesPerNode), so a page that returns exactly that many samples MAY - /// have more behind it ⇒ a time-based continuation point is emitted (see - /// ). An inbound continuation point on a node resumes its stored - /// read. NumValuesPerNode == 0 ("all values") never pages. - /// - /// + /// protected override void HistoryReadRawModified( ServerSystemContext context, ReadRawModifiedDetails details, @@ -1816,15 +1807,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } } - /// - /// Serve a HistoryRead-Processed request, mapping each node's per-node aggregate NodeId (from the - /// parallel AggregateType collection — the base guarantees it is the same length as - /// nodesToRead) to a and dispatching to - /// . An unknown aggregate yields - /// BadAggregateNotSupported for that node. Single-shot (no continuation point): - /// ReadProcessedDetails carries no client count cap — the bucket count is deterministic - /// (window / interval) — so there is no "full page" signal to page on. - /// + /// protected override void HistoryReadProcessed( ServerSystemContext context, ReadProcessedDetails details, @@ -1864,12 +1847,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } } - /// - /// Serve a HistoryRead-AtTime request, dispatching the requested timestamps to - /// . Single-shot (no continuation point): - /// AtTime carries no client count cap — the request IS the timestamp list and the result is - /// exactly one sample per requested timestamp — so there is no "full page" signal to page on. - /// + /// protected override void HistoryReadAtTime( ServerSystemContext context, ReadAtTimeDetails details, @@ -1891,18 +1869,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } } - /// - /// Serve a HistoryRead-Events request over the equipment-folder event-notifier nodes (the folders - /// that own alarm conditions). Each handle's NodeId identifier is resolved against - /// : a miss ⇒ BadHistoryOperationUnsupported (a node we own - /// that isn't a registered event-history source — e.g. a plain folder, or one promoted while no - /// historian was wired); a hit block-bridges to - /// for the folder's source name and projects each into a - /// per the request's event filter. Like the Raw/Processed/AtTime - /// arms this is NOT invoked under the node-manager Lock, so the block-bridge is safe; each - /// handle is served under try/catch so a backend throw becomes a Bad status for THAT node only and - /// never throws out of the batch. - /// + /// protected override void HistoryReadEvents( ServerSystemContext context, ReadEventDetails details, @@ -1936,7 +1903,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 sourceName, details.StartTime, details.EndTime, - // OpcUaServer-002: NumValuesPerNode==0 means "no limit — return ALL values" per OPC UA + // NumValuesPerNode==0 means "no limit — return ALL values" per OPC UA // Part 4/11, so translate it to UNBOUNDED (int.MaxValue) here. Passing the int<=0 // backend-default-cap sentinel instead would silently truncate a "give me everything" // events read at the backend default. A positive cap passes through (clamped). @@ -1974,7 +1941,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 /// The clamped non-negative int. private static int ClampToInt(uint value) => value > int.MaxValue ? int.MaxValue : (int)value; - /// OpcUaServer-002: map a HistoryRead-Events NumValuesPerNode request cap onto the + /// Map a HistoryRead-Events NumValuesPerNode request cap onto the /// maxEvents argument, honouring the OPC UA /// Part 4/11 semantics that NumValuesPerNode == 0 means "no limit — return ALL values". /// We translate 0 to UNBOUNDED () — a very large positive cap — rather than @@ -2315,14 +2282,7 @@ public sealed class OtOpcUaNodeManager : CustomNodeManager2 } } - /// - /// Drop the resume state for any continuation points the client asked to release - /// (releaseContinuationPoints == true) and return WITHOUT reading data, per OPC UA Part 4. - /// The base dispatcher routes a release-only HistoryRead here (it never reaches the per-details - /// arms), so this is the single place that must free Raw's stored cursors. Each handle's released - /// point is nodesToRead[handle.Index].ContinuationPoint; releasing an unknown / null point - /// is a harmless no-op. Errors are left Good (the base pre-seeds them) — a release does not fail. - /// + /// protected override void HistoryReleaseContinuationPoints( ServerSystemContext context, IList nodesToRead, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs index 995b0269..9611c9fc 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs @@ -20,76 +20,37 @@ public sealed class SdkAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgicalAddre _nodeManager = nodeManager; } - /// Writes a value to the OPC UA address space. - /// The OPC UA node identifier. - /// The value being written. - /// The OPC UA quality status. - /// The source timestamp in UTC. + /// public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) => _nodeManager.WriteValue(nodeId, value, quality, sourceTimestampUtc); - /// Writes the full Part 9 alarm-condition state to the OPC UA address space. - /// The alarm node identifier. - /// The full condition state to project onto the node. - /// The source timestamp in UTC. + /// public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc) => _nodeManager.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc); - /// Materialises a real Part 9 alarm-condition node in the address space. - /// The alarm node identifier (== ScriptedAlarmId). - /// The equipment folder node identifier the condition parents under. - /// The human-readable condition name. - /// The domain alarm type. - /// The domain severity. - /// True for a driver-fed (native) equipment-tag alarm; false (default) for a scripted alarm. + /// public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false) => _nodeManager.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative); - /// Ensures a folder node exists in the address space. - /// The folder node identifier. - /// The parent folder node identifier. - /// The display name for the folder. + /// public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) => _nodeManager.EnsureFolder(folderNodeId, parentNodeId, displayName); - /// Ensures a variable node exists in the address space. - /// The variable node identifier. - /// The parent folder node identifier. - /// The display name for the variable. - /// The OPC UA data type. - /// When true the node is created read/write; otherwise read-only. - /// null ⇒ not historized; non-null ⇒ create Historizing with the - /// HistoryRead access bit and register the historian tagname. - /// When true the node is created as a 1-D array; when false (default) scalar. - /// The declared length of the 1-D array when is true. + /// public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null) => _nodeManager.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength); - /// F10b: surgically update an existing variable node's Writable + Historizing + presentation - /// shape (DataType / array-ness) in place (no rebuild). Returns false when the node does not exist - /// (caller falls back to a full rebuild). - /// The variable node identifier. - /// When true the node becomes read/write with the inbound-write handler; otherwise read-only. - /// null ⇒ not historized; non-null ⇒ Historizing with the HistoryRead bit and tagname binding. - /// The OPC UA built-in data type name to apply in place. - /// When true the node becomes a 1-D array; when false scalar. - /// The declared length of the 1-D array when is true. + /// public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength) => _nodeManager.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength); - /// OpcUaServer-001: surgically update an existing folder node's display name in place (no - /// rebuild) for a UNS Area / Line rename. Returns false when the folder does not exist (caller falls - /// back to a full rebuild). - /// The folder node identifier whose display name to update in place. - /// The new display name to apply. - /// True when the in-place update was applied; false when the folder is missing. + /// public bool UpdateFolderDisplayName(string folderNodeId, string displayName) => _nodeManager.UpdateFolderDisplayName(folderNodeId, displayName); - /// Rebuilds the entire OPC UA address space. + /// public void RebuildAddressSpace() => _nodeManager.RebuildAddressSpace(); - /// Announces a runtime NodeAdded model-change (discovered-node injection) to subscribed clients. - /// The node under which discovered nodes were added. + /// public void RaiseNodesAddedModelChange(string affectedNodeId) => _nodeManager.RaiseNodesAddedModelChange(affectedNodeId); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkServiceLevelPublisher.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkServiceLevelPublisher.cs index b3f34461..e3d8f708 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkServiceLevelPublisher.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkServiceLevelPublisher.cs @@ -32,8 +32,7 @@ public sealed class SdkServiceLevelPublisher : IServiceLevelPublisher _logger = logger; } - /// Publishes the service level to the OPC UA Server object. - /// The service level value to publish. + /// public void Publish(byte serviceLevel) { var node = _serverInternal.ServerObject?.ServiceLevel; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/IOpcUaUserAuthenticator.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/IOpcUaUserAuthenticator.cs index 7d647dca..1f136333 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/IOpcUaUserAuthenticator.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/IOpcUaUserAuthenticator.cs @@ -55,11 +55,7 @@ public sealed class NullOpcUaUserAuthenticator : IOpcUaUserAuthenticator public static readonly NullOpcUaUserAuthenticator Instance = new(); private NullOpcUaUserAuthenticator() { } - /// Authenticates a username (always denies in this null implementation). - /// The username to authenticate. - /// The cleartext password to authenticate. - /// The cancellation token. - /// A task that returns a denial result indicating no authenticator is configured. + /// public Task AuthenticateUserNameAsync(string username, string password, CancellationToken ct) => Task.FromResult(OpcUaUserAuthResult.Deny("No UserName authenticator is configured on this server.")); } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/OpcUaDataPlaneRoles.cs b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/OpcUaDataPlaneRoles.cs index f8666d56..5ed2968c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/OpcUaDataPlaneRoles.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/OpcUaDataPlaneRoles.cs @@ -25,7 +25,7 @@ public static class OpcUaDataPlaneRoles /// The role that grants authority to write a writable equipment-tag variable node /// (FreeAccess / Operate attributes). A session must carry this role for the inbound - /// OnWriteValue handler (Task 11) to route the value to the backing driver; absent it the + /// OnWriteValue handler to route the value to the backing driver; absent it the /// write is denied with BadUserAccessDenied before any driver call. public const string WriteOperate = "WriteOperate"; } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/CapturingAddressSpaceBuilder.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/CapturingAddressSpaceBuilder.cs index 8ba73869..626807d5 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/CapturingAddressSpaceBuilder.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/CapturingAddressSpaceBuilder.cs @@ -55,13 +55,17 @@ public sealed class CapturingAddressSpaceBuilder : IAddressSpaceBuilder /// A variable handle whose alarm marking is a no-op (discovered alarms are out of scope). private sealed class NullHandle(string fullRef) : IVariableHandle { + /// public string FullReference => fullRef; + + /// public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) => new NullSink(); } /// A null sink that ignores alarm condition transitions. private sealed class NullSink : IAlarmConditionSink { + /// public void OnTransition(AlarmEventArgs args) { } } } diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs index be565639..41788a89 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs @@ -53,6 +53,7 @@ public static class DeploymentArtifact /// Empty / malformed blobs return an empty list — callers log + treat as "no drivers". /// /// The deployment artifact blob to parse. + /// The parsed driver-instance specs, or an empty list for empty/malformed blobs. public static IReadOnlyList ParseDriverInstances(ReadOnlySpan blob) { if (blob.IsEmpty) return Array.Empty(); @@ -91,7 +92,7 @@ public static class DeploymentArtifact /// other parsers). /// /// The deployment artifact blob to inspect. - /// The node's identity (e.g. "central-1:4053") to match against Nodes. + /// The node's identity (e.g. a "host:port" string) to match against Nodes. /// The resolved decision for this node. public static ClusterScope ResolveClusterScope(ReadOnlySpan blob, string nodeId) { @@ -133,7 +134,7 @@ public static class DeploymentArtifact /// cluster's specs (or none when the node's row is absent). /// /// The deployment artifact blob to parse. - /// The node's identity (e.g. "central-1:4053") used to resolve cluster scope. + /// The node's identity (e.g. a "host:port" string) used to resolve cluster scope. /// The driver-instance specs this node should spawn. public static IReadOnlyList ParseDriverInstances(ReadOnlySpan blob, string nodeId) { @@ -149,7 +150,7 @@ public static class DeploymentArtifact /// NOT a JSON string (number / bool / object / array) yields null rather than throwing /// from GetString(). Keeps the artifact-decode lenient /// — a wrong-typed field degrades to the field default / a skipped row, matching the documented - /// "malformed blobs return an empty list/composition" contract (Runtime-002). + /// "malformed blobs return an empty list/composition" contract. private static string? ReadString(JsonElement el, string property) => el.TryGetProperty(property, out var p) && p.ValueKind == JsonValueKind.String ? p.GetString() : null; @@ -190,6 +191,7 @@ public static class DeploymentArtifact /// nodes. /// /// The deployment artifact blob to parse. + /// The projected , or an empty composition for empty/malformed blobs. public static AddressSpaceComposition ParseComposition(ReadOnlySpan blob) { if (blob.IsEmpty) return Empty(); @@ -231,7 +233,7 @@ public static class DeploymentArtifact /// area is in-cluster. /// When is supplied it is invoked with a human-readable message for each /// kept driver-bound equipment whose owning UNS line is NOT in the node's cluster — a cross-cluster binding - /// that violates the same-cluster invariant (decision #122) and would orphan the equipment folder. This is + /// that violates the same-cluster invariant and would orphan the equipment folder. This is /// detection only (observability); the equipment is still returned, since the upstream draft validator /// is the authority that should prevent the binding in the first place. /// The deployment artifact blob. @@ -835,7 +837,7 @@ public static class DeploymentArtifact private static EquipmentNode? ReadEquipmentNode(JsonElement el, IReadOnlyDictionary deviceHostById) { var id = ReadString(el, "EquipmentId"); - // DisplayName = the UNS level-5 Name segment (friendly browse name, matching UnsArea/UnsLine + // DisplayName = the UNS Name segment (friendly browse name, matching UnsArea/UnsLine // + the live rebuild's source of truth) — NOT the colloquial MachineCode. NodeId stays the // logical EquipmentId. var displayName = ReadString(el, "Name"); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs index 34460595..1df6bb83 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs @@ -201,7 +201,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers private IActorRef _mediator = null!; /// - /// Routes an inbound operator write (resolved from the OPC UA node-manager side, Task 11) to the + /// Routes an inbound operator write (resolved from the OPC UA node-manager side) to the /// owning driver child. gates on the local node being the /// driver Primary, resolves the reverse map, and Asks the child a /// carrying the driver-side . @@ -234,11 +234,13 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers private sealed record ChildEntry(IActorRef Actor, DriverInstanceSpec Spec, bool Stubbed) { // Convenience accessors for sites that don't need the full spec. + /// Gets the driver type name from the child's spec. public string DriverType => Spec.DriverType; + /// Gets the last applied driver configuration JSON from the child's spec. public string LastConfigJson => Spec.DriverConfig; } - /// + /// Gets or sets the timer scheduler for this actor's config-db retry and rediscovery timers. public ITimerScheduler Timers { get; set; } = null!; public sealed class RetryConfigDbConnection @@ -279,6 +281,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers /// ScriptedAlarm host instead of spawning a real child, so /// tests can intercept the message. Null /// in production (the real host is spawned). + /// The Akka.NET used to spawn this actor. public static Props Props( IDbContextFactory dbFactory, CommonsNodeId localNode, @@ -676,7 +679,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers /// /// Partitions a multi-device driver's captured FixedTree by its (normalized) device-host folder segment /// (FolderPathSegments[1]) and maps each device's subset under the candidate equipment whose - /// matches — the follow-up E part-2 multi-device graft. Returns + /// matches — the multi-device graft. Returns /// the per-equipment plan map (one entry per device that matched AND had at least one new variable); /// EMPTY when nothing is graftable. /// @@ -696,7 +699,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers /// regardless of case/whitespace. /// Warn-spam taming. The unmatched/ambiguous/degenerate condition is warned ONCE then /// Debug-logged on the repeated re-discovery passes (see ). - /// Mid-connect partition shrink (M2). If a later pass yields FEWER device partitions than a + /// Mid-connect partition shrink. If a later pass yields FEWER device partitions than a /// prior pass within the same connect, the dropped partition's routes + materialised nodes are NOT /// actively pruned until the next full redeploy ( Clears + rebuilds /// the maps). This matches the existing "FixedTree grows-then-stabilises within a connect" assumption — @@ -995,7 +998,7 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers }; /// - /// Routes an inbound operator write (Task 11 Asks this from the OPC UA node-manager side) to the + /// Routes an inbound operator write (Asked from the OPC UA node-manager side) to the /// owning driver child. Order matters: /// /// PRIMARY gate FIRST — reuses the same redundancy signal the @@ -1137,8 +1140,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers _log.Debug("DriverHost {Node}: dropping native-alarm ack for {Node2} while Stale (config DB unreachable)", _localNode, msg.ConditionNodeId)); // A driver child's post-connect DiscoveredNodesReady can't be injected while Stale (no composition is - // applied yet, so the equipment can't be resolved). Drop it — Task 6's re-discovery loop re-sends it - // and the Task-8 post-recovery re-apply self-heal it once an apply runs (matches the no-op drops above). + // applied yet, so the equipment can't be resolved). Drop it — the re-discovery loop re-sends it + // and the post-recovery re-apply self-heals it once an apply runs (matches the no-op drops above). Receive(_ => { }); Receive(_ => { /* PubSub ack */ }); Timers.StartPeriodicTimer("retry-db", RetryConfigDbConnection.Instance, ReconnectInterval); @@ -1481,8 +1484,8 @@ public sealed class DriverHostActor : ReceiveActor, IWithTimers // redeploy / re-apply (one that runs while the host is alive, so _discoveredByDriver is populated) // would drop the injected FixedTree routes + materialised nodes until the driver happens to reconnect // and re-discover. This loop is INERT on the bootstrap-restore path (RestoreApplied): there the actor - // is freshly constructed so _discoveredByDriver is empty — restart survival comes from Task 6's - // post-connect re-discovery, NOT this re-apply. Re-resolve each cached driver's candidate equipments + // is freshly constructed so _discoveredByDriver is empty — restart survival comes from the + // post-connect re-discovery loop, NOT this re-apply. Re-resolve each cached driver's candidate equipments // from the CURRENT composition (the SAME EquipmentNodes-UNION-EquipmentTags logic HandleDiscoveredNodes // uses), then validate each cached (equipmentId → plan) entry PER ENTRY: drop the entry if its // equipmentId is no longer a resolved candidate for the driver, OR the plan's NodeIds aren't scoped to diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs index c940c004..552f85b1 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs @@ -69,7 +69,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers /// Sets the set of references this driver should keep subscribed for the lifetime of the /// current deployment. Unlike the one-shot , the desired set is /// retained and (re)established automatically every time the actor (re)enters - /// Connected — closing the F8b/#113 "re-subscribe across reconnects" gap and giving + /// Connected — closing the F8b "re-subscribe across reconnects" gap and giving /// a single message to drive the SubscribeBulk pass after an /// apply. Sending an empty set clears the desired subscription. /// @@ -212,6 +212,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers /// Optional interval between post-connect re-discovery passes; defaults to 2 seconds. /// Optional cap on re-discovery passes; defaults to 15. /// Optional per-pass timeout for ; defaults to 30 seconds. + /// A instance configured to create the wrapped . public static Props Props( IDriver driver, TimeSpan? reconnectInterval = null, @@ -241,6 +242,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers /// /// The type identifier of the driver. /// Operational roles configured for this instance. + /// True if the driver should start in DEV-STUB mode; otherwise false. public static bool ShouldStub(string driverType, IEnumerable roles) { var isWindowsOnly = driverType is "Galaxy"; @@ -328,7 +330,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers Receive(msg => InitializeAsync(msg.DriverConfigJson)); // Fast-fail writes while still connecting — without this the inbound WriteAttribute dead-letters // and DriverHostActor.HandleRouteNodeWrite waits its full 8s Ask before reporting a generic - // "write timeout". Synchronous Receive: Sender.Tell on the actor thread is safe (#4a-instance). + // "write timeout". Synchronous Receive: Sender.Tell on the actor thread is safe. Receive(_ => Sender.Tell(new WriteAttributeResult(false, "driver not connected"))); // An ack arriving while still connecting can't reach the upstream alarm system; drop it (the ack is @@ -456,7 +458,7 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers { Receive(_ => InitializeAsync(_currentConfigJson ?? "{}")); // Fast-fail writes while reconnecting (same reason as Connecting — avoids the 8s host Ask - // timeout on an inbound write to a transiently-down driver). Synchronous Receive (#4a-instance). + // timeout on an inbound write to a transiently-down driver). Synchronous Receive. Receive(_ => Sender.Tell(new WriteAttributeResult(false, "driver not connected"))); // An ack arriving while reconnecting can't reach the upstream alarm system; drop it (fire-and-forget, @@ -565,8 +567,8 @@ public sealed class DriverInstanceActor : ReceiveActor, IWithTimers } var request = new[] { new WriteRequest(msg.TagId, msg.Value) }; - // Bound the write so a hung backend can't pin this actor forever — decision #44/#45 keeps - // retry off by default, but a stalled call still needs an answer. + // Bound the write so a hung backend can't pin this actor forever — retry stays + // off by default, but a stalled call still needs an answer. using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); try { diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverSpawnPlan.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverSpawnPlan.cs index 189d2d53..7f2207c8 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverSpawnPlan.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverSpawnPlan.cs @@ -23,6 +23,7 @@ public static class DriverSpawnPlanner /// /// The currently running driver children keyed by ID. /// The target driver instances from the deployment artifact. + /// The computed with spawn, apply-delta, and stop sets. public static DriverSpawnPlan Compute( IReadOnlyDictionary current, IReadOnlyList target) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/DbHealthProbeActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/DbHealthProbeActor.cs index 61762d8d..5ea6c44f 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/DbHealthProbeActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Health/DbHealthProbeActor.cs @@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.Health; /// /// Single-flight cached health probe against the ConfigDb. Reads cached state via /// Ask<DbHealthStatus>; a single SELECT 1 runs at most every RefreshInterval. -/// Consumed by both the host's /health/ready endpoint (Task 54) and +/// Consumed by both the host's /health/ready endpoint and /// RedundancyStateActor's stale calc. /// public sealed class DbHealthProbeActor : ReceiveActor, IWithTimers @@ -28,6 +28,7 @@ public sealed class DbHealthProbeActor : ReceiveActor, IWithTimers /// Creates a Props instance for the DbHealthProbeActor. /// The factory for creating ConfigDb contexts. + /// The Akka for creating this actor. public static Props Props(IDbContextFactory dbFactory) => Akka.Actor.Props.Create(() => new DbHealthProbeActor(dbFactory)); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/HistorianAdapterActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/HistorianAdapterActor.cs index 137d5ed0..93eb4142 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/HistorianAdapterActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Historian/HistorianAdapterActor.cs @@ -128,10 +128,7 @@ public sealed class HistorianAdapterActor : ReceiveActor _ => AlarmSeverity.Critical, }; - /// Subscribes to the redundancy-state topic (so cluster role changes land as - /// and cache this node's role — the historize enqueue is gated to - /// the Primary so the alerts feed doesn't double-write across the warm-redundant pair) and to the - /// alerts topic (so live s are translated + historized). + /// protected override void PreStart() { _mediator = DistributedPubSub.Get(Context.System).Mediator; diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs index c317a8aa..a22ffcad 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs @@ -17,7 +17,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.OpcUa; /// /// Single-threaded bridge between Akka messages and the OPC UA SDK address space. Hosted on -/// the pinned opcua-synchronized-dispatcher (Task 19 HOCON) so the OPC UA SDK sees +/// the pinned opcua-synchronized-dispatcher (HOCON) so the OPC UA SDK sees /// only one thread per actor instance — its session/subscription locks expect strict /// single-threaded access. /// @@ -121,6 +121,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers /// The period between self-driven DB-health refresh ticks (each /// Asks for its cached status); defaults to 5 seconds. No timer is /// started when is null. + /// The configured for creating this actor, pinned to the + /// dispatcher. public static Props Props( IOpcUaAddressSpaceSink? sink = null, IServiceLevelPublisher? serviceLevel = null, @@ -161,6 +163,8 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers /// The period between self-driven DB-health refresh ticks (each /// Asks for its cached status); defaults to 5 seconds. No timer is /// started when is null. + /// The configured for creating this actor in tests, without dispatcher + /// pinning or the DPS subscribe. public static Props PropsForTests( IOpcUaAddressSpaceSink? sink = null, IServiceLevelPublisher? serviceLevel = null, @@ -331,9 +335,6 @@ public sealed class OpcUaPublishActor : ReceiveActor, IWithTimers var outcome = _applier.Apply(plan); _lastApplied = composition; - // #85 — after the plan diff lands, rebuild the UNS folder hierarchy so OPC UA - // clients see Area/Line/Equipment as proper folders. Idempotent; AddressSpaceApplier - // skips folders that already exist with the same node id. _applier.MaterialiseHierarchy(composition); // T14 — scripted alarms get their own pass right after the hierarchy so the equipment // folders they parent under already exist. Materialises real Part 9 AlarmConditionState diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/DependencyMuxTagUpstreamSource.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/DependencyMuxTagUpstreamSource.cs index e8ac68d7..b58e6d7d 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/DependencyMuxTagUpstreamSource.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/DependencyMuxTagUpstreamSource.cs @@ -95,13 +95,11 @@ public sealed class DependencyMuxTagUpstreamSource : ITagUpstreamSource return new DataValueSnapshot(Value: null, StatusCode: StatusBad, SourceTimestampUtc: null, ServerTimestampUtc: now); } + // No-replay contract: a new subscriber does NOT receive a synthetic initial + // notification for the current cached value. Callers that need the current value must + // call ReadTag immediately after subscribing. The engine's cold-start path + // (startup-recovery + read-cache-refill) already does this. /// - /// - /// No-replay contract: a new subscriber does NOT receive a synthetic initial - /// notification for the current cached value. Callers that need the current value must - /// call immediately after subscribing. The engine's cold-start path - /// (startup-recovery + read-cache-refill) already does this. - /// public IDisposable SubscribeTag(string path, Action observer) { ArgumentNullException.ThrowIfNull(path); @@ -152,6 +150,10 @@ public sealed class DependencyMuxTagUpstreamSource : ITagUpstreamSource private readonly string _path; private readonly Action _observer; + /// Creates a handle that deregisters from for on dispose. + /// The upstream source to deregister from. + /// The tag path the observer is subscribed to. + /// The observer callback to deregister. public Subscription(DependencyMuxTagUpstreamSource owner, string path, Action observer) { _owner = owner; @@ -159,6 +161,7 @@ public sealed class DependencyMuxTagUpstreamSource : ITagUpstreamSource _observer = observer; } + /// Deregisters the observer from the owning source. Idempotent — safe to call more than once. public void Dispose() { // Swap _owner to null first so a double-dispose can't deregister twice (the diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/EfAlarmConditionStateStore.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/EfAlarmConditionStateStore.cs index b0bdbe53..9308cbe6 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/EfAlarmConditionStateStore.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/EfAlarmConditionStateStore.cs @@ -18,7 +18,7 @@ namespace ZB.MOM.WW.OtOpcUa.Runtime.ScriptedAlarms; /// /// ActiveState is NOT persisted — the entity has no Active column. On /// it is restored as ; -/// the engine re-derives it from the live predicate on startup (Phase 7 decision #14). +/// the engine re-derives it from the live predicate on startup. /// /// /// LastTransitionUtc ↔ UpdatedAtUtc: the table has no dedicated transition @@ -74,14 +74,6 @@ public sealed class EfAlarmConditionStateStore : IAlarmStateStore } /// - /// - /// Concurrency assumption: saves for a given alarmId are serialized by the - /// owning host actor (one actor owns the engine per equipment). The check-then-insert - /// pattern is therefore safe under that guarantee — two concurrent inserts for the same - /// alarm cannot occur in the live - /// runtime. The catch handles the edge case of a - /// racing concurrent restart during crash recovery. - /// public async Task SaveAsync(AlarmConditionState state, CancellationToken ct) { using var db = await _dbFactory.CreateDbContextAsync(ct).ConfigureAwait(false); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs index 178fe93c..7d93e51d 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ScriptedAlarms/ScriptedAlarmHostActor.cs @@ -130,6 +130,7 @@ public sealed class ScriptedAlarmHostActor : ReceiveActor /// The local cluster node id, used to read this node's /// from the redundancy-state topic so only the Primary publishes the cluster-wide alerts /// transition. Null (the default) leaves the role unknown ⇒ default-emit (single-node deploys + tests). + /// The used to instantiate the actor. public static Props Props( IActorRef publishActor, IActorRef? mux, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Scripting/DpsScriptLogPublisher.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Scripting/DpsScriptLogPublisher.cs index 248d69ad..fa77729c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Scripting/DpsScriptLogPublisher.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Scripting/DpsScriptLogPublisher.cs @@ -37,12 +37,7 @@ public sealed class DpsScriptLogPublisher : IScriptLogPublisher public DpsScriptLogPublisher(Func system) => _system = system ?? throw new ArgumentNullException(nameof(system)); - /// - /// Publishes onto the DPS script-logs topic. Any failure - /// (system not yet ready, mediator unavailable) is swallowed so the logging pipeline is - /// never disrupted by a transient cluster condition. - /// - /// The entry to publish. + /// public void Publish(ScriptLogEntry entry) { try diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs index 9d0d6cf3..6872052e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/ServiceCollectionExtensions.cs @@ -43,6 +43,7 @@ public static class ServiceCollectionExtensions /// Call this BEFORE AddAkka. /// /// The service collection to register with. + /// The same instance for chaining. public static IServiceCollection AddOtOpcUaRuntime(this IServiceCollection services) { services.TryAddSingleton(NullAlarmHistorianSink.Instance); @@ -189,6 +190,7 @@ public static class ServiceCollectionExtensions /// /// /// The Akka configuration builder. + /// The same instance for chaining. public static AkkaConfigurationBuilder WithOtOpcUaRuntimeActors(this AkkaConfigurationBuilder builder) { // Production cluster HOCON (akka.conf) carries this dispatcher block, but consumers that diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs index d40ebeb7..29d1c909 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagActor.cs @@ -43,6 +43,7 @@ public sealed class VirtualTagActor : ReceiveActor /// Optional factory for creating DPS publishers. /// Optional list of dependency tag references; defaults to empty. /// Optional reference to a dependency multiplexer actor. + /// A configured for creating the actor. public static Props Props( string virtualTagId, string expression, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs index 00beb670..10785fd9 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Runtime/VirtualTags/VirtualTagHostActor.cs @@ -58,6 +58,7 @@ public sealed class VirtualTagHostActor : ReceiveActor /// Sink for results whose plan has Historize=true. Null ⇒ /// (no durable historian wired), so existing call sites /// compile unchanged and never historize. + /// The used to spawn a . public static Props Props(IActorRef publishActor, IActorRef? mux, IVirtualTagEvaluator evaluator, IHistoryWriter? historyWriter = null) => Akka.Actor.Props.Create(() => new VirtualTagHostActor(publishActor, mux, evaluator, historyWriter)); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Auth/AutoLoginAuthenticationHandler.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Auth/AutoLoginAuthenticationHandler.cs index 0ef99825..6b509a96 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Auth/AutoLoginAuthenticationHandler.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Auth/AutoLoginAuthenticationHandler.cs @@ -33,9 +33,14 @@ public sealed class AutoLoginAuthenticationHandler => _opts = disableLoginOptions.Value; /// No-op: auto-login writes no cookie, so an explicit sign-in has nothing to persist. + /// The principal that would be signed in. + /// Authentication properties that would be persisted. + /// A task that represents the asynchronous operation. public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties? properties) => Task.CompletedTask; /// No-op: there is no auth cookie to clear; the next request re-authenticates via this handler. + /// Authentication properties that would be cleared. + /// A task that represents the asynchronous operation. public Task SignOutAsync(AuthenticationProperties? properties) => Task.CompletedTask; /// diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Blazor/CookieAuthenticationStateProvider.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Blazor/CookieAuthenticationStateProvider.cs index cdbd7b39..0e42b789 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Blazor/CookieAuthenticationStateProvider.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Blazor/CookieAuthenticationStateProvider.cs @@ -65,7 +65,8 @@ public sealed class CookieAuthenticationStateProvider : AuthenticationStateProvi } } - /// + /// Cancels the background ping loop, awaits its completion, and disposes the cancellation token source. + /// A task that represents the asynchronous operation. public async ValueTask DisposeAsync() { _cts.Cancel(); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Endpoints/AuthEndpoints.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Endpoints/AuthEndpoints.cs index 9bb01e02..56ac6ef7 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Endpoints/AuthEndpoints.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Endpoints/AuthEndpoints.cs @@ -132,7 +132,7 @@ public static class AuthEndpoints await http.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal); if (!isForm) return Results.NoContent(); - // Security-001: validate returnUrl is a relative (local) URL before redirecting. + // Validate returnUrl is a relative (local) URL before redirecting. // An attacker can supply an absolute URL (e.g. https://evil.com) via a crafted link; // LoginCard echoes the value verbatim into the hidden form field. Fall back to the // app root rather than following the external URL (open-redirect prevention). diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Jwt/JwtTokenService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Jwt/JwtTokenService.cs index 89981d9e..4dd38a2e 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Jwt/JwtTokenService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Jwt/JwtTokenService.cs @@ -135,6 +135,7 @@ public sealed class JwtTokenService /// required pairing whenever a JwtBearer scheme is wired. /// /// + /// The used to validate tokens minted by . public TokenValidationParameters BuildValidationParameters() => new() { ValidateIssuer = true, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/LdapOptions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/LdapOptions.cs index f750df7a..5b5cdde1 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/LdapOptions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/LdapOptions.cs @@ -34,7 +34,7 @@ public sealed class LdapOptions /// Transport security for the LDAP connection — (implicit /// TLS), (upgrade), or /// (plaintext, dev/test only — requires ). Replaces the former - /// UseTls bool (Task 1.4): true, + /// UseTls bool: true, /// false. /// public LdapTransport Transport { get; set; } = LdapTransport.None; @@ -78,7 +78,7 @@ public sealed class LdapOptions /// /// Maps LDAP group name → Admin role. Group match is case-insensitive. A user gets every /// role whose source group is in their membership list. Values are the canonical control-plane - /// roles (Task 1.7). Example dev mapping: + /// roles. Example dev mapping: /// "ReadOnly":"Viewer","ReadWrite":"Designer","AlarmAck":"Administrator" /// public Dictionary GroupToRole { get; set; } = new(StringComparer.OrdinalIgnoreCase); @@ -90,6 +90,7 @@ public sealed class LdapOptions /// handled by the app wrapper around the library service; is carried /// through so the library's own feature gate stays consistent with the app master switch. /// + /// The projected for the shared LDAP client library. public LibLdapOptions ToLibraryOptions() => new() { Enabled = Enabled, diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaGroupRoleMapper.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaGroupRoleMapper.cs index d8500c5e..9eed7e47 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaGroupRoleMapper.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaGroupRoleMapper.cs @@ -20,7 +20,13 @@ public sealed class OtOpcUaGroupRoleMapper( IOptions ldapOptions, ILdapGroupRoleMappingService dbMappings) : IGroupRoleMapper { - /// + /// Maps a user's LDAP group memberships to their OtOpcUa roles. + /// The LDAP group names the user is a member of. + /// Cancellation token for the DB lookup. + /// + /// The merged role mapping — the appsettings GroupToRole baseline unioned with + /// any matching system-wide DB grants, with a null . + /// public async Task> MapAsync(IReadOnlyList groups, CancellationToken ct) { ArgumentNullException.ThrowIfNull(groups); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaLdapAuthService.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaLdapAuthService.cs index 0739a372..373ee4f4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaLdapAuthService.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/OtOpcUaLdapAuthService.cs @@ -29,7 +29,7 @@ namespace ZB.MOM.WW.OtOpcUa.Security.Ldap; /// /// Fail-closed: the library never throws, and this wrapper adds no new throwing paths. The /// DevStub result grants the canonical "Administrator" control-plane role (group -/// "dev") so the dev session can navigate the full Admin UI (Task 1.7 renamed the prior +/// "dev") so the dev session can navigate the full Admin UI (renamed from the prior /// "FleetAdmin" to the canonical "Administrator"). /// public sealed class OtOpcUaLdapAuthService : ILdapAuthService @@ -80,7 +80,7 @@ public sealed class OtOpcUaLdapAuthService : ILdapAuthService { // Dev bypass: accept any non-empty credentials and grant Administrator WITHOUT a real bind. // Pre-populated Roles are unioned with the mapper output by both consumers, so the grant - // survives the move to IGroupRoleMapper. (Task 1.7 canonicalized the role string from the + // survives the move to IGroupRoleMapper. (The role string was canonicalized from the // prior "FleetAdmin" to "Administrator".) _logger.LogWarning( "OtOpcUaLdapAuthService: DevStubMode bypass — accepting {User} without a real LDAP bind", username); @@ -90,7 +90,7 @@ public sealed class OtOpcUaLdapAuthService : ILdapAuthService // Fail closed on a plaintext transport unless explicitly opted in. The bespoke service // enforced this at login (not startup), so the host still boots with an insecure-by-default // config and only refuses the bind here — preserved verbatim after the UseTls→Transport - // migration (Task 1.4). The shared library's directory client does not re-check this. + // migration. The shared library's directory client does not re-check this. if (_options.Transport == LdapTransport.None && !_options.AllowInsecure) return new(false, null, username, [], [], "Insecure LDAP is disabled. Enable a TLS transport or set AllowInsecure for dev/test."); diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/RoleMapper.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/RoleMapper.cs index 8e0716e6..bea5921c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/RoleMapper.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/Ldap/RoleMapper.cs @@ -33,6 +33,7 @@ public static class RoleMapper /// /// Roles already resolved from appsettings (or the dev stub). /// LdapGroupRoleMapping rows for the user's groups (from GetByGroupsAsync). + /// The merged, de-duplicated list of roles. public static IReadOnlyList Merge( IReadOnlyCollection baselineRoles, IReadOnlyCollection dbRows) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs b/src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs index c64e518f..b01744e2 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Security/ServiceCollectionExtensions.cs @@ -33,6 +33,7 @@ public static class ServiceCollectionExtensions /// /// The service collection. /// The application configuration root. + /// The same service collection, for chaining. public static IServiceCollection AddOtOpcUaAuth(this IServiceCollection services, IConfiguration configuration) { services.AddOptions().Bind(configuration.GetSection(JwtOptions.SectionName)); @@ -148,7 +149,7 @@ public static class ServiceCollectionExtensions // DriverOperator (policy NAME kept stable): may issue Reconnect/Restart commands against // live driver instances from the Admin UI DriverStatusPanel. The role STRINGS it requires - // are the canonical control-plane roles (Task 1.7): Operator (was DriverOperator) and + // are the canonical control-plane roles: Operator (was DriverOperator) and // Administrator (was FleetAdmin). Map LDAP group → role via GroupToRole in appsettings // (e.g. "ot-driver-operator": "Operator"). o.AddPolicy("DriverOperator", policy =>