1 Commits

Author SHA1 Message Date
Joseph Doherty bd6c0b4d3d docs: complete XML doc comments via fixdocs (2757 to 131 findings)
Add missing <returns>/<param>/<summary>/<typeparam> tags and clean up
misused inheritdoc across 481 files so the documented API surface is
complete. Documentation-only (zero code lines changed). The 131 remaining
findings are inheritdoc-style warnings deliberately left to preserve
hand-written implementation rationale (plan-decision notes, race-condition
explanations).
2026-06-03 12:34:34 -04:00
481 changed files with 2550 additions and 1668 deletions
@@ -67,11 +67,13 @@ public abstract class CommandBase : ICommand
/// Executes the command-specific workflow against the configured OPC UA endpoint. /// Executes the command-specific workflow against the configured OPC UA endpoint.
/// </summary> /// </summary>
/// <param name="console">The CLI console used for output and cancellation handling.</param> /// <param name="console">The CLI console used for output and cancellation handling.</param>
/// <returns>A value task that represents the asynchronous command execution.</returns>
public abstract ValueTask ExecuteAsync(IConsole console); public abstract ValueTask ExecuteAsync(IConsole console);
/// <summary> /// <summary>
/// Creates a <see cref="ConnectionSettings" /> from the common command options. /// Creates a <see cref="ConnectionSettings" /> from the common command options.
/// </summary> /// </summary>
/// <returns>A <see cref="ConnectionSettings"/> populated from the current command option values.</returns>
protected ConnectionSettings CreateConnectionSettings() protected ConnectionSettings CreateConnectionSettings()
{ {
var securityMode = SecurityModeMapper.FromString(Security); var securityMode = SecurityModeMapper.FromString(Security);
@@ -97,6 +99,7 @@ public abstract class CommandBase : ICommand
/// and returns both the service and the connection info. /// and returns both the service and the connection info.
/// </summary> /// </summary>
/// <param name="ct">The cancellation token that aborts connection setup for the command.</param> /// <param name="ct">The cancellation token that aborts connection setup for the command.</param>
/// <returns>A tuple of the connected <see cref="IOpcUaClientService"/> and the resulting <see cref="ConnectionInfo"/>.</returns>
protected async Task<(IOpcUaClientService Service, ConnectionInfo Info)> CreateServiceAndConnectAsync( protected async Task<(IOpcUaClientService Service, ConnectionInfo Info)> CreateServiceAndConnectAsync(
CancellationToken ct) CancellationToken ct)
{ {
@@ -12,9 +12,7 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
{ {
private static readonly ILogger Logger = Log.ForContext<DefaultApplicationConfigurationFactory>(); private static readonly ILogger Logger = Log.ForContext<DefaultApplicationConfigurationFactory>();
/// <summary>Creates an OPC UA application configuration from the provided connection settings.</summary> /// <inheritdoc />
/// <param name="settings">The connection settings to use.</param>
/// <param name="ct">Token to cancel the operation.</param>
public async Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct) public async Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct)
{ {
// Resolve the canonical PKI path lazily on first use so constructing a // Resolve the canonical PKI path lazily on first use so constructing a
@@ -11,10 +11,7 @@ internal sealed class DefaultEndpointDiscovery : IEndpointDiscovery
{ {
private static readonly ILogger Logger = Log.ForContext<DefaultEndpointDiscovery>(); private static readonly ILogger Logger = Log.ForContext<DefaultEndpointDiscovery>();
/// <summary>Selects an OPC UA endpoint matching the requested security mode.</summary> /// <inheritdoc />
/// <param name="config">The application configuration.</param>
/// <param name="endpointUrl">The endpoint URL to query.</param>
/// <param name="requestedMode">The requested message security mode.</param>
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
MessageSecurityMode requestedMode) MessageSecurityMode requestedMode)
{ {
@@ -53,6 +50,7 @@ internal static class EndpointSelector
/// Thrown when no endpoint matches <paramref name="requestedMode"/>; the message lists the /// Thrown when no endpoint matches <paramref name="requestedMode"/>; the message lists the
/// security mode + policy combinations the server returned so operators can diagnose mismatches. /// security mode + policy combinations the server returned so operators can diagnose mismatches.
/// </exception> /// </exception>
/// <returns>The best matching <see cref="EndpointDescription"/> with its URL rewritten to the requested host.</returns>
public static EndpointDescription SelectBest( public static EndpointDescription SelectBest(
IEnumerable<EndpointDescription> allEndpoints, IEnumerable<EndpointDescription> allEndpoints,
string endpointUrl, string endpointUrl,
@@ -13,5 +13,6 @@ internal interface IApplicationConfigurationFactory
/// </summary> /// </summary>
/// <param name="settings">The connection settings to configure.</param> /// <param name="settings">The connection settings to configure.</param>
/// <param name="ct">Cancellation token for the operation.</param> /// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A task that resolves to the validated <see cref="ApplicationConfiguration"/>.</returns>
Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct = default); Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct = default);
} }
@@ -14,6 +14,7 @@ internal interface IEndpointDiscovery
/// <param name="config">The OPC UA application configuration.</param> /// <param name="config">The OPC UA application configuration.</param>
/// <param name="endpointUrl">The endpoint URL to discover.</param> /// <param name="endpointUrl">The endpoint URL to discover.</param>
/// <param name="requestedMode">The requested message security mode.</param> /// <param name="requestedMode">The requested message security mode.</param>
/// <returns>The best matching endpoint description for the requested security mode.</returns>
EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl, EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
MessageSecurityMode requestedMode); MessageSecurityMode requestedMode);
} }
@@ -58,6 +58,7 @@ internal interface ISessionAdapter : IDisposable
/// </summary> /// </summary>
/// <param name="nodeId">The node whose current runtime value should be read.</param> /// <param name="nodeId">The node whose current runtime value should be read.</param>
/// <param name="ct">The cancellation token that aborts the server read if the client cancels the request.</param> /// <param name="ct">The cancellation token that aborts the server read if the client cancels the request.</param>
/// <returns>A task that resolves to the current <see cref="DataValue"/> for the node.</returns>
Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default); Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -66,6 +67,7 @@ internal interface ISessionAdapter : IDisposable
/// <param name="nodeId">The node whose value should be updated.</param> /// <param name="nodeId">The node whose value should be updated.</param>
/// <param name="value">The typed OPC UA data value to write to the server.</param> /// <param name="value">The typed OPC UA data value to write to the server.</param>
/// <param name="ct">The cancellation token that aborts the write if the client cancels the request.</param> /// <param name="ct">The cancellation token that aborts the write if the client cancels the request.</param>
/// <returns>A task that resolves to the OPC UA <see cref="StatusCode"/> for the write operation.</returns>
Task<StatusCode> WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct = default); Task<StatusCode> WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -75,6 +77,7 @@ internal interface ISessionAdapter : IDisposable
/// <param name="nodeId">The starting node for the hierarchical browse.</param> /// <param name="nodeId">The starting node for the hierarchical browse.</param>
/// <param name="nodeClassMask">The node classes that should be returned to the caller.</param> /// <param name="nodeClassMask">The node classes that should be returned to the caller.</param>
/// <param name="ct">The cancellation token that aborts the browse request.</param> /// <param name="ct">The cancellation token that aborts the browse request.</param>
/// <returns>A task that resolves to a tuple of an optional continuation point and the returned references.</returns>
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync( Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync(
NodeId nodeId, uint nodeClassMask = 0, CancellationToken ct = default); NodeId nodeId, uint nodeClassMask = 0, CancellationToken ct = default);
@@ -83,6 +86,7 @@ internal interface ISessionAdapter : IDisposable
/// </summary> /// </summary>
/// <param name="continuationPoint">The continuation token returned by a prior browse result page.</param> /// <param name="continuationPoint">The continuation token returned by a prior browse result page.</param>
/// <param name="ct">The cancellation token that aborts the browse-next request.</param> /// <param name="ct">The cancellation token that aborts the browse-next request.</param>
/// <returns>A task that resolves to a tuple of an optional next continuation point and the returned references.</returns>
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync( Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
byte[] continuationPoint, CancellationToken ct = default); byte[] continuationPoint, CancellationToken ct = default);
@@ -91,6 +95,7 @@ internal interface ISessionAdapter : IDisposable
/// </summary> /// </summary>
/// <param name="nodeId">The node to inspect for child objects or variables.</param> /// <param name="nodeId">The node to inspect for child objects or variables.</param>
/// <param name="ct">The cancellation token that aborts the child lookup.</param> /// <param name="ct">The cancellation token that aborts the child lookup.</param>
/// <returns>A task that resolves to <see langword="true"/> if the node has at least one child; otherwise <see langword="false"/>.</returns>
Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct = default); Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -101,6 +106,7 @@ internal interface ISessionAdapter : IDisposable
/// <param name="endTime">The inclusive end of the requested history window.</param> /// <param name="endTime">The inclusive end of the requested history window.</param>
/// <param name="maxValues">The maximum number of raw samples to return to the client.</param> /// <param name="maxValues">The maximum number of raw samples to return to the client.</param>
/// <param name="ct">The cancellation token that aborts the history read.</param> /// <param name="ct">The cancellation token that aborts the history read.</param>
/// <returns>A task that resolves to the ordered list of raw historical data values.</returns>
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
int maxValues, CancellationToken ct = default); int maxValues, CancellationToken ct = default);
@@ -113,6 +119,7 @@ internal interface ISessionAdapter : IDisposable
/// <param name="aggregateId">The OPC UA aggregate function to evaluate over the history window.</param> /// <param name="aggregateId">The OPC UA aggregate function to evaluate over the history window.</param>
/// <param name="intervalMs">The processing interval, in milliseconds, for each aggregate bucket.</param> /// <param name="intervalMs">The processing interval, in milliseconds, for each aggregate bucket.</param>
/// <param name="ct">The cancellation token that aborts the aggregate history read.</param> /// <param name="ct">The cancellation token that aborts the aggregate history read.</param>
/// <returns>A task that resolves to the ordered list of processed aggregate data values.</returns>
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
NodeId aggregateId, double intervalMs, CancellationToken ct = default); NodeId aggregateId, double intervalMs, CancellationToken ct = default);
@@ -121,6 +128,7 @@ internal interface ISessionAdapter : IDisposable
/// </summary> /// </summary>
/// <param name="publishingIntervalMs">The requested publishing interval for monitored items on the new subscription.</param> /// <param name="publishingIntervalMs">The requested publishing interval for monitored items on the new subscription.</param>
/// <param name="ct">The cancellation token that aborts subscription creation.</param> /// <param name="ct">The cancellation token that aborts subscription creation.</param>
/// <returns>A task that resolves to the newly created <see cref="ISubscriptionAdapter"/>.</returns>
Task<ISubscriptionAdapter> CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct = default); Task<ISubscriptionAdapter> CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -130,11 +138,13 @@ internal interface ISessionAdapter : IDisposable
/// <param name="methodId">The method node to invoke.</param> /// <param name="methodId">The method node to invoke.</param>
/// <param name="inputArguments">The ordered input arguments supplied to the server method call.</param> /// <param name="inputArguments">The ordered input arguments supplied to the server method call.</param>
/// <param name="ct">The cancellation token that aborts the method invocation.</param> /// <param name="ct">The cancellation token that aborts the method invocation.</param>
/// <returns>A task that resolves to the list of output arguments returned by the method, or <see langword="null"/> if none.</returns>
Task<IList<object>?> CallMethodAsync(NodeId objectId, NodeId methodId, object[] inputArguments, CancellationToken ct = default); Task<IList<object>?> CallMethodAsync(NodeId objectId, NodeId methodId, object[] inputArguments, CancellationToken ct = default);
/// <summary> /// <summary>
/// Closes the underlying session gracefully before the adapter is disposed or replaced during failover. /// Closes the underlying session gracefully before the adapter is disposed or replaced during failover.
/// </summary> /// </summary>
/// <param name="ct">The cancellation token that aborts the close request.</param> /// <param name="ct">The cancellation token that aborts the close request.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task CloseAsync(CancellationToken ct = default); Task CloseAsync(CancellationToken ct = default);
} }
@@ -28,6 +28,7 @@ internal interface ISubscriptionAdapter : IDisposable
/// </summary> /// </summary>
/// <param name="clientHandle">The client handle returned when the monitored item was created.</param> /// <param name="clientHandle">The client handle returned when the monitored item was created.</param>
/// <param name="ct">The cancellation token that aborts the monitored-item removal.</param> /// <param name="ct">The cancellation token that aborts the monitored-item removal.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct = default); Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -46,11 +47,13 @@ internal interface ISubscriptionAdapter : IDisposable
/// Requests a condition refresh for this subscription. /// Requests a condition refresh for this subscription.
/// </summary> /// </summary>
/// <param name="ct">The cancellation token that aborts the condition refresh request.</param> /// <param name="ct">The cancellation token that aborts the condition refresh request.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ConditionRefreshAsync(CancellationToken ct = default); Task ConditionRefreshAsync(CancellationToken ct = default);
/// <summary> /// <summary>
/// Removes all monitored items and deletes the subscription. /// Removes all monitored items and deletes the subscription.
/// </summary> /// </summary>
/// <param name="ct">The cancellation token that aborts subscription deletion.</param> /// <param name="ct">The cancellation token that aborts subscription deletion.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DeleteAsync(CancellationToken ct = default); Task DeleteAsync(CancellationToken ct = default);
} }
@@ -28,6 +28,7 @@ public static class ClientStoragePaths
/// one-shot legacy-folder migration before returning so callers that depend on this /// 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. /// path (PKI store, settings file) find their existing state at the canonical name.
/// </summary> /// </summary>
/// <returns>The absolute path to the client's top-level folder under LocalApplicationData.</returns>
public static string GetRoot() public static string GetRoot()
{ {
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
@@ -37,6 +38,7 @@ public static class ClientStoragePaths
} }
/// <summary>Subfolder for the application's PKI store — used by both CLI + UI.</summary> /// <summary>Subfolder for the application's PKI store — used by both CLI + UI.</summary>
/// <returns>The absolute path to the PKI store subfolder.</returns>
public static string GetPkiPath() => Path.Combine(GetRoot(), "pki"); public static string GetPkiPath() => Path.Combine(GetRoot(), "pki");
/// <summary> /// <summary>
@@ -45,6 +47,7 @@ public static class ClientStoragePaths
/// folder existed + was moved to canonical, false when no migration was needed or /// folder existed + was moved to canonical, false when no migration was needed or
/// canonical was already present. /// canonical was already present.
/// </summary> /// </summary>
/// <returns><see langword="true"/> when the legacy folder was found and moved; <see langword="false"/> when no migration was needed.</returns>
public static bool TryRunLegacyMigration() public static bool TryRunLegacyMigration()
{ {
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
@@ -24,12 +24,14 @@ public interface IOpcUaClientService : IDisposable
/// </summary> /// </summary>
/// <param name="settings">The endpoint, security, and authentication settings used to establish the session.</param> /// <param name="settings">The endpoint, security, and authentication settings used to establish the session.</param>
/// <param name="ct">The cancellation token that aborts the connect workflow.</param> /// <param name="ct">The cancellation token that aborts the connect workflow.</param>
/// <returns>A <see cref="ConnectionInfo"/> describing the active session after a successful connect.</returns>
Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default); Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default);
/// <summary> /// <summary>
/// Disconnects from the active OPC UA endpoint and tears down subscriptions owned by the client. /// Disconnects from the active OPC UA endpoint and tears down subscriptions owned by the client.
/// </summary> /// </summary>
/// <param name="ct">The cancellation token that aborts disconnect cleanup.</param> /// <param name="ct">The cancellation token that aborts disconnect cleanup.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task DisconnectAsync(CancellationToken ct = default); Task DisconnectAsync(CancellationToken ct = default);
/// <summary> /// <summary>
@@ -37,6 +39,7 @@ public interface IOpcUaClientService : IDisposable
/// </summary> /// </summary>
/// <param name="nodeId">The node whose value should be retrieved.</param> /// <param name="nodeId">The node whose value should be retrieved.</param>
/// <param name="ct">The cancellation token that aborts the read request.</param> /// <param name="ct">The cancellation token that aborts the read request.</param>
/// <returns>The current <see cref="DataValue"/> including value, status code, and timestamps.</returns>
Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default); Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -45,6 +48,7 @@ public interface IOpcUaClientService : IDisposable
/// <param name="nodeId">The node whose value should be updated.</param> /// <param name="nodeId">The node whose value should be updated.</param>
/// <param name="value">The raw value supplied by the CLI or UI workflow.</param> /// <param name="value">The raw value supplied by the CLI or UI workflow.</param>
/// <param name="ct">The cancellation token that aborts the write request.</param> /// <param name="ct">The cancellation token that aborts the write request.</param>
/// <returns>The OPC UA <see cref="StatusCode"/> returned by the server for the write operation.</returns>
Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default); Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -52,6 +56,7 @@ public interface IOpcUaClientService : IDisposable
/// </summary> /// </summary>
/// <param name="parentNodeId">The node to browse, or <see cref="ObjectIds.ObjectsFolder"/> when omitted.</param> /// <param name="parentNodeId">The node to browse, or <see cref="ObjectIds.ObjectsFolder"/> when omitted.</param>
/// <param name="ct">The cancellation token that aborts the browse request.</param> /// <param name="ct">The cancellation token that aborts the browse request.</param>
/// <returns>The list of child nodes discovered under the specified parent.</returns>
Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default); Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -60,6 +65,7 @@ public interface IOpcUaClientService : IDisposable
/// <param name="nodeId">The node whose value changes should be monitored.</param> /// <param name="nodeId">The node whose value changes should be monitored.</param>
/// <param name="intervalMs">The monitored-item sampling and publishing interval in milliseconds.</param> /// <param name="intervalMs">The monitored-item sampling and publishing interval in milliseconds.</param>
/// <param name="ct">The cancellation token that aborts subscription creation.</param> /// <param name="ct">The cancellation token that aborts subscription creation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default); Task SubscribeAsync(NodeId nodeId, int intervalMs = 1000, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -67,6 +73,7 @@ public interface IOpcUaClientService : IDisposable
/// </summary> /// </summary>
/// <param name="nodeId">The node whose live-data subscription should be removed.</param> /// <param name="nodeId">The node whose live-data subscription should be removed.</param>
/// <param name="ct">The cancellation token that aborts the unsubscribe request.</param> /// <param name="ct">The cancellation token that aborts the unsubscribe request.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default); Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default);
/// <summary> /// <summary>
@@ -75,18 +82,21 @@ public interface IOpcUaClientService : IDisposable
/// <param name="sourceNodeId">The event source to monitor, or the server object when omitted.</param> /// <param name="sourceNodeId">The event source to monitor, or the server object when omitted.</param>
/// <param name="intervalMs">The publishing interval in milliseconds for the alarm subscription.</param> /// <param name="intervalMs">The publishing interval in milliseconds for the alarm subscription.</param>
/// <param name="ct">The cancellation token that aborts alarm subscription creation.</param> /// <param name="ct">The cancellation token that aborts alarm subscription creation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default); Task SubscribeAlarmsAsync(NodeId? sourceNodeId = null, int intervalMs = 1000, CancellationToken ct = default);
/// <summary> /// <summary>
/// Removes the active alarm subscription. /// Removes the active alarm subscription.
/// </summary> /// </summary>
/// <param name="ct">The cancellation token that aborts alarm subscription cleanup.</param> /// <param name="ct">The cancellation token that aborts alarm subscription cleanup.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UnsubscribeAlarmsAsync(CancellationToken ct = default); Task UnsubscribeAlarmsAsync(CancellationToken ct = default);
/// <summary> /// <summary>
/// Requests retained alarm conditions again so a client can repopulate its alarm list after reconnecting. /// Requests retained alarm conditions again so a client can repopulate its alarm list after reconnecting.
/// </summary> /// </summary>
/// <param name="ct">The cancellation token that aborts the condition refresh request.</param> /// <param name="ct">The cancellation token that aborts the condition refresh request.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RequestConditionRefreshAsync(CancellationToken ct = default); Task RequestConditionRefreshAsync(CancellationToken ct = default);
/// <summary> /// <summary>
@@ -111,6 +121,7 @@ public interface IOpcUaClientService : IDisposable
/// <param name="endTime">The inclusive end of the requested history range.</param> /// <param name="endTime">The inclusive end of the requested history range.</param>
/// <param name="maxValues">The maximum number of raw values to return.</param> /// <param name="maxValues">The maximum number of raw values to return.</param>
/// <param name="ct">The cancellation token that aborts the history read.</param> /// <param name="ct">The cancellation token that aborts the history read.</param>
/// <returns>The raw historical <see cref="DataValue"/> samples in the requested range.</returns>
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime, Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
int maxValues = 1000, CancellationToken ct = default); int maxValues = 1000, CancellationToken ct = default);
@@ -123,6 +134,7 @@ public interface IOpcUaClientService : IDisposable
/// <param name="aggregate">The aggregate function the operator selected for processed history.</param> /// <param name="aggregate">The aggregate function the operator selected for processed history.</param>
/// <param name="intervalMs">The processing interval, in milliseconds, for each aggregate bucket.</param> /// <param name="intervalMs">The processing interval, in milliseconds, for each aggregate bucket.</param>
/// <param name="ct">The cancellation token that aborts the processed history request.</param> /// <param name="ct">The cancellation token that aborts the processed history request.</param>
/// <returns>The processed historical <see cref="DataValue"/> samples computed by the requested aggregate.</returns>
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime, Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default); AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default);
@@ -130,6 +142,7 @@ public interface IOpcUaClientService : IDisposable
/// Reads redundancy status data such as redundancy mode, service level, and partner endpoint URIs. /// Reads redundancy status data such as redundancy mode, service level, and partner endpoint URIs.
/// </summary> /// </summary>
/// <param name="ct">The cancellation token that aborts redundancy inspection.</param> /// <param name="ct">The cancellation token that aborts redundancy inspection.</param>
/// <returns>A <see cref="RedundancyInfo"/> snapshot containing redundancy mode, service level, and partner endpoint URIs.</returns>
Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default); Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default);
/// <summary> /// <summary>
@@ -73,13 +73,13 @@ public sealed class OpcUaClientService : IOpcUaClientService
{ {
} }
/// <inheritdoc /> /// <summary>Raised when subscribed node values change.</summary>
public event EventHandler<DataChangedEventArgs>? DataChanged; public event EventHandler<DataChangedEventArgs>? DataChanged;
/// <inheritdoc /> /// <summary>Raised when an alarm event is received from the server.</summary>
public event EventHandler<AlarmEventArgs>? AlarmEvent; public event EventHandler<AlarmEventArgs>? AlarmEvent;
/// <inheritdoc /> /// <summary>Raised when the connection state changes.</summary>
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged; public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
/// <inheritdoc /> /// <inheritdoc />
@@ -7,8 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
/// </summary> /// </summary>
public sealed class AvaloniaUiDispatcher : IUiDispatcher public sealed class AvaloniaUiDispatcher : IUiDispatcher
{ {
/// <summary>Posts an action to the Avalonia UI thread for execution.</summary> /// <inheritdoc />
/// <param name="action">The action to execute on the UI thread.</param>
public void Post(Action action) public void Post(Action action)
{ {
Dispatcher.UIThread.Post(action); Dispatcher.UIThread.Post(action);
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
public interface ISettingsService public interface ISettingsService
{ {
/// <summary>Loads user settings from persistent storage.</summary> /// <summary>Loads user settings from persistent storage.</summary>
/// <returns>The persisted <see cref="UserSettings"/>, or a default instance if none are saved.</returns>
UserSettings Load(); UserSettings Load();
/// <summary>Saves user settings to persistent storage.</summary> /// <summary>Saves user settings to persistent storage.</summary>
/// <param name="settings">The settings to save.</param> /// <param name="settings">The settings to save.</param>
@@ -19,8 +19,7 @@ public sealed class JsonSettingsService : ISettingsService
WriteIndented = true WriteIndented = true
}; };
/// <summary>Loads user settings from the settings file.</summary> /// <inheritdoc />
/// <returns>The loaded user settings, or a new default instance if load fails.</returns>
public UserSettings Load() public UserSettings Load()
{ {
try try
@@ -37,8 +36,7 @@ public sealed class JsonSettingsService : ISettingsService
} }
} }
/// <summary>Saves user settings to the settings file.</summary> /// <inheritdoc />
/// <param name="settings">The user settings to save.</param>
public void Save(UserSettings settings) public void Save(UserSettings settings)
{ {
try try
@@ -6,8 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
/// </summary> /// </summary>
public sealed class SynchronousUiDispatcher : IUiDispatcher public sealed class SynchronousUiDispatcher : IUiDispatcher
{ {
/// <summary>Executes the action synchronously on the calling thread.</summary> /// <inheritdoc />
/// <param name="action">The action to execute.</param>
public void Post(Action action) public void Post(Action action)
{ {
action(); action();
@@ -195,6 +195,7 @@ public partial class AlarmsViewModel : ObservableObject
/// <summary> /// <summary>
/// Returns the monitored node ID for persistence, or null if not subscribed. /// Returns the monitored node ID for persistence, or null if not subscribed.
/// </summary> /// </summary>
/// <returns>The monitored node ID string, or null if not currently subscribed.</returns>
public string? GetAlarmSourceNodeId() public string? GetAlarmSourceNodeId()
{ {
return IsSubscribed ? MonitoredNodeIdText : null; return IsSubscribed ? MonitoredNodeIdText : null;
@@ -30,6 +30,7 @@ public class BrowseTreeViewModel : ObservableObject
/// <summary> /// <summary>
/// Loads root nodes by browsing with a null parent. /// Loads root nodes by browsing with a null parent.
/// </summary> /// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task LoadRootsAsync() public async Task LoadRootsAsync()
{ {
var results = await _service.BrowseAsync(); var results = await _service.BrowseAsync();
@@ -143,6 +143,7 @@ public partial class SubscriptionsViewModel : ObservableObject
/// </summary> /// </summary>
/// <param name="nodeIdStr">The node ID to subscribe to from the browse tree or persisted settings.</param> /// <param name="nodeIdStr">The node ID to subscribe to from the browse tree or persisted settings.</param>
/// <param name="intervalMs">The monitored-item interval, in milliseconds, for the subscription.</param> /// <param name="intervalMs">The monitored-item interval, in milliseconds, for the subscription.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task AddSubscriptionForNodeAsync(string nodeIdStr, int intervalMs = 1000) public async Task AddSubscriptionForNodeAsync(string nodeIdStr, int intervalMs = 1000)
{ {
if (!IsConnected || string.IsNullOrWhiteSpace(nodeIdStr)) return; if (!IsConnected || string.IsNullOrWhiteSpace(nodeIdStr)) return;
@@ -176,6 +177,7 @@ public partial class SubscriptionsViewModel : ObservableObject
/// <param name="nodeIdStr">The root node whose variables should be subscribed recursively.</param> /// <param name="nodeIdStr">The root node whose variables should be subscribed recursively.</param>
/// <param name="nodeClass">The node class of the starting node so variables can be subscribed immediately.</param> /// <param name="nodeClass">The node class of the starting node so variables can be subscribed immediately.</param>
/// <param name="intervalMs">The monitored-item interval, in milliseconds, used for created subscriptions.</param> /// <param name="intervalMs">The monitored-item interval, in milliseconds, used for created subscriptions.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task AddSubscriptionRecursiveAsync(string nodeIdStr, string nodeClass, int intervalMs = 1000) public Task AddSubscriptionRecursiveAsync(string nodeIdStr, string nodeClass, int intervalMs = 1000)
{ {
return AddSubscriptionRecursiveAsync(nodeIdStr, nodeClass, intervalMs, maxDepth: 10, currentDepth: 0); return AddSubscriptionRecursiveAsync(nodeIdStr, nodeClass, intervalMs, maxDepth: 10, currentDepth: 0);
@@ -211,6 +213,7 @@ public partial class SubscriptionsViewModel : ObservableObject
/// <summary> /// <summary>
/// Returns the node IDs of all active subscriptions for persistence. /// Returns the node IDs of all active subscriptions for persistence.
/// </summary> /// </summary>
/// <returns>The list of node ID strings for all currently active subscriptions.</returns>
public List<string> GetSubscribedNodeIds() public List<string> GetSubscribedNodeIds()
{ {
return ActiveSubscriptions.Select(s => s.NodeId).ToList(); 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. /// Restores subscriptions from a saved list of node IDs.
/// </summary> /// </summary>
/// <param name="nodeIds">The node IDs persisted from a prior UI session.</param> /// <param name="nodeIds">The node IDs persisted from a prior UI session.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task RestoreSubscriptionsAsync(IEnumerable<string> nodeIds) public async Task RestoreSubscriptionsAsync(IEnumerable<string> nodeIds)
{ {
foreach (var nodeId in nodeIds) foreach (var nodeId in nodeIds)
@@ -232,6 +236,7 @@ public partial class SubscriptionsViewModel : ObservableObject
/// </summary> /// </summary>
/// <param name="nodeIdStr">The node ID the operator wants to write.</param> /// <param name="nodeIdStr">The node ID the operator wants to write.</param>
/// <param name="rawValue">The raw text value entered by the operator.</param> /// <param name="rawValue">The raw text value entered by the operator.</param>
/// <returns>A tuple of (success flag, operator-readable message) describing the outcome of the write.</returns>
public async Task<(bool Success, string Message)> ValidateAndWriteAsync(string nodeIdStr, string rawValue) public async Task<(bool Success, string Message)> ValidateAndWriteAsync(string nodeIdStr, string rawValue)
{ {
try try
@@ -43,20 +43,16 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
_subscriber = system.ActorOf(Props.Create(() => new SubscriberActor(this)), "clusterroleinfo-subscriber"); _subscriber = system.ActorOf(Props.Create(() => new SubscriberActor(this)), "clusterroleinfo-subscriber");
} }
/// <summary>Gets the local cluster node identifier.</summary> /// <inheritdoc />
public CommonsNodeId LocalNode => _localNode; public CommonsNodeId LocalNode => _localNode;
/// <summary>Gets the set of roles assigned to the local node.</summary> /// <inheritdoc />
public IReadOnlySet<string> LocalRoles => _localRoles; public IReadOnlySet<string> LocalRoles => _localRoles;
/// <summary>Checks if the local node has a specific role.</summary> /// <inheritdoc />
/// <param name="role">The role name to check.</param>
/// <returns>True if the local node has the specified role; otherwise false.</returns>
public bool HasRole(string role) => _localRoles.Contains(role); public bool HasRole(string role) => _localRoles.Contains(role);
/// <summary>Gets all cluster members that have a specific role.</summary> /// <inheritdoc />
/// <param name="role">The role name.</param>
/// <returns>A read-only list of node IDs with the specified role.</returns>
public IReadOnlyList<CommonsNodeId> MembersWithRole(string role) public IReadOnlyList<CommonsNodeId> MembersWithRole(string role)
{ {
lock (_lock) lock (_lock)
@@ -68,9 +64,7 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
} }
} }
/// <summary>Gets the current leader node for a specific role.</summary> /// <inheritdoc />
/// <param name="role">The role name.</param>
/// <returns>The node ID of the current role leader, or null if no leader is elected.</returns>
public CommonsNodeId? RoleLeader(string role) public CommonsNodeId? RoleLeader(string role)
{ {
lock (_lock) lock (_lock)
@@ -9,6 +9,7 @@ public static class RoleParser
/// <summary>Parses a comma-separated string of role names into a validated array.</summary> /// <summary>Parses a comma-separated string of role names into a validated array.</summary>
/// <param name="raw">The raw role string to parse.</param> /// <param name="raw">The raw role string to parse.</param>
/// <returns>An array of validated, distinct, lower-cased role names; empty array when the input is null or whitespace.</returns>
public static string[] Parse(string? raw) public static string[] Parse(string? raw)
{ {
if (string.IsNullOrWhiteSpace(raw)) return Array.Empty<string>(); if (string.IsNullOrWhiteSpace(raw)) return Array.Empty<string>();
@@ -18,6 +18,7 @@ public static class ServiceCollectionExtensions
/// </summary> /// </summary>
/// <param name="services">The service collection to configure.</param> /// <param name="services">The service collection to configure.</param>
/// <param name="configuration">The application configuration containing cluster options.</param> /// <param name="configuration">The application configuration containing cluster options.</param>
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
public static IServiceCollection AddOtOpcUaCluster(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddOtOpcUaCluster(this IServiceCollection services, IConfiguration configuration)
{ {
services.AddOptions<AkkaClusterOptions>() services.AddOptions<AkkaClusterOptions>()
@@ -45,6 +46,7 @@ public static class ServiceCollectionExtensions
/// </summary> /// </summary>
/// <param name="builder">The Akka configuration builder to configure.</param> /// <param name="builder">The Akka configuration builder to configure.</param>
/// <param name="serviceProvider">The service provider for resolving cluster options.</param> /// <param name="serviceProvider">The service provider for resolving cluster options.</param>
/// <returns>The same <see cref="AkkaConfigurationBuilder"/> for chaining.</returns>
public static AkkaConfigurationBuilder WithOtOpcUaClusterBootstrap( public static AkkaConfigurationBuilder WithOtOpcUaClusterBootstrap(
this AkkaConfigurationBuilder builder, this AkkaConfigurationBuilder builder,
IServiceProvider serviceProvider) IServiceProvider serviceProvider)
@@ -16,14 +16,22 @@ public interface IBrowseSession : IAsyncDisposable
DateTime LastUsedUtc { get; } DateTime LastUsedUtc { get; }
/// <summary>Returns the top-level browse nodes.</summary> /// <summary>Returns the top-level browse nodes.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that resolves to the list of top-level browse nodes.</returns>
Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken); Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken);
/// <summary>Returns the direct children of the node identified by /// <summary>Returns the direct children of the node identified by
/// <paramref name="nodeId"/>.</summary> /// <paramref name="nodeId"/>.</summary>
/// <param name="nodeId">The identifier of the node whose children to expand.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that resolves to the list of direct child nodes.</returns>
Task<IReadOnlyList<BrowseNode>> ExpandAsync(string nodeId, CancellationToken cancellationToken); Task<IReadOnlyList<BrowseNode>> ExpandAsync(string nodeId, CancellationToken cancellationToken);
/// <summary>Returns the attributes of the node identified by <paramref name="nodeId"/>. /// <summary>Returns the attributes of the node identified by <paramref name="nodeId"/>.
/// Empty for drivers whose tree is uniform (OPC UA Client). Galaxy uses this to populate /// 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.</summary> /// the attribute side-panel after the user selects an object.</summary>
/// <param name="nodeId">The identifier of the node whose attributes to retrieve.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that resolves to the list of attribute descriptors for the node.</returns>
Task<IReadOnlyList<AttributeInfo>> AttributesAsync(string nodeId, CancellationToken cancellationToken); Task<IReadOnlyList<AttributeInfo>> AttributesAsync(string nodeId, CancellationToken cancellationToken);
} }
@@ -15,5 +15,6 @@ public interface IDriverBrowser
/// <param name="configJson">Driver options serialized as JSON; same shape the runtime /// <param name="configJson">Driver options serialized as JSON; same shape the runtime
/// driver would consume.</param> /// driver would consume.</param>
/// <param name="cancellationToken">Cancellation for the connect phase only.</param> /// <param name="cancellationToken">Cancellation for the connect phase only.</param>
/// <returns>A task containing the opened browse session.</returns>
Task<IBrowseSession> OpenAsync(string configJson, CancellationToken cancellationToken); Task<IBrowseSession> OpenAsync(string configJson, CancellationToken cancellationToken);
} }
@@ -18,6 +18,7 @@ public interface IAlarmActorStateStore
/// <summary>Saves the alarm actor state snapshot.</summary> /// <summary>Saves the alarm actor state snapshot.</summary>
/// <param name="snapshot">The state snapshot to persist.</param> /// <param name="snapshot">The state snapshot to persist.</param>
/// <param name="ct">Cancellation token.</param> /// <param name="ct">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct); Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct);
} }
@@ -41,14 +42,10 @@ public sealed class NullAlarmActorStateStore : IAlarmActorStateStore
{ {
public static readonly NullAlarmActorStateStore Instance = new(); public static readonly NullAlarmActorStateStore Instance = new();
private NullAlarmActorStateStore() { } private NullAlarmActorStateStore() { }
/// <summary>Always returns null, indicating no persisted state.</summary> /// <inheritdoc />
/// <param name="alarmId">The alarm identifier (unused).</param>
/// <param name="ct">Cancellation token (unused).</param>
public Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct) => public Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct) =>
Task.FromResult<AlarmActorStateSnapshot?>(null); Task.FromResult<AlarmActorStateSnapshot?>(null);
/// <summary>Completes immediately without persisting anything.</summary> /// <inheritdoc />
/// <param name="snapshot">The state snapshot (ignored).</param>
/// <param name="ct">Cancellation token (unused).</param>
public Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct) => public Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct) =>
Task.CompletedTask; Task.CompletedTask;
} }
@@ -43,11 +43,7 @@ public sealed class NullVirtualTagEvaluator : IVirtualTagEvaluator
{ {
public static readonly NullVirtualTagEvaluator Instance = new(); public static readonly NullVirtualTagEvaluator Instance = new();
private NullVirtualTagEvaluator() { } private NullVirtualTagEvaluator() { }
/// <summary>Returns <see cref="VirtualTagEvalResult.NoChange"/> for every evaluation.</summary> /// <inheritdoc />
/// <param name="virtualTagId">The virtual tag identifier (ignored).</param>
/// <param name="expression">The expression string (ignored).</param>
/// <param name="dependencies">The variable dependencies (ignored).</param>
/// <returns>Always returns <see cref="VirtualTagEvalResult.NoChange"/>.</returns>
public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary<string, object?> dependencies) public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary<string, object?> dependencies)
=> VirtualTagEvalResult.NoChange; => VirtualTagEvalResult.NoChange;
} }
@@ -23,5 +23,6 @@ public interface IAdminOperationsClient
/// <typeparam name="T">Expected reply type.</typeparam> /// <typeparam name="T">Expected reply type.</typeparam>
/// <param name="message">The message to send.</param> /// <param name="message">The message to send.</param>
/// <param name="ct">Cancellation token (caller-controlled timeout).</param> /// <param name="ct">Cancellation token (caller-controlled timeout).</param>
/// <returns>A task that resolves to the reply of type <typeparamref name="T"/>.</returns>
Task<T> AskAsync<T>(object message, CancellationToken ct); Task<T> AskAsync<T>(object message, CancellationToken ct);
} }
@@ -11,5 +11,6 @@ public interface IFleetDiagnosticsClient
/// <summary>Gets diagnostics for the specified node.</summary> /// <summary>Gets diagnostics for the specified node.</summary>
/// <param name="nodeId">The node ID to retrieve diagnostics for.</param> /// <param name="nodeId">The node ID to retrieve diagnostics for.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that resolves to the diagnostics snapshot for the specified node.</returns>
Task<NodeDiagnosticsSnapshot> GetDiagnosticsAsync(NodeId nodeId, CancellationToken ct); Task<NodeDiagnosticsSnapshot> GetDiagnosticsAsync(NodeId nodeId, CancellationToken ct);
} }
@@ -69,6 +69,7 @@ public static class OtOpcUaTelemetry
/// null when no listener is attached so the call site stays cheap on undecorated builds. /// null when no listener is attached so the call site stays cheap on undecorated builds.
/// </summary> /// </summary>
/// <param name="deploymentId">The deployment identifier to tag the span with.</param> /// <param name="deploymentId">The deployment identifier to tag the span with.</param>
/// <returns>The started <see cref="Activity"/>, or null when no listener is attached.</returns>
public static Activity? StartDeployApplySpan(string deploymentId) public static Activity? StartDeployApplySpan(string deploymentId)
{ {
var activity = ActivitySource.StartActivity("otopcua.deploy.apply", ActivityKind.Internal); var activity = ActivitySource.StartActivity("otopcua.deploy.apply", ActivityKind.Internal);
@@ -77,6 +78,7 @@ public static class OtOpcUaTelemetry
} }
/// <summary>Span wrapping a full OPC UA address-space rebuild (Phase7 plan → apply).</summary> /// <summary>Span wrapping a full OPC UA address-space rebuild (Phase7 plan → apply).</summary>
/// <returns>The started <see cref="Activity"/>, or null when no listener is attached.</returns>
public static Activity? StartAddressSpaceRebuildSpan() public static Activity? StartAddressSpaceRebuildSpan()
=> ActivitySource.StartActivity("otopcua.opcua.address_space_rebuild", ActivityKind.Internal); => ActivitySource.StartActivity("otopcua.opcua.address_space_rebuild", ActivityKind.Internal);
} }
@@ -22,37 +22,22 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink
public void SetSink(IOpcUaAddressSpaceSink? sink) => public void SetSink(IOpcUaAddressSpaceSink? sink) =>
_inner = sink ?? NullOpcUaAddressSpaceSink.Instance; _inner = sink ?? NullOpcUaAddressSpaceSink.Instance;
/// <summary>Writes a value to the OPC UA address space through the inner sink.</summary> /// <inheritdoc />
/// <param name="nodeId">The node ID of the variable.</param>
/// <param name="value">The value to write.</param>
/// <param name="quality">The OPC UA quality value.</param>
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc) public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
=> _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc); => _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc);
/// <summary>Writes an alarm state through the inner sink.</summary> /// <inheritdoc />
/// <param name="alarmNodeId">The node ID of the alarm condition.</param>
/// <param name="active">Whether the alarm is active.</param>
/// <param name="acknowledged">Whether the alarm has been acknowledged.</param>
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc) public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc)
=> _inner.WriteAlarmState(alarmNodeId, active, acknowledged, sourceTimestampUtc); => _inner.WriteAlarmState(alarmNodeId, active, acknowledged, sourceTimestampUtc);
/// <summary>Ensures a folder exists in the address space through the inner sink.</summary> /// <inheritdoc />
/// <param name="folderNodeId">The node ID of the folder.</param>
/// <param name="parentNodeId">The node ID of the parent folder, or null for root.</param>
/// <param name="displayName">The display name of the folder.</param>
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName) public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
=> _inner.EnsureFolder(folderNodeId, parentNodeId, displayName); => _inner.EnsureFolder(folderNodeId, parentNodeId, displayName);
/// <summary>Ensures a variable exists in the address space through the inner sink.</summary> /// <inheritdoc />
/// <param name="variableNodeId">The node ID of the variable.</param>
/// <param name="parentFolderNodeId">The node ID of the parent folder, or null for root.</param>
/// <param name="displayName">The display name of the variable.</param>
/// <param name="dataType">The OPC UA data type of the variable.</param>
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType) public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType)
=> _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType); => _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType);
/// <summary>Rebuilds the address space through the inner sink.</summary> /// <inheritdoc />
public void RebuildAddressSpace() => _inner.RebuildAddressSpace(); public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
} }
@@ -16,7 +16,6 @@ public sealed class DeferredServiceLevelPublisher : IServiceLevelPublisher
public void SetInner(IServiceLevelPublisher? inner) => public void SetInner(IServiceLevelPublisher? inner) =>
_inner = inner ?? NullServiceLevelPublisher.Instance; _inner = inner ?? NullServiceLevelPublisher.Instance;
/// <summary>Publishes a service level value to the inner publisher.</summary> /// <inheritdoc />
/// <param name="serviceLevel">The service level to publish.</param>
public void Publish(byte serviceLevel) => _inner.Publish(serviceLevel); public void Publish(byte serviceLevel) => _inner.Publish(serviceLevel);
} }
@@ -3,15 +3,18 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
public readonly record struct CorrelationId(Guid Value) public readonly record struct CorrelationId(Guid Value)
{ {
/// <summary>Creates a new CorrelationId with a randomly generated GUID.</summary> /// <summary>Creates a new CorrelationId with a randomly generated GUID.</summary>
/// <returns>A new <see cref="CorrelationId"/> backed by a random GUID.</returns>
public static CorrelationId NewId() => new(Guid.NewGuid()); public static CorrelationId NewId() => new(Guid.NewGuid());
/// <inheritdoc /> /// <inheritdoc />
public override string ToString() => Value.ToString("N"); public override string ToString() => Value.ToString("N");
/// <summary>Parses a lowercase hex string without hyphens into a CorrelationId.</summary> /// <summary>Parses a lowercase hex string without hyphens into a CorrelationId.</summary>
/// <param name="s">The string to parse.</param> /// <param name="s">The string to parse.</param>
/// <returns>A <see cref="CorrelationId"/> parsed from the supplied string.</returns>
public static CorrelationId Parse(string s) => new(Guid.ParseExact(s, "N")); public static CorrelationId Parse(string s) => new(Guid.ParseExact(s, "N"));
/// <summary>Attempts to parse a lowercase hex string without hyphens into a CorrelationId.</summary> /// <summary>Attempts to parse a lowercase hex string without hyphens into a CorrelationId.</summary>
/// <param name="s">The string to parse, or null.</param> /// <param name="s">The string to parse, or null.</param>
/// <param name="id">The resulting CorrelationId if parsing succeeds.</param> /// <param name="id">The resulting CorrelationId if parsing succeeds.</param>
/// <returns><see langword="true"/> if parsing succeeded; otherwise <see langword="false"/>.</returns>
public static bool TryParse(string? s, out CorrelationId id) public static bool TryParse(string? s, out CorrelationId id)
{ {
if (Guid.TryParseExact(s, "N", out var g)) { id = new CorrelationId(g); return true; } if (Guid.TryParseExact(s, "N", out var g)) { id = new CorrelationId(g); return true; }
@@ -21,10 +21,12 @@ public interface ILocalConfigCache
/// <summary>Stores a generation snapshot in the local cache.</summary> /// <summary>Stores a generation snapshot in the local cache.</summary>
/// <param name="snapshot">The generation snapshot to store.</param> /// <param name="snapshot">The generation snapshot to store.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default); Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default);
/// <summary>Removes old generations, keeping only the most recent N.</summary> /// <summary>Removes old generations, keeping only the most recent N.</summary>
/// <param name="clusterId">The cluster identifier.</param> /// <param name="clusterId">The cluster identifier.</param>
/// <param name="keepLatest">The number of latest generations to keep.</param> /// <param name="keepLatest">The number of latest generations to keep.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default); Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default);
} }
@@ -45,9 +45,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
} }
} }
/// <summary>Gets the most recent snapshot for the specified cluster.</summary> /// <inheritdoc />
/// <param name="clusterId">The cluster ID.</param>
/// <param name="ct">Cancellation token.</param>
public Task<GenerationSnapshot?> GetMostRecentAsync(string clusterId, CancellationToken ct = default) public Task<GenerationSnapshot?> GetMostRecentAsync(string clusterId, CancellationToken ct = default)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
@@ -58,9 +56,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
return Task.FromResult<GenerationSnapshot?>(snapshot); return Task.FromResult<GenerationSnapshot?>(snapshot);
} }
/// <summary>Stores a snapshot in the cache.</summary> /// <inheritdoc />
/// <param name="snapshot">The snapshot to store.</param>
/// <param name="ct">Cancellation token.</param>
public async Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default) public async Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
@@ -89,10 +85,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
} }
} }
/// <summary>Removes old generation snapshots, keeping only the latest ones.</summary> /// <inheritdoc />
/// <param name="clusterId">The cluster ID.</param>
/// <param name="keepLatest">Number of latest generations to keep.</param>
/// <param name="ct">Cancellation token.</param>
public Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default) public Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default)
{ {
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
@@ -24,11 +24,13 @@ public interface ILdapGroupRoleMappingService
/// </remarks> /// </remarks>
/// <param name="ldapGroups">The LDAP groups to search for.</param> /// <param name="ldapGroups">The LDAP groups to search for.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task resolving to the list of mappings whose LDAP group matches any of the provided groups.</returns>
Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync( Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
IEnumerable<string> ldapGroups, CancellationToken cancellationToken); IEnumerable<string> ldapGroups, CancellationToken cancellationToken);
/// <summary>Enumerate every mapping; Admin UI listing only.</summary> /// <summary>Enumerate every mapping; Admin UI listing only.</summary>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task resolving to all LDAP group role mappings.</returns>
Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken); Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken);
/// <summary>Create a new grant.</summary> /// <summary>Create a new grant.</summary>
@@ -39,11 +41,13 @@ public interface ILdapGroupRoleMappingService
/// </exception> /// </exception>
/// <param name="row">The LDAP group role mapping to create.</param> /// <param name="row">The LDAP group role mapping to create.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task resolving to the newly created <see cref="LdapGroupRoleMapping"/> with any DB-assigned values populated.</returns>
Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken); Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken);
/// <summary>Delete a mapping by its surrogate key.</summary> /// <summary>Delete a mapping by its surrogate key.</summary>
/// <param name="id">The unique identifier of the mapping to delete.</param> /// <param name="id">The unique identifier of the mapping to delete.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous delete operation.</returns>
Task DeleteAsync(Guid id, CancellationToken cancellationToken); Task DeleteAsync(Guid id, CancellationToken cancellationToken);
} }
@@ -10,10 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Services;
/// </summary> /// </summary>
public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILdapGroupRoleMappingService public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILdapGroupRoleMappingService
{ {
/// <summary>Gets LDAP group role mappings for the specified groups.</summary> /// <inheritdoc />
/// <param name="ldapGroups">The LDAP group names to query.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The matching role mappings.</returns>
public async Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync( public async Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
IEnumerable<string> ldapGroups, CancellationToken cancellationToken) IEnumerable<string> ldapGroups, CancellationToken cancellationToken)
{ {
@@ -21,6 +21,7 @@ public static class DraftValidator
/// Validates a draft snapshot and returns all validation errors found in a single pass. /// Validates a draft snapshot and returns all validation errors found in a single pass.
/// </summary> /// </summary>
/// <param name="draft">The draft snapshot to validate.</param> /// <param name="draft">The draft snapshot to validate.</param>
/// <returns>A read-only list of all validation errors found; empty if the draft is valid.</returns>
public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft) public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft)
{ {
var errors = new List<ValidationError>(); var errors = new List<ValidationError>();
@@ -147,6 +148,7 @@ public static class DraftValidator
/// <summary>Decision #125: EquipmentId = 'EQ-' + lowercase first 12 hex chars of the UUID.</summary> /// <summary>Decision #125: EquipmentId = 'EQ-' + lowercase first 12 hex chars of the UUID.</summary>
/// <param name="uuid">The equipment UUID to derive the ID from.</param> /// <param name="uuid">The equipment UUID to derive the ID from.</param>
/// <returns>The derived equipment ID string in the form <c>EQ-xxxxxxxxxxxx</c>.</returns>
public static string DeriveEquipmentId(Guid uuid) => public static string DeriveEquipmentId(Guid uuid) =>
"EQ-" + uuid.ToString("N")[..12].ToLowerInvariant(); "EQ-" + uuid.ToString("N")[..12].ToLowerInvariant();
@@ -203,6 +205,7 @@ public static class DraftValidator
/// </remarks> /// </remarks>
/// <param name="cluster">The server cluster to validate.</param> /// <param name="cluster">The server cluster to validate.</param>
/// <param name="clusterNodes">The cluster nodes to validate against the cluster configuration.</param> /// <param name="clusterNodes">The cluster nodes to validate against the cluster configuration.</param>
/// <returns>A read-only list of all validation errors found; empty if the topology is valid.</returns>
public static IReadOnlyList<ValidationError> ValidateClusterTopology( public static IReadOnlyList<ValidationError> ValidateClusterTopology(
ServerCluster cluster, ServerCluster cluster,
IReadOnlyList<ClusterNode> clusterNodes) IReadOnlyList<ClusterNode> clusterNodes)
@@ -55,6 +55,7 @@ public sealed class DriverTypeRegistry
/// <summary>Look up a driver type by name. Throws if unknown.</summary> /// <summary>Look up a driver type by name. Throws if unknown.</summary>
/// <param name="driverType">The driver type name to look up.</param> /// <param name="driverType">The driver type name to look up.</param>
/// <returns>The <see cref="DriverTypeMetadata"/> registered for the specified type name.</returns>
public DriverTypeMetadata Get(string driverType) public DriverTypeMetadata Get(string driverType)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(driverType); ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
@@ -69,6 +70,7 @@ public sealed class DriverTypeRegistry
/// <summary>Try to look up a driver type by name. Returns null if unknown (no exception).</summary> /// <summary>Try to look up a driver type by name. Returns null if unknown (no exception).</summary>
/// <param name="driverType">The driver type name to look up.</param> /// <param name="driverType">The driver type name to look up.</param>
/// <returns>The matching <see cref="DriverTypeMetadata"/>, or <c>null</c> if not registered.</returns>
public DriverTypeMetadata? TryGet(string driverType) public DriverTypeMetadata? TryGet(string driverType)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(driverType); ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
@@ -76,6 +78,7 @@ public sealed class DriverTypeRegistry
} }
/// <summary>Snapshot of all registered driver types.</summary> /// <summary>Snapshot of all registered driver types.</summary>
/// <returns>A read-only collection of all currently registered driver type metadata entries.</returns>
public IReadOnlyCollection<DriverTypeMetadata> All() => _types.Values.ToList(); public IReadOnlyCollection<DriverTypeMetadata> All() => _types.Values.ToList();
} }
@@ -28,6 +28,7 @@ public interface IHistorianDataSource : IDisposable
/// <param name="endUtc">The end of the time range in UTC.</param> /// <param name="endUtc">The end of the time range in UTC.</param>
/// <param name="maxValuesPerNode">The maximum number of values to return per node.</param> /// <param name="maxValuesPerNode">The maximum number of values to return per node.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task resolving to a <see cref="HistoryReadResult"/> containing the raw samples.</returns>
Task<HistoryReadResult> ReadRawAsync( Task<HistoryReadResult> ReadRawAsync(
string fullReference, string fullReference,
DateTime startUtc, DateTime startUtc,
@@ -46,6 +47,7 @@ public interface IHistorianDataSource : IDisposable
/// <param name="interval">The interval for bucketing samples.</param> /// <param name="interval">The interval for bucketing samples.</param>
/// <param name="aggregate">The aggregation function to apply to each bucket.</param> /// <param name="aggregate">The aggregation function to apply to each bucket.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task resolving to a <see cref="HistoryReadResult"/> containing the processed interval samples.</returns>
Task<HistoryReadResult> ReadProcessedAsync( Task<HistoryReadResult> ReadProcessedAsync(
string fullReference, string fullReference,
DateTime startUtc, DateTime startUtc,
@@ -63,6 +65,7 @@ public interface IHistorianDataSource : IDisposable
/// <param name="fullReference">The full reference of the tag to read.</param> /// <param name="fullReference">The full reference of the tag to read.</param>
/// <param name="timestampsUtc">The list of timestamps to read values at.</param> /// <param name="timestampsUtc">The list of timestamps to read values at.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task resolving to a <see cref="HistoryReadResult"/> with one sample per requested timestamp.</returns>
Task<HistoryReadResult> ReadAtTimeAsync( Task<HistoryReadResult> ReadAtTimeAsync(
string fullReference, string fullReference,
IReadOnlyList<DateTime> timestampsUtc, IReadOnlyList<DateTime> timestampsUtc,
@@ -93,6 +96,7 @@ public interface IHistorianDataSource : IDisposable
/// <param name="endUtc">The end of the time range in UTC.</param> /// <param name="endUtc">The end of the time range in UTC.</param>
/// <param name="maxEvents">The maximum number of events to return, or a non-positive value to use the default backend cap.</param> /// <param name="maxEvents">The maximum number of events to return, or a non-positive value to use the default backend cap.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param> /// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
/// <returns>A task resolving to a <see cref="HistoricalEventsResult"/> containing historical alarm and event records.</returns>
Task<HistoricalEventsResult> ReadEventsAsync( Task<HistoricalEventsResult> ReadEventsAsync(
string? sourceName, string? sourceName,
DateTime startUtc, DateTime startUtc,
@@ -104,5 +108,6 @@ public interface IHistorianDataSource : IDisposable
/// Point-in-time health snapshot for diagnostics and dashboards. Pure /// Point-in-time health snapshot for diagnostics and dashboards. Pure
/// observation; never blocks on backend I/O. /// observation; never blocks on backend I/O.
/// </summary> /// </summary>
/// <returns>The current <see cref="HistorianHealthSnapshot"/> for this data source.</returns>
HistorianHealthSnapshot GetHealthSnapshot(); HistorianHealthSnapshot GetHealthSnapshot();
} }
@@ -18,6 +18,7 @@ public interface IAddressSpaceBuilder
/// </summary> /// </summary>
/// <param name="browseName">OPC UA browse name (the segment of the path under the parent).</param> /// <param name="browseName">OPC UA browse name (the segment of the path under the parent).</param>
/// <param name="displayName">Human-readable display name. May equal <paramref name="browseName"/>.</param> /// <param name="displayName">Human-readable display name. May equal <paramref name="browseName"/>.</param>
/// <returns>A child builder scoped to inside this folder.</returns>
IAddressSpaceBuilder Folder(string browseName, string displayName); IAddressSpaceBuilder Folder(string browseName, string displayName);
/// <summary> /// <summary>
@@ -27,6 +28,7 @@ public interface IAddressSpaceBuilder
/// <param name="browseName">OPC UA browse name (the segment of the path under the parent folder).</param> /// <param name="browseName">OPC UA browse name (the segment of the path under the parent folder).</param>
/// <param name="displayName">Human-readable display name. May equal <paramref name="browseName"/>.</param> /// <param name="displayName">Human-readable display name. May equal <paramref name="browseName"/>.</param>
/// <param name="attributeInfo">Driver-side metadata for the variable.</param> /// <param name="attributeInfo">Driver-side metadata for the variable.</param>
/// <returns>An opaque handle for the registered variable.</returns>
IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo); IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo);
/// <summary> /// <summary>
@@ -56,6 +58,7 @@ public interface IVariableHandle
/// <c>Acknowledge</c>, <c>Deactivate</c>). /// <c>Acknowledge</c>, <c>Deactivate</c>).
/// </summary> /// </summary>
/// <param name="info">The alarm condition information.</param> /// <param name="info">The alarm condition information.</param>
/// <returns>A sink that receives alarm lifecycle transitions for this condition.</returns>
IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info); IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info);
} }
@@ -13,6 +13,7 @@ public interface IAlarmSource
/// </summary> /// </summary>
/// <param name="sourceNodeIds">The driver node IDs to subscribe to.</param> /// <param name="sourceNodeIds">The driver node IDs to subscribe to.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that resolves to an opaque <see cref="IAlarmSubscriptionHandle"/> for the new subscription.</returns>
Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync( Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
IReadOnlyList<string> sourceNodeIds, IReadOnlyList<string> sourceNodeIds,
CancellationToken cancellationToken); CancellationToken cancellationToken);
@@ -20,11 +21,13 @@ public interface IAlarmSource
/// <summary>Cancel an alarm subscription returned by <see cref="SubscribeAlarmsAsync"/>.</summary> /// <summary>Cancel an alarm subscription returned by <see cref="SubscribeAlarmsAsync"/>.</summary>
/// <param name="handle">The subscription handle returned from <see cref="SubscribeAlarmsAsync"/>.</param> /// <param name="handle">The subscription handle returned from <see cref="SubscribeAlarmsAsync"/>.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken); Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken);
/// <summary>Acknowledge one or more active alarms by source node ID + condition ID.</summary> /// <summary>Acknowledge one or more active alarms by source node ID + condition ID.</summary>
/// <param name="acknowledgements">The batch of alarm acknowledgement requests.</param> /// <param name="acknowledgements">The batch of alarm acknowledgement requests.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task AcknowledgeAsync( Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
CancellationToken cancellationToken); CancellationToken cancellationToken);
@@ -23,6 +23,7 @@ public interface IDriver
/// <summary>Initialize the driver from its <c>DriverConfig</c> JSON; open connections; prepare for first use.</summary> /// <summary>Initialize the driver from its <c>DriverConfig</c> JSON; open connections; prepare for first use.</summary>
/// <param name="driverConfigJson">The driver configuration as JSON.</param> /// <param name="driverConfigJson">The driver configuration as JSON.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken); Task InitializeAsync(string driverConfigJson, CancellationToken cancellationToken);
/// <summary> /// <summary>
@@ -37,13 +38,16 @@ public interface IDriver
/// </remarks> /// </remarks>
/// <param name="driverConfigJson">The driver configuration as JSON.</param> /// <param name="driverConfigJson">The driver configuration as JSON.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken); Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken);
/// <summary>Stop the driver, close connections, release resources. Called on shutdown or driver removal.</summary> /// <summary>Stop the driver, close connections, release resources. Called on shutdown or driver removal.</summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ShutdownAsync(CancellationToken cancellationToken); Task ShutdownAsync(CancellationToken cancellationToken);
/// <summary>Current health snapshot, polled by Core for the status dashboard and ServiceLevel.</summary> /// <summary>Current health snapshot, polled by Core for the status dashboard and ServiceLevel.</summary>
/// <returns>The current driver health snapshot.</returns>
DriverHealth GetHealth(); DriverHealth GetHealth();
/// <summary> /// <summary>
@@ -56,6 +60,7 @@ public interface IDriver
/// allocation tracking". Tier C drivers (process-isolated) report through the same /// allocation tracking". Tier C drivers (process-isolated) report through the same
/// interface but the cache-flush is internal to their host. /// interface but the cache-flush is internal to their host.
/// </remarks> /// </remarks>
/// <returns>The approximate driver-attributable memory footprint in bytes.</returns>
long GetMemoryFootprint(); long GetMemoryFootprint();
/// <summary> /// <summary>
@@ -63,5 +68,6 @@ public interface IDriver
/// Required-for-correctness state must NOT be flushed. /// Required-for-correctness state must NOT be flushed.
/// </summary> /// </summary>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task FlushOptionalCachesAsync(CancellationToken cancellationToken); Task FlushOptionalCachesAsync(CancellationToken cancellationToken);
} }
@@ -34,12 +34,8 @@ public sealed class NullDriverFactory : IDriverFactory
public static readonly NullDriverFactory Instance = new(); public static readonly NullDriverFactory Instance = new();
private NullDriverFactory() { } private NullDriverFactory() { }
/// <summary>Creates a driver (always returns null in this null implementation).</summary> /// <inheritdoc />
/// <param name="driverType">The driver type name.</param>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="driverConfigJson">The driver configuration as a JSON string.</param>
/// <returns>Always returns null.</returns>
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson) => null; public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson) => null;
/// <summary>Gets the collection of supported driver types (empty in this null implementation).</summary> /// <inheritdoc />
public IReadOnlyCollection<string> SupportedTypes { get; } = Array.Empty<string>(); public IReadOnlyCollection<string> SupportedTypes { get; } = Array.Empty<string>();
} }
@@ -11,6 +11,10 @@ public interface IDriverHealthPublisher
/// Publishes a health snapshot for one driver instance. Implementations must be /// Publishes a health snapshot for one driver instance. Implementations must be
/// non-blocking and tolerant of being called from any thread. /// non-blocking and tolerant of being called from any thread.
/// </summary> /// </summary>
/// <param name="clusterId">The cluster identifier the driver instance belongs to.</param>
/// <param name="driverInstanceId">The unique identifier of the driver instance.</param>
/// <param name="health">The current health state of the driver instance.</param>
/// <param name="errorCount5Min">Number of errors recorded in the past 5 minutes.</param>
void Publish( void Publish(
string clusterId, string clusterId,
string driverInstanceId, string driverInstanceId,
@@ -17,6 +17,10 @@ public interface IDriverProbe
/// timeout cancellation. Never throw on connection failure; instead return a result /// timeout cancellation. Never throw on connection failure; instead return a result
/// with <c>Ok = false</c> + a message. /// with <c>Ok = false</c> + a message.
/// </summary> /// </summary>
/// <param name="configJson">Driver configuration JSON; same shape the runtime driver consumes.</param>
/// <param name="timeout">Maximum duration for the probe attempt.</param>
/// <param name="ct">Cancellation token for the probe operation.</param>
/// <returns>A task containing the probe result with success status and optional latency.</returns>
Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct); Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct);
} }
@@ -22,5 +22,6 @@ public interface IDriverSupervisor
/// </summary> /// </summary>
/// <param name="reason">Human-readable reason — flows into the supervisor's logs.</param> /// <param name="reason">Human-readable reason — flows into the supervisor's logs.</param>
/// <param name="cancellationToken">Cancels the recycle request; an in-flight restart is not interrupted.</param> /// <param name="cancellationToken">Cancels the recycle request; an in-flight restart is not interrupted.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task RecycleAsync(string reason, CancellationToken cancellationToken); Task RecycleAsync(string reason, CancellationToken cancellationToken);
} }
@@ -94,6 +94,7 @@ public interface IHistoryProvider
/// <c>HistorianDataSource</c>). The asymmetry is intentional — Core.Abstractions-006. /// <c>HistorianDataSource</c>). The asymmetry is intentional — Core.Abstractions-006.
/// </param> /// </param>
/// <param name="cancellationToken">Request cancellation.</param> /// <param name="cancellationToken">Request cancellation.</param>
/// <returns>A task that resolves to the historical events result for the requested window.</returns>
/// <remarks> /// <remarks>
/// Default implementation throws. Only drivers with an event historian (Galaxy via the /// Default implementation throws. Only drivers with an event historian (Galaxy via the
/// Wonderware Alarm &amp; Events log) override. Modbus / the OPC UA Client driver stay /// Wonderware Alarm &amp; Events log) override. Modbus / the OPC UA Client driver stay
@@ -16,6 +16,7 @@ public interface IHostConnectivityProbe
/// Snapshot of host-level connectivity. The Core uses this to drive Bad-quality /// 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). /// fan-out scoped to the affected host's subtree (not the whole driver namespace).
/// </summary> /// </summary>
/// <returns>A snapshot list of per-host connectivity statuses.</returns>
IReadOnlyList<HostConnectivityStatus> GetHostStatuses(); IReadOnlyList<HostConnectivityStatus> GetHostStatuses();
/// <summary>Fired when a host transitions Running ↔ Stopped (or similar lifecycle change).</summary> /// <summary>Fired when a host transitions Running ↔ Stopped (or similar lifecycle change).</summary>
@@ -13,5 +13,6 @@ public interface ITagDiscovery
/// </summary> /// </summary>
/// <param name="builder">The address space builder to stream discovered nodes into.</param> /// <param name="builder">The address space builder to stream discovered nodes into.</param>
/// <param name="cancellationToken">A cancellation token for the discovery operation.</param> /// <param name="cancellationToken">A cancellation token for the discovery operation.</param>
/// <returns>A task that represents the asynchronous discovery operation.</returns>
Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken); Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken);
} }
@@ -19,6 +19,7 @@ public interface IWritable
/// </summary> /// </summary>
/// <param name="writes">Pairs of full reference + value to write.</param> /// <param name="writes">Pairs of full reference + value to write.</param>
/// <param name="cancellationToken">Cancellation token; the driver should abort the batch if cancelled.</param> /// <param name="cancellationToken">Cancellation token; the driver should abort the batch if cancelled.</param>
/// <returns>A task that resolves to one <see cref="WriteResult"/> per requested write, in the same order.</returns>
Task<IReadOnlyList<WriteResult>> WriteAsync( Task<IReadOnlyList<WriteResult>> WriteAsync(
IReadOnlyList<WriteRequest> writes, IReadOnlyList<WriteRequest> writes,
CancellationToken cancellationToken); CancellationToken cancellationToken);
@@ -41,6 +41,7 @@ public sealed class PollGroupEngine : IAsyncDisposable
/// <summary>Default floor for publishing intervals — matches the Modbus 100 ms cap.</summary> /// <summary>Default floor for publishing intervals — matches the Modbus 100 ms cap.</summary>
public static readonly TimeSpan DefaultMinInterval = TimeSpan.FromMilliseconds(100); public static readonly TimeSpan DefaultMinInterval = TimeSpan.FromMilliseconds(100);
/// <summary>Initializes a new poll-group engine with the supplied reader, change callback, interval floor, and optional error sink.</summary>
/// <param name="reader">Driver-supplied batch reader; snapshots MUST be returned in the same /// <param name="reader">Driver-supplied batch reader; snapshots MUST be returned in the same
/// order as the input references.</param> /// order as the input references.</param>
/// <param name="onChange">Callback invoked per changed tag — the driver forwards to its own /// <param name="onChange">Callback invoked per changed tag — the driver forwards to its own
@@ -68,6 +69,7 @@ public sealed class PollGroupEngine : IAsyncDisposable
/// <summary>Register a new polled subscription and start its background loop.</summary> /// <summary>Register a new polled subscription and start its background loop.</summary>
/// <param name="fullReferences">The list of tag references to poll.</param> /// <param name="fullReferences">The list of tag references to poll.</param>
/// <param name="publishingInterval">The desired polling interval; will be clamped to the configured minimum.</param> /// <param name="publishingInterval">The desired polling interval; will be clamped to the configured minimum.</param>
/// <returns>A subscription handle that can be passed to <see cref="Unsubscribe"/> to cancel the loop.</returns>
public ISubscriptionHandle Subscribe(IReadOnlyList<string> fullReferences, TimeSpan publishingInterval) public ISubscriptionHandle Subscribe(IReadOnlyList<string> fullReferences, TimeSpan publishingInterval)
{ {
ArgumentNullException.ThrowIfNull(fullReferences); ArgumentNullException.ThrowIfNull(fullReferences);
@@ -207,6 +209,7 @@ public sealed class PollGroupEngine : IAsyncDisposable
} }
/// <summary>Cancel every active subscription and await all loop tasks. Idempotent.</summary> /// <summary>Cancel every active subscription and await all loop tasks. Idempotent.</summary>
/// <returns>A value task that represents the asynchronous dispose operation.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
// Cancel all loops first so they can all start winding down in parallel. // 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 private sealed record PollSubscriptionHandle(long Id) : ISubscriptionHandle
{ {
/// <summary>Gets a diagnostic identifier for this subscription.</summary> /// <inheritdoc />
public string DiagnosticId => $"poll-sub-{Id}"; public string DiagnosticId => $"poll-sub-{Id}";
} }
} }
@@ -26,9 +26,11 @@ public interface IAlarmHistorianSink
/// <summary>Durably enqueue the event. Returns as soon as the queue row is committed.</summary> /// <summary>Durably enqueue the event. Returns as soon as the queue row is committed.</summary>
/// <param name="evt">The alarm historian event to enqueue.</param> /// <param name="evt">The alarm historian event to enqueue.</param>
/// <param name="cancellationToken">A cancellation token for async operations.</param> /// <param name="cancellationToken">A cancellation token for async operations.</param>
/// <returns>A task that represents the asynchronous enqueue operation.</returns>
Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken); Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken);
/// <summary>Snapshot of current queue depth + drain health.</summary> /// <summary>Snapshot of current queue depth + drain health.</summary>
/// <returns>A snapshot of the current queue depth and drain state.</returns>
HistorianSinkStatus GetStatus(); HistorianSinkStatus GetStatus();
} }
@@ -97,6 +99,7 @@ public interface IAlarmHistorianWriter
/// <summary>Push a batch of events to the historian. Returns one outcome per event, same order.</summary> /// <summary>Push a batch of events to the historian. Returns one outcome per event, same order.</summary>
/// <param name="batch">The batch of alarm historian events to write.</param> /// <param name="batch">The batch of alarm historian events to write.</param>
/// <param name="cancellationToken">A cancellation token for async operations.</param> /// <param name="cancellationToken">A cancellation token for async operations.</param>
/// <returns>A task that resolves to one write outcome per event, in the same order as the batch.</returns>
Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync( Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken); IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken);
} }
@@ -255,6 +255,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
/// </remarks> /// </remarks>
/// <param name="evt">The alarm historian event to enqueue.</param> /// <param name="evt">The alarm historian event to enqueue.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken) public async Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken)
{ {
if (evt is null) throw new ArgumentNullException(nameof(evt)); if (evt is null) throw new ArgumentNullException(nameof(evt));
@@ -345,6 +346,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
/// connections per tick, each paying the open + PRAGMA cost. /// connections per tick, each paying the open + PRAGMA cost.
/// </remarks> /// </remarks>
/// <param name="ct">Cancellation token for the operation.</param> /// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task DrainOnceAsync(CancellationToken ct) public async Task DrainOnceAsync(CancellationToken ct)
{ {
if (_disposed) return; if (_disposed) return;
@@ -490,7 +492,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
} }
} }
/// <summary>Gets the current status of the historian sink including queue depth and drain state.</summary> /// <inheritdoc />
public HistorianSinkStatus GetStatus() public HistorianSinkStatus GetStatus()
{ {
// Core.AlarmHistorian-008: read the non-dead-lettered count from the in-memory // Core.AlarmHistorian-008: read the non-dead-lettered count from the in-memory
@@ -534,6 +536,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
} }
/// <summary>Operator action from Admin UI — retry every dead-lettered row. Non-cascading: they rejoin the regular queue + get a fresh backoff.</summary> /// <summary>Operator action from Admin UI — retry every dead-lettered row. Non-cascading: they rejoin the regular queue + get a fresh backoff.</summary>
/// <returns>The number of rows moved back to the active queue.</returns>
public int RetryDeadLettered() public int RetryDeadLettered()
{ {
using var conn = OpenConnection(); using var conn = OpenConnection();
@@ -97,6 +97,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// copy under the gate. (Core.ScriptedAlarms-013.) /// copy under the gate. (Core.ScriptedAlarms-013.)
/// </remarks> /// </remarks>
/// <param name="alarmId">The alarm identifier to look up.</param> /// <param name="alarmId">The alarm identifier to look up.</param>
/// <returns>The live read-cache dictionary for the alarm, or <see langword="null"/> if not yet allocated.</returns>
internal IReadOnlyDictionary<string, DataValueSnapshot>? TryGetScratchReadCacheForTest(string alarmId) internal IReadOnlyDictionary<string, DataValueSnapshot>? TryGetScratchReadCacheForTest(string alarmId)
=> _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.ReadCache : null; => _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.ReadCache : null;
@@ -113,6 +114,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// (Core.ScriptedAlarms-013.) /// (Core.ScriptedAlarms-013.)
/// </remarks> /// </remarks>
/// <param name="alarmId">The alarm identifier to look up.</param> /// <param name="alarmId">The alarm identifier to look up.</param>
/// <returns>The reusable <see cref="AlarmPredicateContext"/> for the alarm, or <see langword="null"/> if not yet allocated.</returns>
internal AlarmPredicateContext? TryGetScratchContextForTest(string alarmId) internal AlarmPredicateContext? TryGetScratchContextForTest(string alarmId)
=> _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.Context : null; => _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.Context : null;
private readonly ConcurrentDictionary<string, DataValueSnapshot> _valueCache private readonly ConcurrentDictionary<string, DataValueSnapshot> _valueCache
@@ -175,6 +177,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// </summary> /// </summary>
/// <param name="definitions">The alarm definitions to load.</param> /// <param name="definitions">The alarm definitions to load.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task LoadAsync(IReadOnlyList<ScriptedAlarmDefinition> definitions, CancellationToken ct) public async Task LoadAsync(IReadOnlyList<ScriptedAlarmDefinition> definitions, CancellationToken ct)
{ {
if (_disposed) throw new ObjectDisposedException(nameof(ScriptedAlarmEngine)); if (_disposed) throw new ObjectDisposedException(nameof(ScriptedAlarmEngine));
@@ -306,10 +309,12 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// unknown alarm. Mainly used for diagnostics + the Admin UI status page. /// unknown alarm. Mainly used for diagnostics + the Admin UI status page.
/// </summary> /// </summary>
/// <param name="alarmId">The alarm identifier.</param> /// <param name="alarmId">The alarm identifier.</param>
/// <returns>The current <see cref="AlarmConditionState"/> for the alarm, or <see langword="null"/> if the alarm is unknown.</returns>
public AlarmConditionState? GetState(string alarmId) public AlarmConditionState? GetState(string alarmId)
=> _alarms.TryGetValue(alarmId, out var s) ? s.Condition : null; => _alarms.TryGetValue(alarmId, out var s) ? s.Condition : null;
/// <summary>Gets the current persisted state for all loaded alarms.</summary> /// <summary>Gets the current persisted state for all loaded alarms.</summary>
/// <returns>A snapshot collection of all current alarm condition states.</returns>
public IReadOnlyCollection<AlarmConditionState> GetAllStates() public IReadOnlyCollection<AlarmConditionState> GetAllStates()
=> _alarms.Values.Select(a => a.Condition).ToArray(); => _alarms.Values.Select(a => a.Condition).ToArray();
@@ -318,6 +323,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// <param name="user">The user performing the acknowledgment.</param> /// <param name="user">The user performing the acknowledgment.</param>
/// <param name="comment">An optional comment to attach to the acknowledgment.</param> /// <param name="comment">An optional comment to attach to the acknowledgment.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task AcknowledgeAsync(string alarmId, string user, string? comment, CancellationToken ct) public Task AcknowledgeAsync(string alarmId, string user, string? comment, CancellationToken ct)
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAcknowledge(cur, user, comment, _clock())); => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAcknowledge(cur, user, comment, _clock()));
@@ -326,6 +332,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// <param name="user">The user performing the confirmation.</param> /// <param name="user">The user performing the confirmation.</param>
/// <param name="comment">An optional comment to attach to the confirmation.</param> /// <param name="comment">An optional comment to attach to the confirmation.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task ConfirmAsync(string alarmId, string user, string? comment, CancellationToken ct) public Task ConfirmAsync(string alarmId, string user, string? comment, CancellationToken ct)
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyConfirm(cur, user, comment, _clock())); => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyConfirm(cur, user, comment, _clock()));
@@ -333,6 +340,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// <param name="alarmId">The alarm identifier.</param> /// <param name="alarmId">The alarm identifier.</param>
/// <param name="user">The user performing the shelve operation.</param> /// <param name="user">The user performing the shelve operation.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task OneShotShelveAsync(string alarmId, string user, CancellationToken ct) public Task OneShotShelveAsync(string alarmId, string user, CancellationToken ct)
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyOneShotShelve(cur, user, _clock())); => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyOneShotShelve(cur, user, _clock()));
@@ -341,6 +349,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// <param name="user">The user performing the shelve operation.</param> /// <param name="user">The user performing the shelve operation.</param>
/// <param name="unshelveAtUtc">The UTC time at which the shelve will automatically expire.</param> /// <param name="unshelveAtUtc">The UTC time at which the shelve will automatically expire.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task TimedShelveAsync(string alarmId, string user, DateTime unshelveAtUtc, CancellationToken ct) public Task TimedShelveAsync(string alarmId, string user, DateTime unshelveAtUtc, CancellationToken ct)
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyTimedShelve(cur, user, unshelveAtUtc, _clock())); => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyTimedShelve(cur, user, unshelveAtUtc, _clock()));
@@ -348,6 +357,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// <param name="alarmId">The alarm identifier.</param> /// <param name="alarmId">The alarm identifier.</param>
/// <param name="user">The user performing the unshelve operation.</param> /// <param name="user">The user performing the unshelve operation.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task UnshelveAsync(string alarmId, string user, CancellationToken ct) public Task UnshelveAsync(string alarmId, string user, CancellationToken ct)
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyUnshelve(cur, user, _clock())); => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyUnshelve(cur, user, _clock()));
@@ -355,6 +365,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// <param name="alarmId">The alarm identifier.</param> /// <param name="alarmId">The alarm identifier.</param>
/// <param name="user">The user performing the enable operation.</param> /// <param name="user">The user performing the enable operation.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task EnableAsync(string alarmId, string user, CancellationToken ct) public Task EnableAsync(string alarmId, string user, CancellationToken ct)
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyEnable(cur, user, _clock())); => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyEnable(cur, user, _clock()));
@@ -362,6 +373,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// <param name="alarmId">The alarm identifier.</param> /// <param name="alarmId">The alarm identifier.</param>
/// <param name="user">The user performing the disable operation.</param> /// <param name="user">The user performing the disable operation.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task DisableAsync(string alarmId, string user, CancellationToken ct) public Task DisableAsync(string alarmId, string user, CancellationToken ct)
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyDisable(cur, user, _clock())); => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyDisable(cur, user, _clock()));
@@ -370,6 +382,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
/// <param name="user">The user adding the comment.</param> /// <param name="user">The user adding the comment.</param>
/// <param name="text">The comment text.</param> /// <param name="text">The comment text.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task AddCommentAsync(string alarmId, string user, string text, CancellationToken ct) public Task AddCommentAsync(string alarmId, string user, string text, CancellationToken ct)
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAddComment(cur, user, text, _clock())); => ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAddComment(cur, user, text, _clock()));
@@ -39,6 +39,7 @@ public static class DependencyExtractor
/// paths, or a list of rejection messages if non-literal paths were used. /// paths, or a list of rejection messages if non-literal paths were used.
/// </summary> /// </summary>
/// <param name="scriptSource">The script source code to analyze.</param> /// <param name="scriptSource">The script source code to analyze.</param>
/// <returns>The extracted dependency paths, or rejection messages for unsupported patterns.</returns>
public static DependencyExtractionResult Extract(string scriptSource) public static DependencyExtractionResult Extract(string scriptSource)
{ {
if (string.IsNullOrWhiteSpace(scriptSource)) if (string.IsNullOrWhiteSpace(scriptSource))
@@ -41,6 +41,7 @@ public abstract class ScriptContext
/// right upstream tags at load time. /// right upstream tags at load time.
/// </remarks> /// </remarks>
/// <param name="path">The literal tag path to read.</param> /// <param name="path">The literal tag path to read.</param>
/// <returns>The current <see cref="DataValueSnapshot"/> for the tag, including value, quality, and timestamp.</returns>
public abstract DataValueSnapshot GetTag(string path); public abstract DataValueSnapshot GetTag(string path);
/// <summary> /// <summary>
@@ -81,6 +82,7 @@ public abstract class ScriptContext
/// <param name="current">The current value to check.</param> /// <param name="current">The current value to check.</param>
/// <param name="previous">The previous value to compare against.</param> /// <param name="previous">The previous value to compare against.</param>
/// <param name="tolerance">The minimum difference threshold for a change to be detected.</param> /// <param name="tolerance">The minimum difference threshold for a change to be detected.</param>
/// <returns><see langword="true"/> when the absolute difference between current and previous exceeds tolerance.</returns>
public static bool Deadband(double current, double previous, double tolerance) public static bool Deadband(double current, double previous, double tolerance)
=> Math.Abs(current - previous) > tolerance; => Math.Abs(current - previous) > tolerance;
} }
@@ -66,6 +66,7 @@ public sealed class ScriptEvaluator<TContext, TResult> : IDisposable
/// <summary>Compiles user script source into an evaluator.</summary> /// <summary>Compiles user script source into an evaluator.</summary>
/// <param name="scriptSource">The user script source code to compile.</param> /// <param name="scriptSource">The user script source code to compile.</param>
/// <returns>A compiled <see cref="ScriptEvaluator{TContext, TResult}"/> ready to invoke.</returns>
public static ScriptEvaluator<TContext, TResult> Compile(string scriptSource) public static ScriptEvaluator<TContext, TResult> Compile(string scriptSource)
{ {
if (scriptSource is null) throw new ArgumentNullException(nameof(scriptSource)); if (scriptSource is null) throw new ArgumentNullException(nameof(scriptSource));
@@ -173,6 +174,7 @@ public sealed class ScriptEvaluator<TContext, TResult> : IDisposable
/// <summary>Runs the script against an already-constructed context.</summary> /// <summary>Runs the script against an already-constructed context.</summary>
/// <param name="context">The script context.</param> /// <param name="context">The script context.</param>
/// <param name="ct">Cancellation token for the operation.</param> /// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A task that resolves to the script's return value.</returns>
public Task<TResult> RunAsync(TContext context, CancellationToken ct = default) public Task<TResult> RunAsync(TContext context, CancellationToken ct = default)
{ {
if (_disposed) throw new ObjectDisposedException(nameof(ScriptEvaluator<TContext, TResult>)); if (_disposed) throw new ObjectDisposedException(nameof(ScriptEvaluator<TContext, TResult>));
@@ -43,6 +43,7 @@ public static class ScriptSandbox
/// to resolve <c>ctx.GetTag(...)</c> calls. /// to resolve <c>ctx.GetTag(...)</c> calls.
/// </summary> /// </summary>
/// <param name="contextType">The concrete script context type to use for compilation.</param> /// <param name="contextType">The concrete script context type to use for compilation.</param>
/// <returns>The sandbox configuration for compiling scripts with the given context type.</returns>
public static SandboxConfig Build(Type contextType) public static SandboxConfig Build(Type contextType)
{ {
if (contextType is null) throw new ArgumentNullException(nameof(contextType)); if (contextType is null) throw new ArgumentNullException(nameof(contextType));
@@ -156,6 +156,7 @@ public sealed class DependencyGraph
/// dependencies. Throws <see cref="DependencyCycleException"/> if any cycle /// dependencies. Throws <see cref="DependencyCycleException"/> if any cycle
/// exists. Implemented via Kahn's algorithm. /// exists. Implemented via Kahn's algorithm.
/// </summary> /// </summary>
/// <returns>A list of node IDs in topological evaluation order.</returns>
public IReadOnlyList<string> TopologicalSort() public IReadOnlyList<string> TopologicalSort()
{ {
// Kahn's framing: edge u -> v means "u must come before v". For dependencies, // 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 /// 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. /// rejection pass so operators see all of them, not just one at a time.
/// </summary> /// </summary>
/// <returns>A list of strongly-connected components that form cycles; empty if the graph is acyclic.</returns>
public IReadOnlyList<IReadOnlyList<string>> DetectCycles() public IReadOnlyList<IReadOnlyList<string>> DetectCycles()
{ {
// Iterative Tarjan's SCC. Avoids recursion so deep graphs don't StackOverflow. // Iterative Tarjan's SCC. Avoids recursion so deep graphs don't StackOverflow.
@@ -30,6 +30,7 @@ public interface ITagUpstreamSource
/// when the path isn't configured. /// when the path isn't configured.
/// </summary> /// </summary>
/// <param name="path">The tag path to read.</param> /// <param name="path">The tag path to read.</param>
/// <returns>The last-known value and quality snapshot for the tag.</returns>
DataValueSnapshot ReadTag(string path); DataValueSnapshot ReadTag(string path);
/// <summary> /// <summary>
@@ -40,5 +41,6 @@ public interface ITagUpstreamSource
/// </summary> /// </summary>
/// <param name="path">The tag path to subscribe to.</param> /// <param name="path">The tag path to subscribe to.</param>
/// <param name="observer">The callback to invoke when the value changes.</param> /// <param name="observer">The callback to invoke when the value changes.</param>
/// <returns>An <see cref="IDisposable"/> that cancels the subscription when disposed.</returns>
IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer); IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer);
} }
@@ -198,6 +198,7 @@ public sealed class VirtualTagEngine : IDisposable
/// default. Also called after a config reload. /// default. Also called after a config reload.
/// </summary> /// </summary>
/// <param name="ct">Cancellation token to stop evaluation.</param> /// <param name="ct">Cancellation token to stop evaluation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task EvaluateAllAsync(CancellationToken ct = default) public async Task EvaluateAllAsync(CancellationToken ct = default)
{ {
EnsureLoaded(); EnsureLoaded();
@@ -212,6 +213,7 @@ public sealed class VirtualTagEngine : IDisposable
/// <summary>Evaluate a single tag — used by the timer trigger + test hooks.</summary> /// <summary>Evaluate a single tag — used by the timer trigger + test hooks.</summary>
/// <param name="path">Path of the virtual tag to evaluate.</param> /// <param name="path">Path of the virtual tag to evaluate.</param>
/// <param name="ct">Cancellation token to stop evaluation.</param> /// <param name="ct">Cancellation token to stop evaluation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public Task EvaluateOneAsync(string path, CancellationToken ct = default) public Task EvaluateOneAsync(string path, CancellationToken ct = default)
{ {
EnsureLoaded(); EnsureLoaded();
@@ -226,6 +228,7 @@ public sealed class VirtualTagEngine : IDisposable
/// evaluation result. /// evaluation result.
/// </summary> /// </summary>
/// <param name="path">Path of the tag to read.</param> /// <param name="path">Path of the tag to read.</param>
/// <returns>The most recently cached value and quality for the tag path.</returns>
public DataValueSnapshot Read(string path) public DataValueSnapshot Read(string path)
{ {
if (string.IsNullOrWhiteSpace(path)) if (string.IsNullOrWhiteSpace(path))
@@ -242,6 +245,7 @@ public sealed class VirtualTagEngine : IDisposable
/// </summary> /// </summary>
/// <param name="path">Path of the tag to subscribe to.</param> /// <param name="path">Path of the tag to subscribe to.</param>
/// <param name="observer">Callback invoked with the tag path and new value on each evaluation.</param> /// <param name="observer">Callback invoked with the tag path and new value on each evaluation.</param>
/// <returns>An <see cref="IDisposable"/> that cancels the subscription when disposed.</returns>
public IDisposable Subscribe(string path, Action<string, DataValueSnapshot> observer) public IDisposable Subscribe(string path, Action<string, DataValueSnapshot> observer)
{ {
// Race-safe pattern paired with Unsub.Dispose: if Unsub.Dispose removed the // Race-safe pattern paired with Unsub.Dispose: if Unsub.Dispose removed the
@@ -19,6 +19,7 @@ public sealed record AuthorizationDecision(
public bool IsAllowed => Verdict == AuthorizationVerdict.Allow; public bool IsAllowed => Verdict == AuthorizationVerdict.Allow;
/// <summary>Convenience constructor for the common "no grants matched" outcome.</summary> /// <summary>Convenience constructor for the common "no grants matched" outcome.</summary>
/// <returns>An <see cref="AuthorizationDecision"/> with <see cref="AuthorizationVerdict.NotGranted"/> and empty provenance.</returns>
public static AuthorizationDecision NotGranted() => new(AuthorizationVerdict.NotGranted, []); public static AuthorizationDecision NotGranted() => new(AuthorizationVerdict.NotGranted, []);
/// <summary>Allow with the list of grants that matched.</summary> /// <summary>Allow with the list of grants that matched.</summary>
@@ -22,5 +22,6 @@ public interface IPermissionEvaluator
/// <param name="session">The user session containing resolved LDAP groups and roles.</param> /// <param name="session">The user session containing resolved LDAP groups and roles.</param>
/// <param name="operation">The OPC UA operation being requested.</param> /// <param name="operation">The OPC UA operation being requested.</param>
/// <param name="scope">The node address scope being accessed.</param> /// <param name="scope">The node address scope being accessed.</param>
/// <returns>An <see cref="AuthorizationDecision"/> indicating whether the operation is allowed.</returns>
AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope); AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope);
} }
@@ -33,6 +33,7 @@ public sealed class PermissionTrie
/// </summary> /// </summary>
/// <param name="scope">The node scope to match permissions for.</param> /// <param name="scope">The node scope to match permissions for.</param>
/// <param name="ldapGroups">The user's LDAP group memberships.</param> /// <param name="ldapGroups">The user's LDAP group memberships.</param>
/// <returns>The list of grants that apply to the given scope for any of the session's LDAP groups.</returns>
public IReadOnlyList<MatchedGrant> CollectMatches(NodeScope scope, IEnumerable<string> ldapGroups) public IReadOnlyList<MatchedGrant> CollectMatches(NodeScope scope, IEnumerable<string> ldapGroups)
{ {
ArgumentNullException.ThrowIfNull(scope); ArgumentNullException.ThrowIfNull(scope);
@@ -41,6 +41,7 @@ public static class PermissionTrieBuilder
/// Core-011 production hazard. The callback fires only when <paramref name="scopePaths"/> /// Core-011 production hazard. The callback fires only when <paramref name="scopePaths"/>
/// is non-null (a null lookup is the explicit deterministic-test fallback mode). /// is non-null (a null lookup is the explicit deterministic-test fallback mode).
/// </param> /// </param>
/// <returns>An immutable <see cref="PermissionTrie"/> for the given cluster and generation.</returns>
public static PermissionTrie Build( public static PermissionTrie Build(
string clusterId, string clusterId,
long generationId, long generationId,
@@ -34,6 +34,7 @@ public sealed class PermissionTrieCache
/// <summary>Get the current-generation trie for a cluster; null when nothing installed.</summary> /// <summary>Get the current-generation trie for a cluster; null when nothing installed.</summary>
/// <param name="clusterId">The cluster identifier.</param> /// <param name="clusterId">The cluster identifier.</param>
/// <returns>The current-generation trie, or null if nothing is installed for the cluster.</returns>
public PermissionTrie? GetTrie(string clusterId) public PermissionTrie? GetTrie(string clusterId)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId); ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
@@ -43,6 +44,7 @@ public sealed class PermissionTrieCache
/// <summary>Get a specific (cluster, generation) trie; null if that pair isn't cached.</summary> /// <summary>Get a specific (cluster, generation) trie; null if that pair isn't cached.</summary>
/// <param name="clusterId">The cluster identifier.</param> /// <param name="clusterId">The cluster identifier.</param>
/// <param name="generationId">The generation identifier.</param> /// <param name="generationId">The generation identifier.</param>
/// <returns>The trie for the specified cluster and generation, or null if not cached.</returns>
public PermissionTrie? GetTrie(string clusterId, long generationId) public PermissionTrie? GetTrie(string clusterId, long generationId)
{ {
if (!_byCluster.TryGetValue(clusterId, out var entry)) return null; if (!_byCluster.TryGetValue(clusterId, out var entry)) return null;
@@ -51,6 +53,7 @@ public sealed class PermissionTrieCache
/// <summary>The generation id the <see cref="GetTrie(string)"/> shortcut currently serves for a cluster.</summary> /// <summary>The generation id the <see cref="GetTrie(string)"/> shortcut currently serves for a cluster.</summary>
/// <param name="clusterId">The cluster identifier.</param> /// <param name="clusterId">The cluster identifier.</param>
/// <returns>The current generation ID, or null if no trie is installed for the cluster.</returns>
public long? CurrentGenerationId(string clusterId) public long? CurrentGenerationId(string clusterId)
=> _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null; => _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null;
@@ -111,11 +114,13 @@ public sealed class PermissionTrieCache
/// <summary>Creates a cluster entry from a single trie.</summary> /// <summary>Creates a cluster entry from a single trie.</summary>
/// <param name="trie">The permission trie to create the entry from.</param> /// <param name="trie">The permission trie to create the entry from.</param>
/// <returns>A new <see cref="ClusterEntry"/> containing the single trie as the current generation.</returns>
public static ClusterEntry FromSingle(PermissionTrie trie) => public static ClusterEntry FromSingle(PermissionTrie trie) =>
new(trie, new Dictionary<long, PermissionTrie> { [trie.GenerationId] = trie }); new(trie, new Dictionary<long, PermissionTrie> { [trie.GenerationId] = trie });
/// <summary>Creates a new entry with an additional trie, updating current if it's newer.</summary> /// <summary>Creates a new entry with an additional trie, updating current if it's newer.</summary>
/// <param name="trie">The new permission trie to add.</param> /// <param name="trie">The new permission trie to add.</param>
/// <returns>A new <see cref="ClusterEntry"/> with the trie added and the current pointer updated if the new generation is newer.</returns>
public ClusterEntry WithAdditional(PermissionTrie trie) public ClusterEntry WithAdditional(PermissionTrie trie)
{ {
var next = new Dictionary<long, PermissionTrie>(Tries) { [trie.GenerationId] = trie }; var next = new Dictionary<long, PermissionTrie>(Tries) { [trie.GenerationId] = trie };
@@ -24,11 +24,7 @@ public sealed class TriePermissionEvaluator : IPermissionEvaluator
_timeProvider = timeProvider ?? TimeProvider.System; _timeProvider = timeProvider ?? TimeProvider.System;
} }
/// <summary>Authorizes an operation against the user's session and node scope.</summary> /// <inheritdoc />
/// <param name="session">The user's authorization session.</param>
/// <param name="operation">The OPC UA operation to authorize.</param>
/// <param name="scope">The target node scope.</param>
/// <returns>An authorization decision indicating whether the operation is allowed.</returns>
public AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope) public AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope)
{ {
ArgumentNullException.ThrowIfNull(session); ArgumentNullException.ThrowIfNull(session);
@@ -64,6 +64,7 @@ public sealed record UserAuthorizationState
/// whenever this is true. /// whenever this is true.
/// </summary> /// </summary>
/// <param name="utcNow">The current UTC time.</param> /// <param name="utcNow">The current UTC time.</param>
/// <returns><c>true</c> when the state exceeds its maximum staleness ceiling.</returns>
public bool IsStale(DateTime utcNow) => utcNow - MembershipResolvedUtc > AuthCacheMaxStaleness; public bool IsStale(DateTime utcNow) => utcNow - MembershipResolvedUtc > AuthCacheMaxStaleness;
/// <summary> /// <summary>
@@ -72,6 +73,7 @@ public sealed record UserAuthorizationState
/// call still evaluates against the cached memberships. /// call still evaluates against the cached memberships.
/// </summary> /// </summary>
/// <param name="utcNow">The current UTC time.</param> /// <param name="utcNow">The current UTC time.</param>
/// <returns><c>true</c> when a background refresh should be initiated but the current cached memberships are still usable.</returns>
public bool NeedsRefresh(DateTime utcNow) => public bool NeedsRefresh(DateTime utcNow) =>
!IsStale(utcNow) && utcNow - MembershipResolvedUtc > MembershipFreshnessInterval; !IsStale(utcNow) && utcNow - MembershipResolvedUtc > MembershipFreshnessInterval;
} }
@@ -63,6 +63,7 @@ public sealed class DriverFactoryRegistry
/// missing-assembly deployment doesn't take down the whole server. /// missing-assembly deployment doesn't take down the whole server.
/// </summary> /// </summary>
/// <param name="driverType">The driver type to look up.</param> /// <param name="driverType">The driver type to look up.</param>
/// <returns>The registered factory delegate, or <see langword="null"/> if no factory was registered for the type.</returns>
public Func<string, string, IDriver>? TryGet(string driverType) public Func<string, string, IDriver>? TryGet(string driverType)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(driverType); ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
@@ -75,6 +76,7 @@ public sealed class DriverFactoryRegistry
/// case upstream; we don't double-surface that failure here. /// case upstream; we don't double-surface that failure here.
/// </summary> /// </summary>
/// <param name="driverType">The driver type to look up.</param> /// <param name="driverType">The driver type to look up.</param>
/// <returns>The registered <see cref="DriverTier"/>, or <see cref="DriverTier.A"/> if the type is unknown.</returns>
public DriverTier GetTier(string driverType) public DriverTier GetTier(string driverType)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(driverType); ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
@@ -20,16 +20,13 @@ public sealed class DriverFactoryRegistryAdapter : IDriverFactory
_registry = registry; _registry = registry;
} }
/// <summary>Attempts to create a driver instance by type and configuration.</summary> /// <inheritdoc />
/// <param name="driverType">The driver type name.</param>
/// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="driverConfigJson">The driver configuration as a JSON string.</param>
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson) public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
{ {
var factory = _registry.TryGet(driverType); var factory = _registry.TryGet(driverType);
return factory?.Invoke(driverInstanceId, driverConfigJson); return factory?.Invoke(driverInstanceId, driverConfigJson);
} }
/// <summary>Gets the collection of supported driver type names.</summary> /// <inheritdoc />
public IReadOnlyCollection<string> SupportedTypes => _registry.RegisteredTypes; public IReadOnlyCollection<string> SupportedTypes => _registry.RegisteredTypes;
} }
@@ -21,6 +21,7 @@ public sealed class DriverHost : IAsyncDisposable
/// <summary>Gets the health status of a registered driver.</summary> /// <summary>Gets the health status of a registered driver.</summary>
/// <param name="driverInstanceId">The driver instance identifier to query.</param> /// <param name="driverInstanceId">The driver instance identifier to query.</param>
/// <returns>The driver health if the driver is registered; otherwise null.</returns>
public DriverHealth? GetHealth(string driverInstanceId) public DriverHealth? GetHealth(string driverInstanceId)
{ {
lock (_lock) lock (_lock)
@@ -33,6 +34,7 @@ public sealed class DriverHost : IAsyncDisposable
/// startup. Returns null when the driver is not registered. /// startup. Returns null when the driver is not registered.
/// </summary> /// </summary>
/// <param name="driverInstanceId">The driver instance identifier to look up.</param> /// <param name="driverInstanceId">The driver instance identifier to look up.</param>
/// <returns>The driver instance if registered; otherwise null.</returns>
public IDriver? GetDriver(string driverInstanceId) public IDriver? GetDriver(string driverInstanceId)
{ {
lock (_lock) lock (_lock)
@@ -47,6 +49,7 @@ public sealed class DriverHost : IAsyncDisposable
/// <param name="driver">The driver instance to register.</param> /// <param name="driver">The driver instance to register.</param>
/// <param name="driverConfigJson">The configuration JSON for the driver.</param> /// <param name="driverConfigJson">The configuration JSON for the driver.</param>
/// <param name="ct">Cancellation token for the operation.</param> /// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task RegisterAsync(IDriver driver, string driverConfigJson, CancellationToken ct) public async Task RegisterAsync(IDriver driver, string driverConfigJson, CancellationToken ct)
{ {
ArgumentNullException.ThrowIfNull(driver); ArgumentNullException.ThrowIfNull(driver);
@@ -70,6 +73,7 @@ public sealed class DriverHost : IAsyncDisposable
/// <summary>Unregisters a driver and calls shutdown.</summary> /// <summary>Unregisters a driver and calls shutdown.</summary>
/// <param name="driverInstanceId">The driver instance identifier to unregister.</param> /// <param name="driverInstanceId">The driver instance identifier to unregister.</param>
/// <param name="ct">Cancellation token for the operation.</param> /// <param name="ct">Cancellation token for the operation.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
public async Task UnregisterAsync(string driverInstanceId, CancellationToken ct) public async Task UnregisterAsync(string driverInstanceId, CancellationToken ct)
{ {
IDriver? driver; IDriver? driver;
@@ -84,6 +88,7 @@ public sealed class DriverHost : IAsyncDisposable
} }
/// <summary>Disposes the driver host and all registered drivers.</summary> /// <summary>Disposes the driver host and all registered drivers.</summary>
/// <returns>A value task that represents the asynchronous operation.</returns>
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
List<IDriver> snapshot; List<IDriver> snapshot;
@@ -27,6 +27,7 @@ public static class DriverHealthReport
{ {
/// <summary>Compute the fleet-wide readiness verdict from per-driver states.</summary> /// <summary>Compute the fleet-wide readiness verdict from per-driver states.</summary>
/// <param name="drivers">The list of per-driver health snapshots to aggregate.</param> /// <param name="drivers">The list of per-driver health snapshots to aggregate.</param>
/// <returns>The fleet-wide <see cref="ReadinessVerdict"/> derived from all driver states.</returns>
public static ReadinessVerdict Aggregate(IReadOnlyList<DriverHealthSnapshot> drivers) public static ReadinessVerdict Aggregate(IReadOnlyList<DriverHealthSnapshot> drivers)
{ {
ArgumentNullException.ThrowIfNull(drivers); ArgumentNullException.ThrowIfNull(drivers);
@@ -54,6 +55,7 @@ public static class DriverHealthReport
/// return per the Stream C.1 state matrix. /// return per the Stream C.1 state matrix.
/// </summary> /// </summary>
/// <param name="verdict">The readiness verdict to map to HTTP status.</param> /// <param name="verdict">The readiness verdict to map to HTTP status.</param>
/// <returns>The HTTP status code (200 or 503) corresponding to the verdict.</returns>
public static int HttpStatus(ReadinessVerdict verdict) => verdict switch public static int HttpStatus(ReadinessVerdict verdict) => verdict switch
{ {
ReadinessVerdict.Healthy => 200, ReadinessVerdict.Healthy => 200,
@@ -22,6 +22,7 @@ public static class LogContextEnricher
/// <param name="driverType">The driver type name.</param> /// <param name="driverType">The driver type name.</param>
/// <param name="capability">The driver capability being invoked.</param> /// <param name="capability">The driver capability being invoked.</param>
/// <param name="correlationId">The correlation ID for tracing the call.</param> /// <param name="correlationId">The correlation ID for tracing the call.</param>
/// <returns>A scope that pops the pushed properties when disposed.</returns>
public static IDisposable Push(string driverInstanceId, string driverType, DriverCapability capability, string correlationId) public static IDisposable Push(string driverInstanceId, string driverType, DriverCapability capability, string correlationId)
{ {
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId); ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
@@ -40,6 +41,7 @@ public static class LogContextEnricher
/// 12-hex-char slice of a GUID — long enough for log correlation, short enough to /// 12-hex-char slice of a GUID — long enough for log correlation, short enough to
/// scan visually. /// scan visually.
/// </summary> /// </summary>
/// <returns>A 12-character hex string suitable for log correlation.</returns>
public static string NewCorrelationId() => Guid.NewGuid().ToString("N")[..12]; public static string NewCorrelationId() => Guid.NewGuid().ToString("N")[..12];
private sealed class CompositeScope : IDisposable private sealed class CompositeScope : IDisposable
@@ -183,6 +183,7 @@ public static class EquipmentNodeWalker
/// wants an opaque non-JSON reference. /// wants an opaque non-JSON reference.
/// </remarks> /// </remarks>
/// <param name="tagConfig">The tag configuration JSON or string.</param> /// <param name="tagConfig">The tag configuration JSON or string.</param>
/// <returns>The value of the <c>FullName</c> field from the JSON, or the raw <paramref name="tagConfig"/> string as a fallback.</returns>
internal static string ExtractFullName(string tagConfig) internal static string ExtractFullName(string tagConfig)
{ {
if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig; if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig;
@@ -49,6 +49,7 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
/// </summary> /// </summary>
/// <param name="builder">The address space builder to populate.</param> /// <param name="builder">The address space builder to populate.</param>
/// <param name="ct">The cancellation token.</param> /// <param name="ct">The cancellation token.</param>
/// <returns>A task that represents the asynchronous address space build operation.</returns>
public async Task BuildAddressSpaceAsync(IAddressSpaceBuilder builder, CancellationToken ct) public async Task BuildAddressSpaceAsync(IAddressSpaceBuilder builder, CancellationToken ct)
{ {
ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(builder);
@@ -111,23 +112,15 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
IAddressSpaceBuilder inner, IAddressSpaceBuilder inner,
ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IAddressSpaceBuilder ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IAddressSpaceBuilder
{ {
/// <summary>Adds a folder to the address space.</summary> /// <inheritdoc />
/// <param name="browseName">The browse name of the folder node.</param>
/// <param name="displayName">The display name of the folder node.</param>
public IAddressSpaceBuilder Folder(string browseName, string displayName) public IAddressSpaceBuilder Folder(string browseName, string displayName)
=> new CapturingBuilder(inner.Folder(browseName, displayName), sinks); => new CapturingBuilder(inner.Folder(browseName, displayName), sinks);
/// <summary>Adds a variable to the address space.</summary> /// <inheritdoc />
/// <param name="browseName">The browse name of the variable node.</param>
/// <param name="displayName">The display name of the variable node.</param>
/// <param name="attributeInfo">Metadata describing the variable's data type and properties.</param>
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo) public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
=> new CapturingHandle(inner.Variable(browseName, displayName, attributeInfo), sinks); => new CapturingHandle(inner.Variable(browseName, displayName, attributeInfo), sinks);
/// <summary>Adds a property to the address space.</summary> /// <inheritdoc />
/// <param name="browseName">The browse name of the property node.</param>
/// <param name="dataType">The OPC UA data type of the property.</param>
/// <param name="value">The initial value of the property, or null.</param>
public void AddProperty(string browseName, DriverDataType dataType, object? value) public void AddProperty(string browseName, DriverDataType dataType, object? value)
=> inner.AddProperty(browseName, dataType, value); => inner.AddProperty(browseName, dataType, value);
} }
@@ -136,11 +129,10 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
IVariableHandle inner, IVariableHandle inner,
ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IVariableHandle ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IVariableHandle
{ {
/// <summary>Gets the full reference for the variable.</summary> /// <inheritdoc />
public string FullReference => inner.FullReference; public string FullReference => inner.FullReference;
/// <summary>Marks the variable as an alarm condition and registers its sink.</summary> /// <inheritdoc />
/// <param name="info">Configuration for the alarm condition.</param>
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info) public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
{ {
var sink = inner.MarkAsAlarmCondition(info); var sink = inner.MarkAsAlarmCondition(info);
@@ -59,6 +59,7 @@ public sealed class AlarmSurfaceInvoker
/// </summary> /// </summary>
/// <param name="sourceNodeIds">The source node IDs to subscribe to.</param> /// <param name="sourceNodeIds">The source node IDs to subscribe to.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that resolves to one subscription handle per resolved host.</returns>
public async Task<IReadOnlyList<IAlarmSubscriptionHandle>> SubscribeAsync( public async Task<IReadOnlyList<IAlarmSubscriptionHandle>> SubscribeAsync(
IReadOnlyList<string> sourceNodeIds, IReadOnlyList<string> sourceNodeIds,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -89,6 +90,7 @@ public sealed class AlarmSurfaceInvoker
/// </summary> /// </summary>
/// <param name="handle">The subscription handle to unsubscribe.</param> /// <param name="handle">The subscription handle to unsubscribe.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous unsubscribe operation.</returns>
public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
{ {
ArgumentNullException.ThrowIfNull(handle); ArgumentNullException.ThrowIfNull(handle);
@@ -110,6 +112,7 @@ public sealed class AlarmSurfaceInvoker
/// </summary> /// </summary>
/// <param name="acknowledgements">The alarm acknowledgement requests.</param> /// <param name="acknowledgements">The alarm acknowledgement requests.</param>
/// <param name="cancellationToken">The cancellation token.</param> /// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that represents the asynchronous acknowledgement operation.</returns>
public async Task AcknowledgeAsync( public async Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
CancellationToken cancellationToken) CancellationToken cancellationToken)
@@ -166,7 +169,7 @@ public sealed class AlarmSurfaceInvoker
public IAlarmSubscriptionHandle Inner { get; } = inner; public IAlarmSubscriptionHandle Inner { get; } = inner;
/// <summary>Gets the resolved host name.</summary> /// <summary>Gets the resolved host name.</summary>
public string Host { get; } = host; public string Host { get; } = host;
/// <summary>Gets the diagnostic ID from the inner handle.</summary> /// <inheritdoc />
public string DiagnosticId => Inner.DiagnosticId; public string DiagnosticId => Inner.DiagnosticId;
} }
} }
@@ -58,6 +58,7 @@ public sealed class CapabilityInvoker
/// <param name="hostName">The host name for logging and status tracking.</param> /// <param name="hostName">The host name for logging and status tracking.</param>
/// <param name="callSite">The async function to execute.</param> /// <param name="callSite">The async function to execute.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The result produced by <paramref name="callSite"/> after executing through the pipeline.</returns>
public async ValueTask<TResult> ExecuteAsync<TResult>( public async ValueTask<TResult> ExecuteAsync<TResult>(
DriverCapability capability, DriverCapability capability,
string hostName, string hostName,
@@ -86,6 +87,7 @@ public sealed class CapabilityInvoker
/// <param name="hostName">The host name for logging and status tracking.</param> /// <param name="hostName">The host name for logging and status tracking.</param>
/// <param name="callSite">The async function to execute.</param> /// <param name="callSite">The async function to execute.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A value task that represents the asynchronous operation.</returns>
public async ValueTask ExecuteAsync( public async ValueTask ExecuteAsync(
DriverCapability capability, DriverCapability capability,
string hostName, string hostName,
@@ -121,6 +123,7 @@ public sealed class CapabilityInvoker
/// <param name="isIdempotent">Whether the write operation is idempotent.</param> /// <param name="isIdempotent">Whether the write operation is idempotent.</param>
/// <param name="callSite">The async function to execute.</param> /// <param name="callSite">The async function to execute.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param> /// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>The result produced by <paramref name="callSite"/> after executing through the write pipeline.</returns>
public async ValueTask<TResult> ExecuteWriteAsync<TResult>( public async ValueTask<TResult> ExecuteWriteAsync<TResult>(
string hostName, string hostName,
bool isIdempotent, bool isIdempotent,
@@ -50,6 +50,7 @@ public static class DriverResilienceOptionsParser
/// <param name="tier">The driver tier for default resilience options.</param> /// <param name="tier">The driver tier for default resilience options.</param>
/// <param name="resilienceConfigJson">The optional JSON configuration string to parse.</param> /// <param name="resilienceConfigJson">The optional JSON configuration string to parse.</param>
/// <param name="parseDiagnostic">An out parameter containing diagnostic information if parsing fails.</param> /// <param name="parseDiagnostic">An out parameter containing diagnostic information if parsing fails.</param>
/// <returns>The effective resilience options; tier defaults when the JSON is null or malformed.</returns>
public static DriverResilienceOptions ParseOrDefaults( public static DriverResilienceOptions ParseOrDefaults(
DriverTier tier, DriverTier tier,
string? resilienceConfigJson, string? resilienceConfigJson,
@@ -54,6 +54,7 @@ public sealed class DriverResiliencePipelineBuilder
/// </param> /// </param>
/// <param name="capability">Which capability surface is being called.</param> /// <param name="capability">Which capability surface is being called.</param>
/// <param name="options">Per-driver-instance options (tier + per-capability overrides).</param> /// <param name="options">Per-driver-instance options (tier + per-capability overrides).</param>
/// <returns>The cached or newly created <see cref="ResiliencePipeline"/> for the given key.</returns>
public ResiliencePipeline GetOrCreate( public ResiliencePipeline GetOrCreate(
string driverInstanceId, string driverInstanceId,
string hostName, string hostName,
@@ -128,10 +128,12 @@ public sealed class DriverResilienceStatusTracker
/// <summary>Snapshot of a specific (instance, host) pair; null if no counters recorded yet.</summary> /// <summary>Snapshot of a specific (instance, host) pair; null if no counters recorded yet.</summary>
/// <param name="driverInstanceId">The driver instance identifier.</param> /// <param name="driverInstanceId">The driver instance identifier.</param>
/// <param name="hostName">The host name.</param> /// <param name="hostName">The host name.</param>
/// <returns>The current <see cref="ResilienceStatusSnapshot"/> for the pair, or <see langword="null"/> if no counters have been recorded.</returns>
public ResilienceStatusSnapshot? TryGet(string driverInstanceId, string hostName) => public ResilienceStatusSnapshot? TryGet(string driverInstanceId, string hostName) =>
_status.TryGetValue(new StatusKey(driverInstanceId, hostName), out var snapshot) ? snapshot : null; _status.TryGetValue(new StatusKey(driverInstanceId, hostName), out var snapshot) ? snapshot : null;
/// <summary>Copy of every currently-tracked (instance, host, snapshot) triple. Safe under concurrent writes.</summary> /// <summary>Copy of every currently-tracked (instance, host, snapshot) triple. Safe under concurrent writes.</summary>
/// <returns>A snapshot list of all currently tracked driver instance and host resilience states.</returns>
public IReadOnlyList<(string DriverInstanceId, string HostName, ResilienceStatusSnapshot Snapshot)> Snapshot() => public IReadOnlyList<(string DriverInstanceId, string HostName, ResilienceStatusSnapshot Snapshot)> Snapshot() =>
_status.Select(kvp => (kvp.Key.DriverInstanceId, kvp.Key.HostName, kvp.Value)).ToList(); _status.Select(kvp => (kvp.Key.DriverInstanceId, kvp.Key.HostName, kvp.Value)).ToList();
@@ -33,6 +33,7 @@ public sealed class MemoryTracking
/// <summary>Tier-default multiplier/floor constants per decision #146.</summary> /// <summary>Tier-default multiplier/floor constants per decision #146.</summary>
/// <param name="tier">The driver tier.</param> /// <param name="tier">The driver tier.</param>
/// <returns>A tuple with the growth multiplier and the minimum floor bytes for the specified tier.</returns>
public static (int Multiplier, long FloorBytes) GetTierConstants(DriverTier tier) => tier switch public static (int Multiplier, long FloorBytes) GetTierConstants(DriverTier tier) => tier switch
{ {
DriverTier.A => (Multiplier: 3, FloorBytes: 50L * 1024 * 1024), DriverTier.A => (Multiplier: 3, FloorBytes: 50L * 1024 * 1024),
@@ -73,6 +74,7 @@ public sealed class MemoryTracking
/// </summary> /// </summary>
/// <param name="footprintBytes">The current memory footprint in bytes.</param> /// <param name="footprintBytes">The current memory footprint in bytes.</param>
/// <param name="utcNow">The current UTC time.</param> /// <param name="utcNow">The current UTC time.</param>
/// <returns>The <see cref="MemoryTrackingAction"/> classifying this sample against the soft/hard thresholds.</returns>
public MemoryTrackingAction Sample(long footprintBytes, DateTime utcNow) public MemoryTrackingAction Sample(long footprintBytes, DateTime utcNow)
{ {
if (_phase == TrackingPhase.WarmingUp) if (_phase == TrackingPhase.WarmingUp)
@@ -60,6 +60,7 @@ public abstract class AbCipCommandBase : DriverCommandBase
/// probe loop would race the operator's own reads. /// probe loop would race the operator's own reads.
/// </summary> /// </summary>
/// <param name="tags">The list of tag definitions to include in the options.</param> /// <param name="tags">The list of tag definitions to include in the options.</param>
/// <returns>A fully-configured <see cref="AbCipDriverOptions"/> with probe and alarm projection disabled.</returns>
protected AbCipDriverOptions BuildOptions(IReadOnlyList<AbCipTagDefinition> tags) => new() protected AbCipDriverOptions BuildOptions(IReadOnlyList<AbCipTagDefinition> tags) => new()
{ {
Devices = [new AbCipDeviceOptions( Devices = [new AbCipDeviceOptions(
@@ -66,6 +66,7 @@ public sealed class ReadCommand : AbCipCommandBase
/// </summary> /// </summary>
/// <param name="tagPath">The symbolic tag path.</param> /// <param name="tagPath">The symbolic tag path.</param>
/// <param name="type">The data type.</param> /// <param name="type">The data type.</param>
/// <returns>A combined tag-name string in <c>path:type</c> form.</returns>
internal static string SynthesiseTagName(string tagPath, AbCipDataType type) internal static string SynthesiseTagName(string tagPath, AbCipDataType type)
=> $"{tagPath}:{type}"; => $"{tagPath}:{type}";
} }
@@ -58,6 +58,7 @@ public sealed class ReadCommand : AbLegacyCommandBase
/// <summary>Tag-name key the driver uses internally. Address+type is already unique.</summary> /// <summary>Tag-name key the driver uses internally. Address+type is already unique.</summary>
/// <param name="address">The PCCC file address.</param> /// <param name="address">The PCCC file address.</param>
/// <param name="type">The data type of the address.</param> /// <param name="type">The data type of the address.</param>
/// <returns>A combined tag name string in the form <c>address:type</c>.</returns>
internal static string SynthesiseTagName(string address, AbLegacyDataType type) internal static string SynthesiseTagName(string address, AbLegacyDataType type)
=> $"{address}:{type}"; => $"{address}:{type}";
} }
@@ -23,6 +23,7 @@ public static class SnapshotFormatter
/// </summary> /// </summary>
/// <param name="tagName">The tag name to include in the output.</param> /// <param name="tagName">The tag name to include in the output.</param>
/// <param name="snapshot">The data value snapshot to format.</param> /// <param name="snapshot">The data value snapshot to format.</param>
/// <returns>A multi-line string representation of the tag and its value.</returns>
public static string Format(string tagName, DataValueSnapshot snapshot) public static string Format(string tagName, DataValueSnapshot snapshot)
{ {
ArgumentNullException.ThrowIfNull(snapshot); ArgumentNullException.ThrowIfNull(snapshot);
@@ -42,6 +43,7 @@ public static class SnapshotFormatter
/// </summary> /// </summary>
/// <param name="tagName">The tag name to include in the output.</param> /// <param name="tagName">The tag name to include in the output.</param>
/// <param name="result">The write result to format.</param> /// <param name="result">The write result to format.</param>
/// <returns>A single-line string showing the tag name and write status.</returns>
public static string FormatWrite(string tagName, WriteResult result) public static string FormatWrite(string tagName, WriteResult result)
{ {
ArgumentNullException.ThrowIfNull(result); ArgumentNullException.ThrowIfNull(result);
@@ -54,6 +56,7 @@ public static class SnapshotFormatter
/// </summary> /// </summary>
/// <param name="tagNames">The list of tag names to include as rows.</param> /// <param name="tagNames">The list of tag names to include as rows.</param>
/// <param name="snapshots">The list of data value snapshots to format.</param> /// <param name="snapshots">The list of data value snapshots to format.</param>
/// <returns>An aligned table string with tag, value, status, and source-time columns.</returns>
public static string FormatTable( public static string FormatTable(
IReadOnlyList<string> tagNames, IReadOnlyList<DataValueSnapshot> snapshots) IReadOnlyList<string> tagNames, IReadOnlyList<DataValueSnapshot> snapshots)
{ {
@@ -52,6 +52,7 @@ public sealed class ReadCommand : FocasCommandBase
/// <summary>Constructs a tag name from address and data type.</summary> /// <summary>Constructs a tag name from address and data type.</summary>
/// <param name="address">The FOCAS address.</param> /// <param name="address">The FOCAS address.</param>
/// <param name="type">The data type.</param> /// <param name="type">The data type.</param>
/// <returns>A synthesized tag name string combining the address and data type.</returns>
internal static string SynthesiseTagName(string address, FocasDataType type) internal static string SynthesiseTagName(string address, FocasDataType type)
=> $"{address}:{type}"; => $"{address}:{type}";
} }
@@ -47,6 +47,7 @@ public abstract class FocasCommandBase : DriverCommandBase
/// as <c>BadCommunicationError</c>. /// as <c>BadCommunicationError</c>.
/// </summary> /// </summary>
/// <param name="tags">The tag definitions to include in the driver options.</param> /// <param name="tags">The tag definitions to include in the driver options.</param>
/// <returns>A <see cref="FocasDriverOptions"/> configured with the CNC target and the supplied tag list.</returns>
protected FocasDriverOptions BuildOptions(IReadOnlyList<FocasTagDefinition> tags) => new() protected FocasDriverOptions BuildOptions(IReadOnlyList<FocasTagDefinition> tags) => new()
{ {
Devices = [new FocasDeviceOptions( Devices = [new FocasDeviceOptions(
@@ -48,6 +48,7 @@ public abstract class ModbusCommandBase : DriverCommandBase
/// command against its own keep-alive reads. /// command against its own keep-alive reads.
/// </summary> /// </summary>
/// <param name="tags">The tag definitions to include in the options.</param> /// <param name="tags">The tag definitions to include in the options.</param>
/// <returns>A <see cref="ModbusDriverOptions"/> configured for a one-shot CLI run.</returns>
protected ModbusDriverOptions BuildOptions(IReadOnlyList<ModbusTagDefinition> tags) => new() protected ModbusDriverOptions BuildOptions(IReadOnlyList<ModbusTagDefinition> tags) => new()
{ {
Host = Host, Host = Host,
@@ -61,6 +61,7 @@ public sealed class ReadCommand : S7CommandBase
/// <summary>Tag-name key used internally. Address + type is already unique.</summary> /// <summary>Tag-name key used internally. Address + type is already unique.</summary>
/// <param name="address">The S7 address to encode in the tag name.</param> /// <param name="address">The S7 address to encode in the tag name.</param>
/// <param name="type">The data type to encode in the tag name.</param> /// <param name="type">The data type to encode in the tag name.</param>
/// <returns>The synthesised tag name encoding the address and type.</returns>
internal static string SynthesiseTagName(string address, S7DataType type) internal static string SynthesiseTagName(string address, S7DataType type)
=> $"{address}:{type}"; => $"{address}:{type}";
} }
@@ -51,6 +51,7 @@ public abstract class S7CommandBase : DriverCommandBase
/// disabled — CLI runs are one-shot. /// disabled — CLI runs are one-shot.
/// </summary> /// </summary>
/// <param name="tags">The tag definitions to include in the options.</param> /// <param name="tags">The tag definitions to include in the options.</param>
/// <returns>An <see cref="S7DriverOptions"/> populated with the current command-line values and the supplied tags.</returns>
protected S7DriverOptions BuildOptions(IReadOnlyList<S7TagDefinition> tags) => new() protected S7DriverOptions BuildOptions(IReadOnlyList<S7TagDefinition> tags) => new()
{ {
Host = Host, Host = Host,
@@ -90,6 +90,7 @@ public sealed class BrowseCommand : TwinCATCommandBase
/// </summary> /// </summary>
/// <param name="source">The source collection to filter.</param> /// <param name="source">The source collection to filter.</param>
/// <param name="prefix">The prefix to filter on, or null to keep everything.</param> /// <param name="prefix">The prefix to filter on, or null to keep everything.</param>
/// <returns>A filtered list of variables whose browse names start with the given prefix.</returns>
internal static List<(string BrowseName, DriverAttributeInfo Info)> FilterByPrefix( internal static List<(string BrowseName, DriverAttributeInfo Info)> FilterByPrefix(
IReadOnlyList<(string BrowseName, DriverAttributeInfo Info)> source, string? prefix) IReadOnlyList<(string BrowseName, DriverAttributeInfo Info)> source, string? prefix)
=> source => source
@@ -102,6 +103,7 @@ public sealed class BrowseCommand : TwinCATCommandBase
/// </summary> /// </summary>
/// <param name="matchedCount">The number of matched items.</param> /// <param name="matchedCount">The number of matched items.</param>
/// <param name="max">The maximum number to show, or 0 for unbounded.</param> /// <param name="max">The maximum number to show, or 0 for unbounded.</param>
/// <returns>The effective print limit: <paramref name="matchedCount"/> when unbounded, otherwise the lesser of <paramref name="max"/> and <paramref name="matchedCount"/>.</returns>
internal static int PrintLimit(int matchedCount, int max) internal static int PrintLimit(int matchedCount, int max)
=> max <= 0 ? matchedCount : Math.Min(max, matchedCount); => max <= 0 ? matchedCount : Math.Min(max, matchedCount);
@@ -112,6 +114,7 @@ public sealed class BrowseCommand : TwinCATCommandBase
/// authorization is enforced server-side. /// authorization is enforced server-side.
/// </summary> /// </summary>
/// <param name="info">The attribute info to label.</param> /// <param name="info">The attribute info to label.</param>
/// <returns>"RO" for view-only attributes; "RW" for all others.</returns>
internal static string AccessTag(DriverAttributeInfo info) internal static string AccessTag(DriverAttributeInfo info)
=> info.SecurityClass == SecurityClassification.ViewOnly ? "RO" : "RW"; => info.SecurityClass == SecurityClassification.ViewOnly ? "RO" : "RW";
@@ -69,6 +69,7 @@ public sealed class WriteCommand : TwinCATTagCommandBase
/// <summary>Parse <c>--value</c> per <see cref="TwinCATDataType"/>, invariant culture.</summary> /// <summary>Parse <c>--value</c> per <see cref="TwinCATDataType"/>, invariant culture.</summary>
/// <param name="raw">The raw string value to parse.</param> /// <param name="raw">The raw string value to parse.</param>
/// <param name="type">The target TwinCAT data type.</param> /// <param name="type">The target TwinCAT data type.</param>
/// <returns>The parsed value as a boxed .NET object matching the requested data type.</returns>
internal static object ParseValue(string raw, TwinCATDataType type) => type switch internal static object ParseValue(string raw, TwinCATDataType type) => type switch
{ {
TwinCATDataType.Bool => ParseBool(raw), TwinCATDataType.Bool => ParseBool(raw),
@@ -24,6 +24,7 @@ public abstract class TwinCATTagCommandBase : TwinCATCommandBase
/// native notifications toggled by <see cref="PollOnly"/>. /// native notifications toggled by <see cref="PollOnly"/>.
/// </summary> /// </summary>
/// <param name="tags">Tag definitions for the driver.</param> /// <param name="tags">Tag definitions for the driver.</param>
/// <returns>A <see cref="TwinCATDriverOptions"/> configured for a single-device CLI run.</returns>
protected TwinCATDriverOptions BuildOptions(IReadOnlyList<TwinCATTagDefinition> tags) => new() protected TwinCATDriverOptions BuildOptions(IReadOnlyList<TwinCATTagDefinition> tags) => new()
{ {
Devices = [new TwinCATDeviceOptions( Devices = [new TwinCATDeviceOptions(
@@ -39,6 +40,7 @@ public abstract class TwinCATTagCommandBase : TwinCATCommandBase
// ---- Test hook ---- // ---- Test hook ----
/// <summary>Test hook that exposes BuildOptions for unit testing.</summary> /// <summary>Test hook that exposes BuildOptions for unit testing.</summary>
/// <param name="tags">Tag definitions for the driver.</param> /// <param name="tags">Tag definitions for the driver.</param>
/// <returns>A <see cref="TwinCATDriverOptions"/> configured for a single-device CLI run.</returns>
internal TwinCATDriverOptions BuildOptionsForTest(IReadOnlyList<TwinCATTagDefinition> tags) internal TwinCATDriverOptions BuildOptionsForTest(IReadOnlyList<TwinCATTagDefinition> tags)
=> BuildOptions(tags); => BuildOptions(tags);
} }
@@ -130,10 +130,10 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// </summary> /// </summary>
internal AbCipTemplateCache TemplateCache => _templateCache; internal AbCipTemplateCache TemplateCache => _templateCache;
/// <summary>Gets the unique identifier for this driver instance.</summary> /// <inheritdoc />
public string DriverInstanceId => _driverInstanceId; public string DriverInstanceId => _driverInstanceId;
/// <summary>Gets the driver type identifier.</summary> /// <inheritdoc />
public string DriverType => "AbCip"; public string DriverType => "AbCip";
/// <summary> /// <summary>
@@ -244,10 +244,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>Reinitialize the driver by shutting down and reinitializing with new configuration.</summary> /// <inheritdoc />
/// <param name="driverConfigJson">The new driver configuration as JSON.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A task representing the asynchronous reinitialization.</returns>
public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken) public async Task ReinitializeAsync(string driverConfigJson, CancellationToken cancellationToken)
{ {
await ShutdownAsync(cancellationToken).ConfigureAwait(false); await ShutdownAsync(cancellationToken).ConfigureAwait(false);
@@ -305,19 +302,12 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- ISubscribable (polling overlay via shared engine) ---- // ---- ISubscribable (polling overlay via shared engine) ----
/// <summary>Subscribe to value changes for the specified tag references.</summary> /// <inheritdoc />
/// <param name="fullReferences">The tag references to subscribe to.</param>
/// <param name="publishingInterval">The interval at which to publish changes.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A handle representing the subscription.</returns>
public Task<ISubscriptionHandle> SubscribeAsync( public Task<ISubscriptionHandle> SubscribeAsync(
IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) => IReadOnlyList<string> fullReferences, TimeSpan publishingInterval, CancellationToken cancellationToken) =>
Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval)); Task.FromResult(_poll.Subscribe(fullReferences, publishingInterval));
/// <summary>Unsubscribe from value changes using a subscription handle.</summary> /// <inheritdoc />
/// <param name="handle">The subscription handle to unsubscribe.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A completed task.</returns>
public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken) public Task UnsubscribeAsync(ISubscriptionHandle handle, CancellationToken cancellationToken)
{ {
_poll.Unsubscribe(handle); _poll.Unsubscribe(handle);
@@ -349,19 +339,13 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
return _alarmProjection.SubscribeAsync(sourceNodeIds, cancellationToken); return _alarmProjection.SubscribeAsync(sourceNodeIds, cancellationToken);
} }
/// <summary>Unsubscribe from alarm events.</summary> /// <inheritdoc />
/// <param name="handle">The alarm subscription handle.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A completed task.</returns>
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) => public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken) =>
_options.EnableAlarmProjection _options.EnableAlarmProjection
? _alarmProjection.UnsubscribeAsync(handle, cancellationToken) ? _alarmProjection.UnsubscribeAsync(handle, cancellationToken)
: Task.CompletedTask; : Task.CompletedTask;
/// <summary>Acknowledge alarms.</summary> /// <inheritdoc />
/// <param name="acknowledgements">The alarm acknowledgements to process.</param>
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A completed task.</returns>
public Task AcknowledgeAsync( public Task AcknowledgeAsync(
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) => IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken) =>
_options.EnableAlarmProjection _options.EnableAlarmProjection
@@ -370,8 +354,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
// ---- IHostConnectivityProbe ---- // ---- IHostConnectivityProbe ----
/// <summary>Gets the connectivity status of all configured devices.</summary> /// <inheritdoc />
/// <returns>A read-only list of host connectivity statuses.</returns>
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() => public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() =>
[.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))]; [.. _devices.Values.Select(s => new HostConnectivityStatus(s.Options.HostAddress, s.HostState, s.HostStateChangedUtc))];
@@ -873,8 +856,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
} }
} }
/// <summary>Gets the current health status of the driver.</summary> /// <inheritdoc />
/// <returns>The driver health information.</returns>
public DriverHealth GetHealth() => _health; public DriverHealth GetHealth() => _health;
/// <summary> /// <summary>
@@ -885,9 +867,7 @@ public sealed class AbCipDriver : IDriver, IReadable, IWritable, ITagDiscovery,
/// <returns>The memory footprint in bytes.</returns> /// <returns>The memory footprint in bytes.</returns>
public long GetMemoryFootprint() => 0; public long GetMemoryFootprint() => 0;
/// <summary>Flushes optional caches to free memory.</summary> /// <inheritdoc />
/// <param name="cancellationToken">Cancellation token for the operation.</param>
/// <returns>A completed task.</returns>
public Task FlushOptionalCachesAsync(CancellationToken cancellationToken) public Task FlushOptionalCachesAsync(CancellationToken cancellationToken)
{ {
_templateCache.Clear(); _templateCache.Clear();
@@ -48,6 +48,7 @@ public static class AbCipStatusMapper
/// <summary>Map a CIP general-status byte to an OPC UA StatusCode.</summary> /// <summary>Map a CIP general-status byte to an OPC UA StatusCode.</summary>
/// <param name="status">The CIP general-status byte value.</param> /// <param name="status">The CIP general-status byte value.</param>
/// <returns>The corresponding OPC UA StatusCode.</returns>
public static uint MapCipGeneralStatus(byte status) => status switch public static uint MapCipGeneralStatus(byte status) => status switch
{ {
0x00 => Good, 0x00 => Good,
@@ -72,6 +73,7 @@ public static class AbCipStatusMapper
/// operation; every other (negative) member is an error. /// operation; every other (negative) member is an error.
/// </summary> /// </summary>
/// <param name="status">The libplctag status code as an integer.</param> /// <param name="status">The libplctag status code as an integer.</param>
/// <returns>The corresponding OPC UA StatusCode.</returns>
public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status); public static uint MapLibplctagStatus(int status) => MapLibplctagStatus((Status)status);
/// <summary> /// <summary>
@@ -80,6 +82,7 @@ public static class AbCipStatusMapper
/// <see cref="IAbCipTagRuntime.GetStatus"/> seam, which returns the boxed-as-int value. /// <see cref="IAbCipTagRuntime.GetStatus"/> seam, which returns the boxed-as-int value.
/// </summary> /// </summary>
/// <param name="status">The libplctag Status enum value.</param> /// <param name="status">The libplctag Status enum value.</param>
/// <returns>The corresponding OPC UA StatusCode.</returns>
public static uint MapLibplctagStatus(Status status) => status switch public static uint MapLibplctagStatus(Status status) => status switch
{ {
Status.Ok => Good, Status.Ok => Good,
@@ -19,6 +19,7 @@ public static class AbCipSystemTagFilter
/// always preserved case and the system-tag prefixes are uppercase by convention. /// always preserved case and the system-tag prefixes are uppercase by convention.
/// </summary> /// </summary>
/// <param name="tagName">The tag name to check.</param> /// <param name="tagName">The tag name to check.</param>
/// <returns><see langword="true"/> if the tag is a system tag that should be hidden; otherwise <see langword="false"/>.</returns>
public static bool IsSystemTag(string tagName) public static bool IsSystemTag(string tagName)
{ {
if (string.IsNullOrWhiteSpace(tagName)) return true; if (string.IsNullOrWhiteSpace(tagName)) return true;
@@ -24,6 +24,7 @@ public sealed record AbCipTagPath(
int? BitIndex) int? BitIndex)
{ {
/// <summary>Rebuild the canonical Logix tag string.</summary> /// <summary>Rebuild the canonical Logix tag string.</summary>
/// <returns>The canonical Logix tag string suitable for the libplctag <c>name=</c> attribute.</returns>
public string ToLibplctagName() public string ToLibplctagName()
{ {
var buf = new System.Text.StringBuilder(); var buf = new System.Text.StringBuilder();
@@ -23,6 +23,7 @@ public sealed class AbCipTemplateCache
/// </summary> /// </summary>
/// <param name="deviceHostAddress">The device host address and port.</param> /// <param name="deviceHostAddress">The device host address and port.</param>
/// <param name="templateInstanceId">The template instance ID.</param> /// <param name="templateInstanceId">The template instance ID.</param>
/// <returns>The cached <see cref="AbCipUdtShape"/>, or <c>null</c> if not yet populated.</returns>
public AbCipUdtShape? TryGet(string deviceHostAddress, uint templateInstanceId) => public AbCipUdtShape? TryGet(string deviceHostAddress, uint templateInstanceId) =>
_shapes.TryGetValue((deviceHostAddress, templateInstanceId), out var shape) ? shape : null; _shapes.TryGetValue((deviceHostAddress, templateInstanceId), out var shape) ? shape : null;
@@ -30,6 +30,7 @@ public static class AbCipUdtMemberLayout
/// if any member type is unsupported for declaration-only layout. /// if any member type is unsupported for declaration-only layout.
/// </summary> /// </summary>
/// <param name="members">The list of UDT member declarations.</param> /// <param name="members">The list of UDT member declarations.</param>
/// <returns>A dictionary mapping member name to byte offset, or <see langword="null"/> if any member type is unsupported.</returns>
public static IReadOnlyDictionary<string, int>? TryBuild( public static IReadOnlyDictionary<string, int>? TryBuild(
IReadOnlyList<AbCipStructureMember> members) IReadOnlyList<AbCipStructureMember> members)
{ {
@@ -28,6 +28,7 @@ public static class AbCipUdtReadPlanner
/// <param name="requests">The list of tag references to read.</param> /// <param name="requests">The list of tag references to read.</param>
/// <param name="tagsByName">Dictionary mapping tag names to their definitions.</param> /// <param name="tagsByName">Dictionary mapping tag names to their definitions.</param>
/// <param name="enableDeclarationOnlyGrouping">Whether to enable UDT member grouping based on declaration order.</param> /// <param name="enableDeclarationOnlyGrouping">Whether to enable UDT member grouping based on declaration order.</param>
/// <returns>An <see cref="AbCipUdtReadPlan"/> partitioning requests into whole-UDT groups and per-tag fallbacks.</returns>
public static AbCipUdtReadPlan Build( public static AbCipUdtReadPlan Build(
IReadOnlyList<string> requests, IReadOnlyList<string> requests,
IReadOnlyDictionary<string, AbCipTagDefinition> tagsByName, IReadOnlyDictionary<string, AbCipTagDefinition> tagsByName,
@@ -15,6 +15,7 @@ public interface IAbCipTagEnumerator : IDisposable
/// </summary> /// </summary>
/// <param name="deviceParams">Parameters for creating device tags.</param> /// <param name="deviceParams">Parameters for creating device tags.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An async enumerable of discovered tags.</returns>
IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync( IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
AbCipTagCreateParams deviceParams, AbCipTagCreateParams deviceParams,
CancellationToken cancellationToken); CancellationToken cancellationToken);
@@ -26,6 +27,7 @@ public interface IAbCipTagEnumeratorFactory
/// <summary> /// <summary>
/// Creates a new tag enumerator instance. /// Creates a new tag enumerator instance.
/// </summary> /// </summary>
/// <returns>A new <see cref="IAbCipTagEnumerator"/> instance.</returns>
IAbCipTagEnumerator Create(); IAbCipTagEnumerator Create();
} }
@@ -59,6 +61,7 @@ internal sealed class EmptyAbCipTagEnumerator : IAbCipTagEnumerator
/// </summary> /// </summary>
/// <param name="deviceParams">Parameters for creating device tags.</param> /// <param name="deviceParams">Parameters for creating device tags.</param>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>An empty async enumerable of discovered tags.</returns>
public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync( public async IAsyncEnumerable<AbCipDiscoveredTag> EnumerateAsync(
AbCipTagCreateParams deviceParams, AbCipTagCreateParams deviceParams,
[System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken) [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken)
@@ -79,5 +82,6 @@ internal sealed class EmptyAbCipTagEnumeratorFactory : IAbCipTagEnumeratorFactor
/// <summary> /// <summary>
/// Creates a new empty tag enumerator. /// Creates a new empty tag enumerator.
/// </summary> /// </summary>
/// <returns>A new <see cref="EmptyAbCipTagEnumerator"/> instance.</returns>
public IAbCipTagEnumerator Create() => new EmptyAbCipTagEnumerator(); public IAbCipTagEnumerator Create() => new EmptyAbCipTagEnumerator();
} }
@@ -11,20 +11,24 @@ public interface IAbCipTagRuntime : IDisposable
{ {
/// <summary>Create the underlying native tag (equivalent to libplctag's <c>plc_tag_create</c>).</summary> /// <summary>Create the underlying native tag (equivalent to libplctag's <c>plc_tag_create</c>).</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task InitializeAsync(CancellationToken cancellationToken); Task InitializeAsync(CancellationToken cancellationToken);
/// <summary>Issue a read; on completion the local buffer holds the current PLC value.</summary> /// <summary>Issue a read; on completion the local buffer holds the current PLC value.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task ReadAsync(CancellationToken cancellationToken); Task ReadAsync(CancellationToken cancellationToken);
/// <summary>Flush the local buffer to the PLC.</summary> /// <summary>Flush the local buffer to the PLC.</summary>
/// <param name="cancellationToken">Cancellation token.</param> /// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that represents the asynchronous operation.</returns>
Task WriteAsync(CancellationToken cancellationToken); Task WriteAsync(CancellationToken cancellationToken);
/// <summary> /// <summary>
/// Raw libplctag status code — mapped to an OPC UA StatusCode via /// Raw libplctag status code — mapped to an OPC UA StatusCode via
/// <see cref="AbCipStatusMapper.MapLibplctagStatus(int)"/>. Zero on success, negative on error. /// <see cref="AbCipStatusMapper.MapLibplctagStatus(int)"/>. Zero on success, negative on error.
/// </summary> /// </summary>
/// <returns>The raw libplctag status integer; zero on success, negative on error.</returns>
int GetStatus(); int GetStatus();
/// <summary> /// <summary>
@@ -34,6 +38,7 @@ public interface IAbCipTagRuntime : IDisposable
/// </summary> /// </summary>
/// <param name="type">CIP data type to decode.</param> /// <param name="type">CIP data type to decode.</param>
/// <param name="bitIndex">Bit index for BOOL-within-DINT extraction, or null.</param> /// <param name="bitIndex">Bit index for BOOL-within-DINT extraction, or null.</param>
/// <returns>The decoded .NET value, or <c>null</c> if the buffer cannot be decoded for the given type.</returns>
object? DecodeValue(AbCipDataType type, int? bitIndex); object? DecodeValue(AbCipDataType type, int? bitIndex);
/// <summary> /// <summary>
@@ -48,6 +53,7 @@ public interface IAbCipTagRuntime : IDisposable
/// <param name="type">CIP data type to decode.</param> /// <param name="type">CIP data type to decode.</param>
/// <param name="offset">Byte offset in the buffer.</param> /// <param name="offset">Byte offset in the buffer.</param>
/// <param name="bitIndex">Bit index for BOOL-within-DINT extraction, or null.</param> /// <param name="bitIndex">Bit index for BOOL-within-DINT extraction, or null.</param>
/// <returns>The decoded .NET value at the specified offset, or <c>null</c> if the offset is unsupported.</returns>
object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex); object? DecodeValueAt(AbCipDataType type, int offset, int? bitIndex);
/// <summary> /// <summary>
@@ -68,6 +74,7 @@ public interface IAbCipTagFactory
{ {
/// <summary>Creates a tag runtime handle from the specified creation parameters.</summary> /// <summary>Creates a tag runtime handle from the specified creation parameters.</summary>
/// <param name="createParams">Parameters needed to create the tag runtime.</param> /// <param name="createParams">Parameters needed to create the tag runtime.</param>
/// <returns>A new <see cref="IAbCipTagRuntime"/> instance for the specified tag.</returns>
IAbCipTagRuntime Create(AbCipTagCreateParams createParams); IAbCipTagRuntime Create(AbCipTagCreateParams createParams);
} }

Some files were not shown because too many files have changed in this diff Show More