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.md — soft = 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 =>