docs(xmldoc): fill missing XML docs + strip tracking-ID comments across src
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
Adds <summary>/<param>/<returns>/<inheritdoc> where missing and removes project bookkeeping IDs (task/tracking refs) from shipped code comments, so the docs read cleanly and CommentChecker is quiet except for known false positives (PLC/protocol terms, event/IEqualityComparer inheritdoc). Doc/comment-only; no logic changed; solution builds clean.
This commit is contained in:
@@ -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 task that represents the asynchronous operation.</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>The connection settings built from the current command options.</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>The connected client service along with its connection info.</returns>
|
||||||
protected async Task<(IOpcUaClientService Service, ConnectionInfo Info)> CreateServiceAndConnectAsync(
|
protected async Task<(IOpcUaClientService Service, ConnectionInfo Info)> CreateServiceAndConnectAsync(
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,10 +36,7 @@ public class AcknowledgeCommand : CommandBase
|
|||||||
[CommandOption("comment", 'c', Description = "Operator comment for the acknowledgment")]
|
[CommandOption("comment", 'c', Description = "Operator comment for the acknowledgment")]
|
||||||
public string Comment { get; init; } = string.Empty;
|
public string Comment { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Connects to the server and acknowledges the specified alarm condition.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
ConfigureLogging();
|
ConfigureLogging();
|
||||||
|
|||||||
@@ -38,10 +38,6 @@ public class AlarmsCommand : CommandBase
|
|||||||
[CommandOption("refresh", Description = "Request a ConditionRefresh after subscribing")]
|
[CommandOption("refresh", Description = "Request a ConditionRefresh after subscribing")]
|
||||||
public bool Refresh { get; init; }
|
public bool Refresh { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connects to the server, subscribes to alarm events, and streams operator-facing alarm state changes to the console.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -36,10 +36,7 @@ public class ConfirmCommand : CommandBase
|
|||||||
[CommandOption("comment", 'c', Description = "Operator comment for the confirmation")]
|
[CommandOption("comment", 'c', Description = "Operator comment for the confirmation")]
|
||||||
public string Comment { get; init; } = string.Empty;
|
public string Comment { get; init; } = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Connects to the server and confirms the specified acknowledged alarm condition.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
ConfigureLogging();
|
ConfigureLogging();
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ public class DisableCommand : CommandBase
|
|||||||
[CommandOption("node", 'n', Description = "Condition node ID of the alarm to disable", IsRequired = true)]
|
[CommandOption("node", 'n', Description = "Condition node ID of the alarm to disable", IsRequired = true)]
|
||||||
public string NodeId { get; init; } = default!;
|
public string NodeId { get; init; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Connects to the server and disables the specified alarm condition.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
ConfigureLogging();
|
ConfigureLogging();
|
||||||
|
|||||||
@@ -24,10 +24,7 @@ public class EnableCommand : CommandBase
|
|||||||
[CommandOption("node", 'n', Description = "Condition node ID of the alarm to enable", IsRequired = true)]
|
[CommandOption("node", 'n', Description = "Condition node ID of the alarm to enable", IsRequired = true)]
|
||||||
public string NodeId { get; init; } = default!;
|
public string NodeId { get; init; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Connects to the server and enables the specified alarm condition.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
ConfigureLogging();
|
ConfigureLogging();
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ public class RedundancyCommand : CommandBase
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Connects to the server and prints redundancy mode, service level, and partner-server identity data.</summary>
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -38,10 +38,7 @@ public class ShelveCommand : CommandBase
|
|||||||
[CommandOption("duration", 'd', Description = "Shelving duration in seconds (must be > 0; in seconds, converted to milliseconds for the OPC UA call; required for --kind Timed)")]
|
[CommandOption("duration", 'd', Description = "Shelving duration in seconds (must be > 0; in seconds, converted to milliseconds for the OPC UA call; required for --kind Timed)")]
|
||||||
public double DurationSeconds { get; init; }
|
public double DurationSeconds { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Connects to the server and shelves or unshelves the specified alarm condition.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
ConfigureLogging();
|
ConfigureLogging();
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ public class WriteCommand : CommandBase
|
|||||||
[CommandOption("value", 'v', Description = "Value to write", IsRequired = true)]
|
[CommandOption("value", 'v', Description = "Value to write", IsRequired = true)]
|
||||||
public string Value { get; init; } = default!;
|
public string Value { get; init; } = default!;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Connects to the server, converts the supplied value to the node's current data type, and issues the write.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override async ValueTask ExecuteAsync(IConsole console)
|
public override async ValueTask ExecuteAsync(IConsole console)
|
||||||
{
|
{
|
||||||
|
|||||||
+1
-3
@@ -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 endpoint, with its URL host rewritten to match <paramref name="endpointUrl"/> if needed.</returns>
|
||||||
public static EndpointDescription SelectBest(
|
public static EndpointDescription SelectBest(
|
||||||
IEnumerable<EndpointDescription> allEndpoints,
|
IEnumerable<EndpointDescription> allEndpoints,
|
||||||
string endpointUrl,
|
string endpointUrl,
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ internal sealed class DefaultSessionAdapter : ISessionAdapter
|
|||||||
byte[] continuationPoint, CancellationToken ct)
|
byte[] continuationPoint, CancellationToken ct)
|
||||||
{
|
{
|
||||||
// Pass the caller's token so a cancelled browse does not block on the continuation
|
// Pass the caller's token so a cancelled browse does not block on the continuation
|
||||||
// round-trip (Client.Shared-015).
|
// round-trip.
|
||||||
var (_, nextCp, nextRefs) = await _session.BrowseNextAsync(null, false, continuationPoint, ct);
|
var (_, nextCp, nextRefs) = await _session.BrowseNextAsync(null, false, continuationPoint, ct);
|
||||||
return (nextCp, nextRefs ?? []);
|
return (nextCp, nextRefs ?? []);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,14 +11,7 @@ internal sealed class DefaultSessionFactory : ISessionFactory
|
|||||||
{
|
{
|
||||||
private static readonly ILogger Logger = Log.ForContext<DefaultSessionFactory>();
|
private static readonly ILogger Logger = Log.ForContext<DefaultSessionFactory>();
|
||||||
|
|
||||||
/// <summary>Creates a new OPC UA session.</summary>
|
/// <inheritdoc />
|
||||||
/// <param name="config">The OPC UA application configuration.</param>
|
|
||||||
/// <param name="endpoint">The endpoint description to connect to.</param>
|
|
||||||
/// <param name="sessionName">The name for the session.</param>
|
|
||||||
/// <param name="sessionTimeoutMs">The session timeout in milliseconds.</param>
|
|
||||||
/// <param name="identity">The user identity for the session.</param>
|
|
||||||
/// <param name="ct">The cancellation token.</param>
|
|
||||||
/// <returns>An adapter wrapping the created session.</returns>
|
|
||||||
public async Task<ISessionAdapter> CreateSessionAsync(
|
public async Task<ISessionAdapter> CreateSessionAsync(
|
||||||
ApplicationConfiguration config,
|
ApplicationConfiguration config,
|
||||||
EndpointDescription endpoint,
|
EndpointDescription endpoint,
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ internal sealed class DefaultSubscriptionAdapter : ISubscriptionAdapter
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Forward the caller's token so the delete can be cancelled (Client.Shared-014).
|
// Forward the caller's token so the delete can be cancelled.
|
||||||
await _subscription.DeleteAsync(true, ct);
|
await _subscription.DeleteAsync(true, ct);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|||||||
+1
@@ -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>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, with its URL hostname rewritten to match <paramref name="endpointUrl"/> when they differ.</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>The current data value read from the server.</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>The status code returned by the server 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 continuation point (or <c>null</c> when complete) and the references found.</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>The next continuation point (or <c>null</c> when exhausted) and the additional 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><c>true</c> when the node has at least one forward hierarchical child reference; otherwise <c>false</c>.</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>The raw historical data values for the requested window.</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>The processed/aggregate data values for the requested window.</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>The newly created subscription adapter.</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>The output arguments returned by the method call.</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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Resolves the canonical under-LocalAppData folder for the shared OPC UA client's PKI
|
/// Resolves the canonical under-LocalAppData folder for the shared OPC UA client's PKI
|
||||||
/// store + persisted settings. Renamed from <c>LmxOpcUaClient</c> to <c>OtOpcUaClient</c>
|
/// store + persisted settings. Renamed from <c>LmxOpcUaClient</c> to <c>OtOpcUaClient</c>;
|
||||||
/// in task #208; a one-shot migration shim moves a pre-rename folder in place on first
|
/// a one-shot migration shim moves a pre-rename folder in place on first
|
||||||
/// resolution so existing developer boxes keep their trusted server certs + saved
|
/// resolution so existing developer boxes keep their trusted server certs + saved
|
||||||
/// connection settings on upgrade.
|
/// connection settings on upgrade.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -14,10 +14,10 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared;
|
|||||||
/// </remarks>
|
/// </remarks>
|
||||||
public static class ClientStoragePaths
|
public static class ClientStoragePaths
|
||||||
{
|
{
|
||||||
/// <summary>Canonical client folder name. Post-#208.</summary>
|
/// <summary>Canonical client folder name.</summary>
|
||||||
public const string CanonicalFolderName = "OtOpcUaClient";
|
public const string CanonicalFolderName = "OtOpcUaClient";
|
||||||
|
|
||||||
/// <summary>Pre-#208 folder name. Used only by the migration shim.</summary>
|
/// <summary>Legacy folder name, used only by the migration shim.</summary>
|
||||||
public const string LegacyFolderName = "LmxOpcUaClient";
|
public const string LegacyFolderName = "LmxOpcUaClient";
|
||||||
|
|
||||||
private static readonly Lock _migrationLock = new();
|
private static readonly Lock _migrationLock = new();
|
||||||
@@ -28,6 +28,7 @@ public static class ClientStoragePaths
|
|||||||
/// one-shot legacy-folder migration before returning so callers that depend on this
|
/// 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 canonical top-level folder.</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 client's PKI store folder.</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><c>true</c> when a legacy folder existed and was moved to canonical; otherwise <c>false</c>.</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>The connection metadata for the newly established session.</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 data value of the node.</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 status code returned by the server for the write.</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 child nodes discovered under <paramref name="parentNodeId"/>.</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>
|
||||||
@@ -169,6 +179,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 samples in the requested range, up to <paramref name="maxValues"/>.</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);
|
||||||
|
|
||||||
@@ -181,6 +192,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 aggregated historical values for each processing interval in the requested range.</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);
|
||||||
|
|
||||||
@@ -188,6 +200,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>The current redundancy status of the connected server.</returns>
|
||||||
Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default);
|
Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
|||||||
// Serialises SubscribeAlarmsAsync / UnsubscribeAlarmsAsync so concurrent callers
|
// Serialises SubscribeAlarmsAsync / UnsubscribeAlarmsAsync so concurrent callers
|
||||||
// cannot both pass the _alarmSubscription == null check and create duplicate event
|
// cannot both pass the _alarmSubscription == null check and create duplicate event
|
||||||
// subscriptions. Capacity 1 = at most one waiter; async-friendly (no thread-block).
|
// subscriptions. Capacity 1 = at most one waiter; async-friendly (no thread-block).
|
||||||
// (Client.Shared-013)
|
|
||||||
private readonly SemaphoreSlim _alarmSubscribeSemaphore = new(1, 1);
|
private readonly SemaphoreSlim _alarmSubscribeSemaphore = new(1, 1);
|
||||||
|
|
||||||
// Track active data subscriptions for replay after failover
|
// Track active data subscriptions for replay after failover
|
||||||
@@ -137,7 +136,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
|||||||
var endpointUrl = _session?.EndpointUrl ?? _settings?.EndpointUrl ?? string.Empty;
|
var endpointUrl = _session?.EndpointUrl ?? _settings?.EndpointUrl ?? string.Empty;
|
||||||
|
|
||||||
// Snapshot the subscription adapter references under the lock so a concurrent
|
// Snapshot the subscription adapter references under the lock so a concurrent
|
||||||
// RunFailoverAsync that nulls _dataSubscription / _alarmSubscription (Client.Shared-012)
|
// RunFailoverAsync that nulls _dataSubscription / _alarmSubscription
|
||||||
// cannot invalidate the references between the null-check and the DeleteAsync call.
|
// cannot invalidate the references between the null-check and the DeleteAsync call.
|
||||||
ISubscriptionAdapter? dataSubscription;
|
ISubscriptionAdapter? dataSubscription;
|
||||||
ISubscriptionAdapter? alarmSubscription;
|
ISubscriptionAdapter? alarmSubscription;
|
||||||
@@ -303,7 +302,7 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
|||||||
ThrowIfDisposed();
|
ThrowIfDisposed();
|
||||||
ThrowIfNotConnected();
|
ThrowIfNotConnected();
|
||||||
|
|
||||||
// Serialise check-and-create under the semaphore (Client.Shared-013): concurrent
|
// Serialise check-and-create under the semaphore: concurrent
|
||||||
// callers cannot both pass the null-check and create duplicate alarm subscriptions.
|
// callers cannot both pass the null-check and create duplicate alarm subscriptions.
|
||||||
// The semaphore is async-friendly; the subscription I/O runs inside the critical section.
|
// The semaphore is async-friendly; the subscription I/O runs inside the critical section.
|
||||||
await _alarmSubscribeSemaphore.WaitAsync(ct);
|
await _alarmSubscribeSemaphore.WaitAsync(ct);
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.Shared;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class OpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
public sealed class OpcUaClientServiceFactory : IOpcUaClientServiceFactory
|
||||||
{
|
{
|
||||||
/// <summary>Creates a new OPC UA client service instance with production adapters.</summary>
|
/// <inheritdoc />
|
||||||
/// <returns>A new OpcUaClientService instance.</returns>
|
|
||||||
public IOpcUaClientService Create()
|
public IOpcUaClientService Create()
|
||||||
{
|
{
|
||||||
return new OpcUaClientService();
|
return new OpcUaClientService();
|
||||||
|
|||||||
@@ -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 loaded user settings.</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>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class JsonSettingsService : ISettingsService
|
public sealed class JsonSettingsService : ISettingsService
|
||||||
{
|
{
|
||||||
// ClientStoragePaths.GetRoot runs the one-shot legacy-folder migration so pre-#208
|
// ClientStoragePaths.GetRoot runs the one-shot legacy-folder migration so pre-rename
|
||||||
// developer boxes pick up their existing settings.json on first launch post-rename.
|
// developer boxes pick up their existing settings.json on first launch post-rename.
|
||||||
private static readonly string SettingsDir = ClientStoragePaths.GetRoot();
|
private static readonly string SettingsDir = ClientStoragePaths.GetRoot();
|
||||||
|
|
||||||
@@ -19,8 +19,8 @@ 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>
|
// Returns a new default instance when the settings file is missing or fails to load/parse.
|
||||||
public UserSettings Load()
|
public UserSettings Load()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -37,8 +37,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();
|
||||||
|
|||||||
@@ -256,6 +256,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, or null if not 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 node IDs of 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 indicating whether the write succeeded and a human-readable status message.</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
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ public partial class ShelveAlarmWindow : Window
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Creates the shelve dialog for an alarm.</summary>
|
/// <summary>Creates the shelve dialog for an alarm.</summary>
|
||||||
|
/// <param name="alarmsVm">The view model used to issue the shelve/unshelve command.</param>
|
||||||
|
/// <param name="alarm">The alarm event being shelved.</param>
|
||||||
public ShelveAlarmWindow(AlarmsViewModel alarmsVm, AlarmEventViewModel alarm)
|
public ShelveAlarmWindow(AlarmsViewModel alarmsVm, AlarmEventViewModel alarm)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
|||||||
@@ -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>The distinct, lower-cased, validated role names.</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>();
|
||||||
|
|||||||
@@ -20,6 +20,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 service collection, 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>()
|
||||||
@@ -47,6 +48,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 builder, 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>The 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 node whose direct children are returned.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The direct children of the node.</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 node whose attributes are returned.</param>
|
||||||
|
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The attributes of 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>The opened browse session.</returns>
|
||||||
Task<IBrowseSession> OpenAsync(string configJson, CancellationToken cancellationToken);
|
Task<IBrowseSession> OpenAsync(string configJson, CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,5 +53,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>The reply of type <typeparamref name="T"/> received from the admin singleton.</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>The diagnostics snapshot for the requested 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 activity, or <c>null</c> 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 (AddressSpace plan → apply).</summary>
|
/// <summary>Span wrapping a full OPC UA address-space rebuild (AddressSpace plan → apply).</summary>
|
||||||
|
/// <returns>The started activity, or <c>null</c> 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,85 +22,49 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
|
|||||||
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 a full alarm-condition state through the inner sink.</summary>
|
/// <inheritdoc />
|
||||||
/// <param name="alarmNodeId">The node ID of the alarm condition.</param>
|
|
||||||
/// <param name="state">The full condition state to project onto the node.</param>
|
|
||||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
|
||||||
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
public void WriteAlarmCondition(string alarmNodeId, AlarmConditionSnapshot state, DateTime sourceTimestampUtc)
|
||||||
=> _inner.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc);
|
=> _inner.WriteAlarmCondition(alarmNodeId, state, sourceTimestampUtc);
|
||||||
|
|
||||||
/// <summary>Materialises a real Part 9 alarm-condition node through the inner sink.</summary>
|
/// <inheritdoc />
|
||||||
/// <param name="alarmNodeId">The alarm node ID (== ScriptedAlarmId).</param>
|
|
||||||
/// <param name="equipmentNodeId">The equipment folder node ID the condition parents under.</param>
|
|
||||||
/// <param name="displayName">The human-readable condition name.</param>
|
|
||||||
/// <param name="alarmType">The domain alarm type.</param>
|
|
||||||
/// <param name="severity">The domain severity.</param>
|
|
||||||
/// <param name="isNative">True for a driver-fed (native) equipment-tag alarm; false (default) for a scripted alarm.</param>
|
|
||||||
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
public void MaterialiseAlarmCondition(string alarmNodeId, string equipmentNodeId, string displayName, string alarmType, int severity, bool isNative = false)
|
||||||
=> _inner.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative);
|
=> _inner.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity, isNative);
|
||||||
|
|
||||||
/// <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>
|
|
||||||
/// <param name="writable">When true the node is created read/write; otherwise read-only.</param>
|
|
||||||
/// <param name="historianTagname">null ⇒ not historized; non-null ⇒ create Historizing with the
|
|
||||||
/// HistoryRead access bit and register the historian tagname.</param>
|
|
||||||
/// <param name="isArray">When true the node is created as a 1-D array; when false (default) scalar.</param>
|
|
||||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is true.</param>
|
|
||||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType, bool writable, string? historianTagname = null, bool isArray = false, uint? arrayLength = null)
|
||||||
=> _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength);
|
=> _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType, writable, historianTagname, isArray, arrayLength);
|
||||||
|
|
||||||
/// <summary>Rebuilds the address space through the inner sink.</summary>
|
/// <inheritdoc />
|
||||||
public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
|
public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
|
||||||
|
|
||||||
/// <summary>Announces a runtime NodeAdded model-change (discovered-node injection) through the inner sink.</summary>
|
/// <inheritdoc />
|
||||||
/// <param name="affectedNodeId">The node under which discovered nodes were added.</param>
|
|
||||||
public void RaiseNodesAddedModelChange(string affectedNodeId) => _inner.RaiseNodesAddedModelChange(affectedNodeId);
|
public void RaiseNodesAddedModelChange(string affectedNodeId) => _inner.RaiseNodesAddedModelChange(affectedNodeId);
|
||||||
|
|
||||||
/// <summary>Forwards an in-place tag-attribute update (F10b) to the inner sink when it supports the
|
/// <inheritdoc />
|
||||||
/// surgical capability. Returns false otherwise — before the real <c>SdkAddressSpaceSink</c> is
|
// Forwards to the inner sink when it supports the surgical capability; returns false otherwise —
|
||||||
/// swapped in (inner is still the null sink), or any inner sink that isn't surgical — so the caller
|
// before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any inner
|
||||||
/// (AddressSpaceApplier) falls back to a full rebuild. Without this forward the surgical optimization is
|
// sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild.
|
||||||
/// inert on every driver-role host, because actors inject THIS wrapper, not the inner sink. ALL six args
|
// Without this forward the surgical optimization is inert on every driver-role host, because
|
||||||
/// (including the FB-7 DataType/array-shape ones) MUST be forwarded — a partial forward silently drops the
|
// actors inject THIS wrapper, not the inner sink. ALL six args (including the DataType/array-shape
|
||||||
/// shape update on every driver-role host.</summary>
|
// ones) MUST be forwarded — a partial forward silently drops the shape update on every driver-role host.
|
||||||
/// <param name="variableNodeId">The node ID of the variable to update in place.</param>
|
|
||||||
/// <param name="writable">Whether the node should be read/write.</param>
|
|
||||||
/// <param name="historianTagname">null ⇒ not historized; non-null ⇒ Historizing + historian binding.</param>
|
|
||||||
/// <param name="dataType">The OPC UA built-in data type name to apply in place.</param>
|
|
||||||
/// <param name="isArray">When true the node becomes a 1-D array; when false scalar.</param>
|
|
||||||
/// <param name="arrayLength">The declared length of the 1-D array when <paramref name="isArray"/> is true.</param>
|
|
||||||
/// <returns>True when the inner sink applied the update; false when it lacks the capability or the node is missing.</returns>
|
|
||||||
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
public bool UpdateTagAttributes(string variableNodeId, bool writable, string? historianTagname, string dataType, bool isArray, uint? arrayLength)
|
||||||
=> _inner is ISurgicalAddressSpaceSink surgical
|
=> _inner is ISurgicalAddressSpaceSink surgical
|
||||||
&& surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength);
|
&& surgical.UpdateTagAttributes(variableNodeId, writable, historianTagname, dataType, isArray, arrayLength);
|
||||||
|
|
||||||
/// <summary>Forwards an in-place folder-display-name update (OpcUaServer-001 — UNS Area / Line
|
/// <inheritdoc />
|
||||||
/// rename) to the inner sink when it supports the surgical capability. Returns false otherwise —
|
// Forwards to the inner sink when it supports the surgical capability; returns false otherwise —
|
||||||
/// before the real <c>SdkAddressSpaceSink</c> is swapped in (inner is still the null sink), or any
|
// before the real SdkAddressSpaceSink is swapped in (inner is still the null sink), or any inner
|
||||||
/// inner sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild.
|
// sink that isn't surgical — so the caller (AddressSpaceApplier) falls back to a full rebuild.
|
||||||
/// Without this forward the rename-refresh optimization is inert on every driver-role host, because
|
// Without this forward the rename-refresh optimization is inert on every driver-role host, because
|
||||||
/// actors inject THIS wrapper, not the inner sink.</summary>
|
// actors inject THIS wrapper, not the inner sink.
|
||||||
/// <param name="folderNodeId">The folder node id whose display name to update in place.</param>
|
|
||||||
/// <param name="displayName">The new display name to apply.</param>
|
|
||||||
/// <returns>True when the inner sink applied the update; false when it lacks the capability or the folder is missing.</returns>
|
|
||||||
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
public bool UpdateFolderDisplayName(string folderNodeId, string displayName)
|
||||||
=> _inner is ISurgicalAddressSpaceSink surgical
|
=> _inner is ISurgicalAddressSpaceSink surgical
|
||||||
&& surgical.UpdateFolderDisplayName(folderNodeId, displayName);
|
&& surgical.UpdateFolderDisplayName(folderNodeId, displayName);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ public sealed class NullServiceLevelPublisher : IServiceLevelPublisher
|
|||||||
/// <summary>Gets the last published service level value.</summary>
|
/// <summary>Gets the last published service level value.</summary>
|
||||||
public byte LastPublished { get; private set; }
|
public byte LastPublished { get; private set; }
|
||||||
|
|
||||||
/// <summary>Records the service level value without publishing.</summary>
|
/// <inheritdoc />
|
||||||
/// <param name="serviceLevel">The service level value (0-255).</param>
|
|
||||||
public void Publish(byte serviceLevel) => LastPublished = serviceLevel;
|
public void Publish(byte serviceLevel) => LastPublished = 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, randomly generated <see cref="CorrelationId"/>.</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>The parsed <see cref="CorrelationId"/>.</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><c>true</c> if <paramref name="s"/> was successfully parsed.</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; }
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public static class EquipmentScriptPaths
|
|||||||
|
|
||||||
/// <summary>True when the source uses the <c>{{equip}}</c> token anywhere.</summary>
|
/// <summary>True when the source uses the <c>{{equip}}</c> token anywhere.</summary>
|
||||||
/// <param name="source">The script source to scan.</param>
|
/// <param name="source">The script source to scan.</param>
|
||||||
|
/// <returns>True if <paramref name="source"/> contains the <c>{{equip}}</c> token; otherwise false.</returns>
|
||||||
public static bool ContainsEquipToken(string? source) =>
|
public static bool ContainsEquipToken(string? source) =>
|
||||||
!string.IsNullOrEmpty(source) && source.Contains(EquipToken, StringComparison.Ordinal);
|
!string.IsNullOrEmpty(source) && source.Contains(EquipToken, StringComparison.Ordinal);
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public sealed class ClusterNode
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// OPC UA <c>ApplicationUri</c> — MUST be unique per node per OPC UA spec. Clients pin trust here.
|
/// OPC UA <c>ApplicationUri</c> — MUST be unique per node per OPC UA spec. Clients pin trust here.
|
||||||
/// Fleet-wide unique index enforces no two nodes share a value (decision #86).
|
/// Fleet-wide unique index enforces no two nodes share a value.
|
||||||
/// Stored explicitly, NOT derived from <see cref="Host"/> at runtime — silent rewrite on
|
/// Stored explicitly, NOT derived from <see cref="Host"/> at runtime — silent rewrite on
|
||||||
/// hostname change would break all client trust.
|
/// hostname change would break all client trust.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -31,7 +31,7 @@ public sealed class ClusterNode
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-node override JSON keyed by DriverInstanceId, merged onto cluster-level DriverConfig
|
/// Per-node override JSON keyed by DriverInstanceId, merged onto cluster-level DriverConfig
|
||||||
/// at apply time. Minimal by intent (decision #81). Nullable when no overrides exist.
|
/// at apply time. Minimal by intent. Nullable when no overrides exist.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? DriverConfigOverridesJson { get; set; }
|
public string? DriverConfigOverridesJson { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authenticates a <see cref="ClusterNode"/> to the central config DB.
|
/// Authenticates a <see cref="ClusterNode"/> to the central config DB.
|
||||||
/// Per decision #83 — credentials bind to NodeId, not ClusterId.
|
/// Credentials bind to NodeId, not ClusterId.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ClusterNodeCredential
|
public sealed class ClusterNodeCredential
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Closes the data-layer piece of LMX follow-up #7 (per-AppEngine Admin dashboard
|
/// Supports the per-AppEngine Admin dashboard drill-down. The publisher hosted
|
||||||
/// drill-down). The publisher hosted service on the Server side subscribes to every
|
/// service on the Server side subscribes to every
|
||||||
/// registered driver's <c>OnHostStatusChanged</c> and upserts rows on transitions +
|
/// registered driver's <c>OnHostStatusChanged</c> and upserts rows on transitions +
|
||||||
/// periodic liveness heartbeats. <see cref="LastSeenUtc"/> advances on every
|
/// periodic liveness heartbeats. <see cref="LastSeenUtc"/> advances on every
|
||||||
/// heartbeat so the Admin UI can flag stale rows from a crashed Server.
|
/// heartbeat so the Admin UI can flag stale rows from a crashed Server.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public sealed class DriverInstance
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Logical FK to <see cref="Namespace.NamespaceId"/>. Same-cluster binding enforced by
|
/// Logical FK to <see cref="Namespace.NamespaceId"/>. Same-cluster binding enforced by
|
||||||
/// <c>sp_ValidateDraft</c> per decision #122: Namespace.ClusterId must equal DriverInstance.ClusterId.
|
/// <c>sp_ValidateDraft</c>: Namespace.ClusterId must equal DriverInstance.ClusterId.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string NamespaceId { get; set; }
|
public required string NamespaceId { get; set; }
|
||||||
|
|
||||||
@@ -27,12 +27,12 @@ public sealed class DriverInstance
|
|||||||
/// <summary>Gets or sets a value indicating whether this driver instance is enabled.</summary>
|
/// <summary>Gets or sets a value indicating whether this driver instance is enabled.</summary>
|
||||||
public bool Enabled { get; set; } = true;
|
public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
/// <summary>Schemaless per-driver-type JSON config. Validated against registered JSON schema at draft-publish time (decision #91).</summary>
|
/// <summary>Schemaless per-driver-type JSON config. Validated against registered JSON schema at draft-publish time.</summary>
|
||||||
public required string DriverConfig { get; set; }
|
public required string DriverConfig { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Optional per-instance overrides for the Phase 6.1 shared Polly resilience pipeline.
|
/// Optional per-instance overrides for the Phase 6.1 shared Polly resilience pipeline.
|
||||||
/// Null = use the driver's tier defaults (decision #143). When populated, expected shape:
|
/// Null = use the driver's tier defaults. When populated, expected shape:
|
||||||
/// <code>
|
/// <code>
|
||||||
/// {
|
/// {
|
||||||
/// "bulkheadMaxConcurrent": 16,
|
/// "bulkheadMaxConcurrent": 16,
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// UNS level-5 entity. Only for drivers in Equipment-kind namespaces.
|
/// UNS level-5 entity. Only for drivers in Equipment-kind namespaces.
|
||||||
/// Per decisions #109 (first-class), #116 (5-identifier model), #125 (system-generated EquipmentId),
|
|
||||||
/// #138–139 (OPC 40010 Identification fields as first-class columns).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Equipment
|
public sealed class Equipment
|
||||||
{
|
{
|
||||||
@@ -12,7 +10,7 @@ public sealed class Equipment
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// System-generated stable internal logical ID. Format: <c>'EQ-' + first 12 hex chars of EquipmentUuid</c>.
|
/// System-generated stable internal logical ID. Format: <c>'EQ-' + first 12 hex chars of EquipmentUuid</c>.
|
||||||
/// NEVER operator-supplied, NEVER in CSV imports, NEVER editable in Admin UI (decision #125).
|
/// NEVER operator-supplied, NEVER in CSV imports, NEVER editable in Admin UI.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public required string EquipmentId { get; set; }
|
public required string EquipmentId { get; set; }
|
||||||
|
|
||||||
@@ -34,7 +32,7 @@ public sealed class Equipment
|
|||||||
/// <summary>UNS level 5 segment, matches <c>^[a-z0-9-]{1,32}$</c>.</summary>
|
/// <summary>UNS level 5 segment, matches <c>^[a-z0-9-]{1,32}$</c>.</summary>
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
// Operator-facing / external-system identifiers (decision #116)
|
// Operator-facing / external-system identifiers
|
||||||
|
|
||||||
/// <summary>Operator colloquial id (e.g. "machine_001"). Unique within cluster. Required.</summary>
|
/// <summary>Operator colloquial id (e.g. "machine_001"). Unique within cluster. Required.</summary>
|
||||||
public required string MachineCode { get; set; }
|
public required string MachineCode { get; set; }
|
||||||
@@ -45,7 +43,7 @@ public sealed class Equipment
|
|||||||
/// <summary>SAP PM equipment id. Unique fleet-wide via <see cref="ExternalIdReservation"/>.</summary>
|
/// <summary>SAP PM equipment id. Unique fleet-wide via <see cref="ExternalIdReservation"/>.</summary>
|
||||||
public string? SAPID { get; set; }
|
public string? SAPID { get; set; }
|
||||||
|
|
||||||
// OPC UA Companion Spec OPC 40010 Machinery Identification fields (decision #139).
|
// OPC UA Companion Spec OPC 40010 Machinery Identification fields.
|
||||||
// All nullable so equipment can be added before identity is fully captured.
|
// All nullable so equipment can be added before identity is fully captured.
|
||||||
/// <summary>Gets or sets the manufacturer name for this equipment.</summary>
|
/// <summary>Gets or sets the manufacturer name for this equipment.</summary>
|
||||||
public string? Manufacturer { get; set; }
|
public string? Manufacturer { get; set; }
|
||||||
@@ -66,7 +64,7 @@ public sealed class Equipment
|
|||||||
/// <summary>Gets or sets the device manual URI for this equipment.</summary>
|
/// <summary>Gets or sets the device manual URI for this equipment.</summary>
|
||||||
public string? DeviceManualUri { get; set; }
|
public string? DeviceManualUri { get; set; }
|
||||||
|
|
||||||
/// <summary>Nullable hook for future schemas-repo template ID (decision #112).</summary>
|
/// <summary>Nullable hook for future schemas-repo template ID.</summary>
|
||||||
public string? EquipmentClassRef { get; set; }
|
public string? EquipmentClassRef { get; set; }
|
||||||
|
|
||||||
/// <summary>Gets or sets whether this equipment is enabled.</summary>
|
/// <summary>Gets or sets whether this equipment is enabled.</summary>
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ public sealed class EquipmentImportBatch
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One staged row under an <see cref="EquipmentImportBatch"/>. Mirrors the decision #117
|
/// One staged row under an <see cref="EquipmentImportBatch"/>. Mirrors the required
|
||||||
/// + decision #139 columns from the CSV importer's output + an
|
/// + optional columns from the CSV importer's output + an
|
||||||
/// <see cref="IsAccepted"/> flag + a <see cref="RejectReason"/> string the preview modal
|
/// <see cref="IsAccepted"/> flag + a <see cref="RejectReason"/> string the preview modal
|
||||||
/// renders.
|
/// renders.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -68,7 +68,6 @@ public sealed class EquipmentImportRow
|
|||||||
/// <summary>Gets or sets the reason this row was rejected, if applicable.</summary>
|
/// <summary>Gets or sets the reason this row was rejected, if applicable.</summary>
|
||||||
public string? RejectReason { get; set; }
|
public string? RejectReason { get; set; }
|
||||||
|
|
||||||
// Required (decision #117)
|
|
||||||
/// <summary>Gets or sets the Z tag identifier.</summary>
|
/// <summary>Gets or sets the Z tag identifier.</summary>
|
||||||
public required string ZTag { get; set; }
|
public required string ZTag { get; set; }
|
||||||
|
|
||||||
@@ -93,7 +92,6 @@ public sealed class EquipmentImportRow
|
|||||||
/// <summary>Gets or sets the UNS line name.</summary>
|
/// <summary>Gets or sets the UNS line name.</summary>
|
||||||
public required string UnsLineName { get; set; }
|
public required string UnsLineName { get; set; }
|
||||||
|
|
||||||
// Optional (decision #139 — OPC 40010 Identification)
|
|
||||||
/// <summary>Gets or sets the manufacturer name.</summary>
|
/// <summary>Gets or sets the manufacturer name.</summary>
|
||||||
public string? Manufacturer { get; set; }
|
public string? Manufacturer { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fleet-wide rollback-safe reservation of ZTag and SAPID. Per decision #124 — NOT generation-versioned.
|
/// Fleet-wide rollback-safe reservation of ZTag and SAPID. NOT generation-versioned.
|
||||||
/// Exists outside generation flow specifically because old generations and disabled equipment can
|
/// Exists outside generation flow specifically because old generations and disabled equipment can
|
||||||
/// still hold the same external IDs; per-generation uniqueness indexes fail under rollback/re-enable.
|
/// still hold the same external IDs; per-generation uniqueness indexes fail under rollback/re-enable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|||||||
/// applies fleet-wide.
|
/// applies fleet-wide.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>Per <c>docs/v2/plan.md</c> decisions #105 and #150 — this entity is <b>control-plane
|
/// <para>This entity is <b>control-plane
|
||||||
/// only</b>. The OPC UA data-path evaluator does not read these rows; it reads
|
/// only</b>. The OPC UA data-path evaluator does not read these rows; it reads
|
||||||
/// <see cref="NodeAcl"/> joined directly against the session's resolved LDAP group
|
/// <see cref="NodeAcl"/> joined directly against the session's resolved LDAP group
|
||||||
/// memberships. Collapsing the two would let a user inherit tag permissions via an
|
/// memberships. Collapsing the two would let a user inherit tag permissions via an
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// OPC UA namespace served by a cluster. Generation-versioned per decision #123 —
|
/// OPC UA namespace served by a cluster. Generation-versioned —
|
||||||
/// namespaces are content (affect what consumers see at the endpoint), not topology.
|
/// namespaces are content (affect what consumers see at the endpoint), not topology.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class Namespace
|
public sealed class Namespace
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One ACL grant: an LDAP group gets a set of <see cref="NodePermissions"/> at a specific scope.
|
/// One ACL grant: an LDAP group gets a set of <see cref="NodePermissions"/> at a specific scope.
|
||||||
/// Generation-versioned per decision #130. See <c>acl-design.md</c> for evaluation algorithm.
|
/// Generation-versioned per decision. See <c>acl-design.md</c> for evaluation algorithm.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class NodeAcl
|
public sealed class NodeAcl
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per Phase 7 plan decision #8 — user-authored C# script source, referenced by
|
/// User-authored C# script source, referenced by
|
||||||
/// <see cref="VirtualTag"/> and <see cref="ScriptedAlarm"/>. One row per script,
|
/// <see cref="VirtualTag"/> and <see cref="ScriptedAlarm"/>. One row per script,
|
||||||
/// per generation. <c>SourceHash</c> is the compile-cache key.
|
/// per generation. <c>SourceHash</c> is the compile-cache key.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per Phase 7 plan decisions #5, #13, #15 — a scripted OPC UA Part 9 alarm whose
|
/// A scripted OPC UA Part 9 alarm whose
|
||||||
/// condition is the predicate <see cref="Script"/> referenced by
|
/// condition is the predicate <see cref="Script"/> referenced by
|
||||||
/// <see cref="PredicateScriptId"/>. Materialized by <c>Core.ScriptedAlarms</c> as a
|
/// <see cref="PredicateScriptId"/>. Materialized by <c>Core.ScriptedAlarms</c> as a
|
||||||
/// concrete <c>AlarmConditionType</c> subtype per <see cref="AlarmType"/>.
|
/// concrete <c>AlarmConditionType</c> subtype per <see cref="AlarmType"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Message tokens (<c>{TagPath}</c>) resolved at emission time per plan decision #13.
|
/// Message tokens (<c>{TagPath}</c>) resolved at emission time.
|
||||||
/// <see cref="HistorizeToAveva"/> (plan decision #15) gates whether transitions
|
/// <see cref="HistorizeToAveva"/> gates whether transitions
|
||||||
/// route through the Core.AlarmHistorian SQLite queue + Galaxy.Host to the Aveva
|
/// route through the Core.AlarmHistorian SQLite queue + Galaxy.Host to the Aveva
|
||||||
/// Historian alarm schema.
|
/// Historian alarm schema.
|
||||||
/// </para>
|
/// </para>
|
||||||
@@ -41,7 +41,7 @@ public sealed class ScriptedAlarm
|
|||||||
public required string PredicateScriptId { get; set; }
|
public required string PredicateScriptId { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Plan decision #15 — when true, transitions route through the SQLite store-and-forward
|
/// When true, transitions route through the SQLite store-and-forward
|
||||||
/// queue to the Aveva Historian. Defaults on for scripted alarms because they are the
|
/// queue to the Aveva Historian. Defaults on for scripted alarms because they are the
|
||||||
/// primary motivation for the historian sink; operator can disable per alarm.
|
/// primary motivation for the historian sink; operator can disable per alarm.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per Phase 7 plan decision #14 — persistent runtime state for each scripted alarm.
|
/// Persistent runtime state for each scripted alarm. Survives process restart so
|
||||||
/// Survives process restart so operators don't re-ack and ack history survives for
|
/// operators don't re-ack and ack history survives for
|
||||||
/// GxP / 21 CFR Part 11 compliance. Keyed on <c>ScriptedAlarmId</c> logically (not
|
/// GxP / 21 CFR Part 11 compliance. Keyed on <c>ScriptedAlarmId</c> logically (not
|
||||||
/// per-generation) because ack state follows the alarm's stable identity across
|
/// per-generation) because ack state follows the alarm's stable identity across
|
||||||
/// generations — a Modified alarm keeps its ack history.
|
/// generations — a Modified alarm keeps its ack history.
|
||||||
@@ -24,7 +24,7 @@ public sealed class ScriptedAlarmState
|
|||||||
/// <summary>Logical FK — matches <see cref="ScriptedAlarm.ScriptedAlarmId"/>. One row per alarm identity.</summary>
|
/// <summary>Logical FK — matches <see cref="ScriptedAlarm.ScriptedAlarmId"/>. One row per alarm identity.</summary>
|
||||||
public required string ScriptedAlarmId { get; set; }
|
public required string ScriptedAlarmId { get; set; }
|
||||||
|
|
||||||
/// <summary>Enabled/Disabled. Persists across restart per plan decision #14.</summary>
|
/// <summary>Enabled/Disabled. Persists across restart.</summary>
|
||||||
public required string EnabledState { get; set; } = "Enabled";
|
public required string EnabledState { get; set; } = "Enabled";
|
||||||
|
|
||||||
/// <summary>Unacknowledged / Acknowledged.</summary>
|
/// <summary>Unacknowledged / Acknowledged.</summary>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public sealed class ServerCluster
|
|||||||
/// <summary>Gets or sets the display name for the server cluster.</summary>
|
/// <summary>Gets or sets the display name for the server cluster.</summary>
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
/// <summary>UNS level 1. Canonical org value: "zb" per decision #140.</summary>
|
/// <summary>UNS level 1. Canonical org value: "zb".</summary>
|
||||||
public required string Enterprise { get; set; }
|
public required string Enterprise { get; set; }
|
||||||
|
|
||||||
/// <summary>UNS level 2, e.g. "warsaw-west".</summary>
|
/// <summary>UNS level 2, e.g. "warsaw-west".</summary>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ public sealed class Tag
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public required TagAccessLevel AccessLevel { get; set; }
|
public required TagAccessLevel AccessLevel { get; set; }
|
||||||
|
|
||||||
/// <summary>Per decisions #44–45 — opt-in for write retry eligibility.</summary>
|
/// <summary>Opt-in for write retry eligibility.</summary>
|
||||||
public bool WriteIdempotent { get; set; }
|
public bool WriteIdempotent { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>UNS level-3 segment. Generation-versioned per decision #115.</summary>
|
/// <summary>UNS level-3 segment. Generation-versioned.</summary>
|
||||||
public sealed class UnsArea
|
public sealed class UnsArea
|
||||||
{
|
{
|
||||||
/// <summary>Gets or sets the unique row identifier.</summary>
|
/// <summary>Gets or sets the unique row identifier.</summary>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>UNS level-4 segment. Generation-versioned per decision #115.</summary>
|
/// <summary>UNS level-4 segment. Generation-versioned.</summary>
|
||||||
public sealed class UnsLine
|
public sealed class UnsLine
|
||||||
{
|
{
|
||||||
/// <summary>Gets or sets the unique row identifier for this UNS line.</summary>
|
/// <summary>Gets or sets the unique row identifier for this UNS line.</summary>
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per Phase 7 plan decision #2 — a virtual (calculated) tag that lives in the
|
/// A virtual (calculated) tag that lives in the Equipment tree alongside driver tags.
|
||||||
/// Equipment tree alongside driver tags. Value is produced by the
|
/// Value is produced by the <see cref="Script"/> referenced by <see cref="ScriptId"/>.
|
||||||
/// <see cref="Script"/> referenced by <see cref="ScriptId"/>.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <see cref="EquipmentId"/> is mandatory — virtual tags are always scoped to an
|
/// <see cref="EquipmentId"/> is mandatory — virtual tags are always scoped to an
|
||||||
/// Equipment node per plan decision #2 (unified Equipment tree, not a separate
|
/// Equipment node (unified Equipment tree, not a separate
|
||||||
/// /Virtual namespace). <see cref="DataType"/> matches the shape used by
|
/// /Virtual namespace). <see cref="DataType"/> matches the shape used by
|
||||||
/// <c>Tag.DataType</c>.
|
/// <c>Tag.DataType</c>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// <see cref="ChangeTriggered"/> and <see cref="TimerIntervalMs"/> together realize
|
/// <see cref="ChangeTriggered"/> and <see cref="TimerIntervalMs"/> together realize
|
||||||
/// plan decision #3 (change + timer). At least one must produce evaluations; the
|
/// change + timer evaluation. At least one must produce evaluations; the
|
||||||
/// Core.VirtualTags engine rejects an all-disabled tag at load time.
|
/// Core.VirtualTags engine rejects an all-disabled tag at load time.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
|||||||
@@ -8,14 +8,14 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Per <c>docs/v2/plan.md</c> decision #150 the two concerns share zero runtime code path:
|
/// Per <c>docs/v2/plan.md</c> the two concerns share zero runtime code path:
|
||||||
/// the control plane (Admin UI) consumes <see cref="Entities.LdapGroupRoleMapping"/>; the
|
/// the control plane (Admin UI) consumes <see cref="Entities.LdapGroupRoleMapping"/>; the
|
||||||
/// data plane consumes <see cref="Entities.NodeAcl"/> rows directly. Having them in one
|
/// data plane consumes <see cref="Entities.NodeAcl"/> rows directly. Having them in one
|
||||||
/// table would collapse the distinction + let a user inherit tag permissions via their
|
/// table would collapse the distinction + let a user inherit tag permissions via their
|
||||||
/// admin-role claim path.
|
/// admin-role claim path.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Task 1.7 standardized the member names on the canonical control-plane role vocabulary
|
/// The member names were standardized on the canonical control-plane role vocabulary
|
||||||
/// (<c>ZB.MOM.WW.Auth</c> <c>CanonicalRole</c>): <c>ConfigViewer → Viewer</c>,
|
/// (<c>ZB.MOM.WW.Auth</c> <c>CanonicalRole</c>): <c>ConfigViewer → Viewer</c>,
|
||||||
/// <c>ConfigEditor → Designer</c>, <c>FleetAdmin → Administrator</c>. The appsettings-only
|
/// <c>ConfigEditor → Designer</c>, <c>FleetAdmin → Administrator</c>. The appsettings-only
|
||||||
/// <c>DriverOperator</c> string role likewise became <c>Operator</c>. These members persist
|
/// <c>DriverOperator</c> string role likewise became <c>Operator</c>. These members persist
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||||
|
|
||||||
/// <summary>Credential kind for <see cref="Entities.ClusterNodeCredential"/>. Per decision #83.</summary>
|
/// <summary>Credential kind for <see cref="Entities.ClusterNodeCredential"/>.</summary>
|
||||||
public enum CredentialKind
|
public enum CredentialKind
|
||||||
{
|
{
|
||||||
SqlLogin,
|
SqlLogin,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||||
|
|
||||||
/// <summary>OPC UA namespace kind per decision #107. One of each kind per cluster per generation.</summary>
|
/// <summary>OPC UA namespace kind. One of each kind per cluster per generation.</summary>
|
||||||
public enum NamespaceKind
|
public enum NamespaceKind
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||||
|
|
||||||
/// <summary>External-ID reservation kind. Per decision #124.</summary>
|
/// <summary>External-ID reservation kind.</summary>
|
||||||
public enum ReservationKind
|
public enum ReservationKind
|
||||||
{
|
{
|
||||||
ZTag,
|
ZTag,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using LiteDB;
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Generation-sealed LiteDB cache per <c>docs/v2/plan.md</c> decision #148 and Phase 6.1
|
/// Generation-sealed LiteDB cache per <c>docs/v2/plan.md</c> and Phase 6.1
|
||||||
/// Stream D.1. Each published generation writes one <b>read-only</b> LiteDB file under
|
/// Stream D.1. Each published generation writes one <b>read-only</b> LiteDB file under
|
||||||
/// <c><cache-root>/<clusterId>/<generationId>.db</c>. A per-cluster
|
/// <c><cache-root>/<clusterId>/<generationId>.db</c>. A per-cluster
|
||||||
/// <c>CURRENT</c> text file holds the currently-active generation id; it is updated
|
/// <c>CURRENT</c> text file holds the currently-active generation id; it is updated
|
||||||
@@ -32,7 +32,7 @@ public sealed class GenerationSealedCache
|
|||||||
// BsonMapper.Global is a process-wide singleton whose lazy per-type member registration is
|
// BsonMapper.Global is a process-wide singleton whose lazy per-type member registration is
|
||||||
// not thread-safe across concurrently-constructed LiteDatabase instances; a seal racing a
|
// not thread-safe across concurrently-constructed LiteDatabase instances; a seal racing a
|
||||||
// read (or this cache racing LiteDbConfigCache) corrupts the global mapper, surfacing as
|
// read (or this cache racing LiteDbConfigCache) corrupts the global mapper, surfacing as
|
||||||
// "Member … not found on BsonMapper" or a bogus duplicate-_id insert — Configuration-012.
|
// "Member … not found on BsonMapper" or a bogus duplicate-_id insert.
|
||||||
private static BsonMapper BuildMapper()
|
private static BsonMapper BuildMapper()
|
||||||
{
|
{
|
||||||
var mapper = new BsonMapper();
|
var mapper = new BsonMapper();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per-node local cache of the most-recently-applied generation(s). Used to bootstrap the
|
/// Per-node local cache of the most-recently-applied generation(s). Used to bootstrap the
|
||||||
/// address space when the central DB is unreachable (decision #79 — degraded-but-running).
|
/// address space when the central DB is unreachable (degraded-but-running).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para><b>Concurrency contract:</b> implementations must serialize writes — specifically,
|
/// <para><b>Concurrency contract:</b> implementations must serialize writes — specifically,
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.LocalCache;
|
|||||||
/// LiteDB-backed <see cref="ILocalConfigCache"/>. One file per node (default
|
/// LiteDB-backed <see cref="ILocalConfigCache"/>. One file per node (default
|
||||||
/// <c>config_cache.db</c>), one collection per snapshot. Corruption surfaces as
|
/// <c>config_cache.db</c>), one collection per snapshot. Corruption surfaces as
|
||||||
/// <see cref="LocalConfigCacheCorruptException"/> on construction or read — callers should
|
/// <see cref="LocalConfigCacheCorruptException"/> on construction or read — callers should
|
||||||
/// delete and re-fetch from the central DB (decision #80).
|
/// delete and re-fetch from the central DB.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||||
{
|
{
|
||||||
@@ -17,7 +17,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
|||||||
// instances. When several caches (this one + GenerationSealedCache) initialise in parallel
|
// instances. When several caches (this one + GenerationSealedCache) initialise in parallel
|
||||||
// the global mapper races, surfacing as "Member ClusterId not found on BsonMapper" or a
|
// the global mapper races, surfacing as "Member ClusterId not found on BsonMapper" or a
|
||||||
// bogus "duplicate key _id = 0" (the int auto-id mapping was lost so Insert writes a literal
|
// bogus "duplicate key _id = 0" (the int auto-id mapping was lost so Insert writes a literal
|
||||||
// 0 twice) — Configuration-012. Give each database a private, pre-registered mapper so member
|
// 0 twice). Give each database a private, pre-registered mapper so member
|
||||||
// resolution happens once, single-threaded, at construction and never touches the global.
|
// resolution happens once, single-threaded, at construction and never touches the global.
|
||||||
private static BsonMapper BuildMapper()
|
private static BsonMapper BuildMapper()
|
||||||
{
|
{
|
||||||
@@ -30,7 +30,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
|||||||
private readonly ILiteCollection<GenerationSnapshot> _col;
|
private readonly ILiteCollection<GenerationSnapshot> _col;
|
||||||
// PutAsync is a find-then-insert/update; without serialization, two concurrent puts for the
|
// PutAsync is a find-then-insert/update; without serialization, two concurrent puts for the
|
||||||
// same (ClusterId, GenerationId) can both observe `existing is null` and both Insert,
|
// same (ClusterId, GenerationId) can both observe `existing is null` and both Insert,
|
||||||
// producing duplicate rows (Configuration-005). Serialize writes through this semaphore so
|
// producing duplicate rows. Serialize writes through this semaphore so
|
||||||
// the read-modify-write block is atomic for a given instance. LiteDB itself only locks the
|
// the read-modify-write block is atomic for a given instance. LiteDB itself only locks the
|
||||||
// page-level write, not the find-then-insert window.
|
// page-level write, not the find-then-insert window.
|
||||||
private readonly SemaphoreSlim _writeGate = new(initialCount: 1, maxCount: 1);
|
private readonly SemaphoreSlim _writeGate = new(initialCount: 1, maxCount: 1);
|
||||||
@@ -60,9 +60,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();
|
||||||
@@ -73,15 +71,13 @@ 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();
|
||||||
// Serialize the find-then-insert/update so concurrent callers do not observe a stale
|
// Serialize the find-then-insert/update so concurrent callers do not observe a stale
|
||||||
// `existing is null` and both Insert (Configuration-005). LiteDB's per-call lock is
|
// `existing is null` and both Insert. LiteDB's per-call lock is not enough — the
|
||||||
// not enough — the read and the write are independent calls.
|
// read and the write are independent calls.
|
||||||
await _writeGate.WaitAsync(ct).ConfigureAwait(false);
|
await _writeGate.WaitAsync(ct).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -104,10 +100,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();
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ public sealed class ResilientConfigReader
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Configuration-010: redact connection-string fragments (Password, User Id, Pwd, etc.)
|
/// Redacts connection-string fragments (Password, User Id, Pwd, etc.)
|
||||||
/// that a caller's exception message could carry. Conservative regex pass — anything
|
/// that a caller's exception message could carry. Conservative regex pass — anything
|
||||||
/// matching <c>Key=Value</c> with a known credential key gets its value replaced.
|
/// matching <c>Key=Value</c> with a known credential key gets its value replaced.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -120,7 +120,7 @@ public sealed class ResilientConfigReader
|
|||||||
// that case, not propagate. Only rethrow if the caller actually requested cancellation.
|
// that case, not propagate. Only rethrow if the caller actually requested cancellation.
|
||||||
catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
|
catch (Exception ex) when (ex is not OperationCanceledException || !cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
// Configuration-010: do NOT pass the raw exception object — it carries the stack
|
// Do NOT pass the raw exception object — it carries the stack
|
||||||
// and inner-exception chain, and SqlException/wrapping delegates can surface
|
// and inner-exception chain, and SqlException/wrapping delegates can surface
|
||||||
// connection-string fragments (Password=…, User Id=…) embedded in messages.
|
// connection-string fragments (Password=…, User Id=…) embedded in messages.
|
||||||
// Log only the exception type and a scrubbed message so secrets stay out of logs.
|
// Log only the exception type and a scrubbed message so secrets stay out of logs.
|
||||||
|
|||||||
@@ -72,8 +72,6 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
/// <summary>Gets the DbSet of data protection keys.</summary>
|
/// <summary>Gets the DbSet of data protection keys.</summary>
|
||||||
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
public DbSet<DataProtectionKey> DataProtectionKeys => Set<DataProtectionKey>();
|
||||||
|
|
||||||
/// <summary>Configures the entity model when the context is first created.</summary>
|
|
||||||
/// <param name="modelBuilder">The model builder used to configure the context.</param>
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
@@ -151,13 +149,12 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
|||||||
.HasForeignKey(x => x.ClusterId)
|
.HasForeignKey(x => x.ClusterId)
|
||||||
.OnDelete(DeleteBehavior.Restrict);
|
.OnDelete(DeleteBehavior.Restrict);
|
||||||
|
|
||||||
// Fleet-wide unique per decision #86
|
// Fleet-wide unique.
|
||||||
e.HasIndex(x => x.ApplicationUri).IsUnique().HasDatabaseName("UX_ClusterNode_ApplicationUri");
|
e.HasIndex(x => x.ApplicationUri).IsUnique().HasDatabaseName("UX_ClusterNode_ApplicationUri");
|
||||||
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_ClusterNode_ClusterId");
|
e.HasIndex(x => x.ClusterId).HasDatabaseName("IX_ClusterNode_ClusterId");
|
||||||
// v2: the "one Primary per cluster" filtered unique index (and the RedundancyRole
|
// v2: the "one Primary per cluster" filtered unique index (and the RedundancyRole
|
||||||
// column it filtered on) are gone. Akka cluster leader-of-driver-role is the
|
// column it filtered on) are gone. Akka cluster leader-of-driver-role is the
|
||||||
// authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator,
|
// authoritative primary signal (see RedundancyStateActor + ServiceLevelCalculator).
|
||||||
// Task 35).
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Services;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// CRUD surface for <see cref="LdapGroupRoleMapping"/> — the control-plane mapping from
|
/// CRUD surface for <see cref="LdapGroupRoleMapping"/> — the control-plane mapping from
|
||||||
/// LDAP groups to Admin UI roles. Consumed only by Admin UI code paths; the OPC UA
|
/// LDAP groups to Admin UI roles. Consumed only by Admin UI code paths; the OPC UA
|
||||||
/// data-path evaluator MUST NOT depend on this interface (see decision #150 and the
|
/// data-path evaluator MUST NOT depend on this interface (see the
|
||||||
/// Phase 6.2 compliance check on control/data-plane separation).
|
/// Phase 6.2 compliance check on control/data-plane separation).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
@@ -28,11 +28,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>The mappings whose LDAP group matches one of <paramref name="ldapGroups"/>.</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>Every LDAP group role mapping.</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>
|
||||||
@@ -43,11 +45,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>The created mapping row.</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 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)
|
||||||
{
|
{
|
||||||
@@ -28,9 +25,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
|
|||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Lists all LDAP group role mappings.</summary>
|
/// <inheritdoc />
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>All role mappings ordered by group and cluster ID.</returns>
|
|
||||||
public async Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken)
|
||||||
=> await db.LdapGroupRoleMappings
|
=> await db.LdapGroupRoleMappings
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
@@ -39,10 +34,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
|
|||||||
.ToListAsync(cancellationToken)
|
.ToListAsync(cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
/// <summary>Creates a new LDAP group role mapping.</summary>
|
/// <inheritdoc />
|
||||||
/// <param name="row">The mapping to create.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>The created mapping with generated ID and timestamp.</returns>
|
|
||||||
public async Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken)
|
public async Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(row);
|
ArgumentNullException.ThrowIfNull(row);
|
||||||
@@ -56,10 +48,7 @@ public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILd
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Deletes an LDAP group role mapping.</summary>
|
/// <inheritdoc />
|
||||||
/// <param name="id">The mapping identifier.</param>
|
|
||||||
/// <param name="cancellationToken">The cancellation token.</param>
|
|
||||||
/// <returns>A task that completes when the deletion is done.</returns>
|
|
||||||
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken)
|
public async Task DeleteAsync(Guid id, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var existing = await db.LdapGroupRoleMappings.FindAsync([id], cancellationToken).ConfigureAwait(false);
|
var existing = await db.LdapGroupRoleMappings.FindAsync([id], cancellationToken).ConfigureAwait(false);
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
|||||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
namespace ZB.MOM.WW.OtOpcUa.Configuration.Validation;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Managed-code pre-publish validator per decision #91. Complements the structural checks in
|
/// Managed-code pre-publish validator. Complements the structural checks in
|
||||||
/// <c>sp_ValidateDraft</c> — this layer owns schema validation for JSON columns, UNS segment
|
/// <c>sp_ValidateDraft</c> — this layer owns schema validation for JSON columns, UNS segment
|
||||||
/// regex, EquipmentId derivation, cross-cluster checks, and anything else that's uncomfortable
|
/// regex, EquipmentId derivation, cross-cluster checks, and anything else that's uncomfortable
|
||||||
/// to express in T-SQL. Returns every failing rule in one pass (decision: surface all errors,
|
/// to express in T-SQL. Returns every failing rule in one pass (decision: surface all errors,
|
||||||
@@ -21,6 +21,7 @@ public static class DraftValidator
|
|||||||
/// Validates a draft snapshot and returns all validation errors found in a single pass.
|
/// 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>Every validation error found; empty when 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>();
|
||||||
@@ -204,8 +205,9 @@ public static class DraftValidator
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Decision #125: EquipmentId = 'EQ-' + lowercase first 12 hex chars of the UUID.</summary>
|
/// <summary>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 <c>EQ-</c>-prefixed EquipmentId.</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();
|
||||||
|
|
||||||
@@ -222,7 +224,7 @@ public static class DraftValidator
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Phase 6.3 Stream A.2 + task #148 part 2 — managed pre-publish guard for cluster
|
/// Managed pre-publish guard for cluster
|
||||||
/// topology vs. <see cref="ServerCluster.RedundancyMode"/>. The SQL
|
/// topology vs. <see cref="ServerCluster.RedundancyMode"/>. The SQL
|
||||||
/// <c>CK_ServerCluster_RedundancyMode_NodeCount</c> CHECK already enforces the
|
/// <c>CK_ServerCluster_RedundancyMode_NodeCount</c> CHECK already enforces the
|
||||||
/// (NodeCount, RedundancyMode) pair on the row itself, but it cannot see the
|
/// (NodeCount, RedundancyMode) pair on the row itself, but it cannot see the
|
||||||
@@ -240,6 +242,8 @@ 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>Every failing topology rule found; empty when the cluster's declared and enabled-node
|
||||||
|
/// topology is consistent with its <see cref="ServerCluster.RedundancyMode"/>.</returns>
|
||||||
public static IReadOnlyList<ValidationError> ValidateClusterTopology(
|
public static IReadOnlyList<ValidationError> ValidateClusterTopology(
|
||||||
ServerCluster cluster,
|
ServerCluster cluster,
|
||||||
IReadOnlyList<ClusterNode> clusterNodes)
|
IReadOnlyList<ClusterNode> clusterNodes)
|
||||||
@@ -273,7 +277,7 @@ public static class DraftValidator
|
|||||||
cluster.ClusterId));
|
cluster.ClusterId));
|
||||||
|
|
||||||
// v2: the v1 "exactly one Primary per cluster" invariant is gone. RedundancyRole was
|
// v2: the v1 "exactly one Primary per cluster" invariant is gone. RedundancyRole was
|
||||||
// dropped in Task 14d; in v2 the Akka cluster's role-leader-of-"driver" elects the
|
// dropped; in v2 the Akka cluster's role-leader-of-"driver" elects the
|
||||||
// primary at runtime, so there is no static configuration to validate here.
|
// primary at runtime, so there is no static configuration to validate here.
|
||||||
|
|
||||||
return errors;
|
return errors;
|
||||||
|
|||||||
@@ -6,10 +6,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// shape so the node-manager can pass through quality, source timestamp, and
|
/// shape so the node-manager can pass through quality, source timestamp, and
|
||||||
/// server timestamp without translation.
|
/// server timestamp without translation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Per <c>docs/v2/plan.md</c> decision #13 — every driver maps to the same
|
|
||||||
/// OPC UA StatusCode space; this DTO is the universal carrier.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="Value">The raw value; null when <see cref="StatusCode"/> indicates Bad.</param>
|
/// <param name="Value">The raw value; null when <see cref="StatusCode"/> indicates Bad.</param>
|
||||||
/// <param name="StatusCode">OPC UA status code (numeric value matches the OPC UA spec).</param>
|
/// <param name="StatusCode">OPC UA status code (numeric value matches the OPC UA spec).</param>
|
||||||
/// <param name="SourceTimestampUtc">Driver-side timestamp when the value was sampled at the source. Null if unavailable.</param>
|
/// <param name="SourceTimestampUtc">Driver-side timestamp when the value was sampled at the source. Null if unavailable.</param>
|
||||||
|
|||||||
@@ -27,14 +27,14 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="WriteIdempotent">
|
/// <param name="WriteIdempotent">
|
||||||
/// True when a timed-out or failed write to this attribute is safe to replay. Per
|
/// True when a timed-out or failed write to this attribute is safe to replay. Per
|
||||||
/// <c>docs/v2/plan.md</c> decisions #44, #45, #143 — writes are NOT auto-retried by default
|
/// <c>docs/v2/plan.md</c>, writes are NOT auto-retried by default
|
||||||
/// because replaying a pulse / alarm-ack / counter-increment / recipe-step advance can
|
/// because replaying a pulse / alarm-ack / counter-increment / recipe-step advance can
|
||||||
/// duplicate field actions. Drivers flag only tags whose semantics make retry safe
|
/// duplicate field actions. Drivers flag only tags whose semantics make retry safe
|
||||||
/// (holding registers with level-set values, set-point writes to analog tags) — the
|
/// (holding registers with level-set values, set-point writes to analog tags) — the
|
||||||
/// capability invoker respects this flag when deciding whether to apply Polly retry.
|
/// capability invoker respects this flag when deciding whether to apply Polly retry.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="Source">
|
/// <param name="Source">
|
||||||
/// Per ADR-002 — discriminates which runtime subsystem owns this node's dispatch.
|
/// Discriminates which runtime subsystem owns this node's dispatch.
|
||||||
/// Defaults to <see cref="NodeSourceKind.Driver"/> so existing callers are unchanged.
|
/// Defaults to <see cref="NodeSourceKind.Driver"/> so existing callers are unchanged.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="VirtualTagId">
|
/// <param name="VirtualTagId">
|
||||||
@@ -59,7 +59,7 @@ public sealed record DriverAttributeInfo(
|
|||||||
string? ScriptedAlarmId = null);
|
string? ScriptedAlarmId = null);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Per ADR-002 — discriminates which runtime subsystem owns this node's Read/Write/
|
/// Discriminates which runtime subsystem owns this node's Read/Write/
|
||||||
/// Subscribe dispatch. <c>Driver</c> = a real IDriver capability surface;
|
/// Subscribe dispatch. <c>Driver</c> = a real IDriver capability surface;
|
||||||
/// <c>Virtual</c> = a Phase 7 <see cref="DriverAttributeInfo"/>.VirtualTagId'd tag
|
/// <c>Virtual</c> = a Phase 7 <see cref="DriverAttributeInfo"/>.VirtualTagId'd tag
|
||||||
/// computed by the VirtualTagEngine; <c>ScriptedAlarm</c> = a scripted Part 9 alarm
|
/// computed by the VirtualTagEngine; <c>ScriptedAlarm</c> = a scripted Part 9 alarm
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// <see cref="IAlarmSource"/>, <see cref="IHistoryProvider"/>).
|
/// <see cref="IAlarmSource"/>, <see cref="IHistoryProvider"/>).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Per <c>docs/v2/plan.md</c> decision #143 (per-capability retry policy): Read / HistoryRead /
|
/// Per the per-capability retry policy in <c>docs/v2/plan.md</c>: Read / HistoryRead /
|
||||||
/// Discover / Probe / AlarmSubscribe auto-retry; <see cref="Write"/> does NOT retry unless the
|
/// Discover / Probe / AlarmSubscribe auto-retry; <see cref="Write"/> does NOT retry unless the
|
||||||
/// tag-definition carries <see cref="WriteIdempotentAttribute"/>. Alarm-acknowledge is treated
|
/// tag-definition carries <see cref="WriteIdempotentAttribute"/>. Alarm-acknowledge is treated
|
||||||
/// as a write for retry semantics (an alarm-ack is not idempotent at the plant-floor acknowledgement
|
/// as a write for retry semantics (an alarm-ack is not idempotent at the plant-floor acknowledgement
|
||||||
@@ -34,7 +34,7 @@ public enum DriverCapability
|
|||||||
/// <summary><see cref="IAlarmSource.SubscribeAlarmsAsync"/>. Retries by default.</summary>
|
/// <summary><see cref="IAlarmSource.SubscribeAlarmsAsync"/>. Retries by default.</summary>
|
||||||
AlarmSubscribe,
|
AlarmSubscribe,
|
||||||
|
|
||||||
/// <summary><see cref="IAlarmSource.AcknowledgeAsync"/>. Does NOT retry — ack is a write-shaped operation (decision #143).</summary>
|
/// <summary><see cref="IAlarmSource.AcknowledgeAsync"/>. Does NOT retry — ack is a write-shaped operation.</summary>
|
||||||
AlarmAcknowledge,
|
AlarmAcknowledge,
|
||||||
|
|
||||||
/// <summary><see cref="IHistoryProvider"/> reads (Raw/Processed/AtTime/Events). Retries by default.</summary>
|
/// <summary><see cref="IHistoryProvider"/> reads (Raw/Processed/AtTime/Events). Retries by default.</summary>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// failure (useful for diagnostics), and <see cref="DriverState.Degraded"/> /
|
/// failure (useful for diagnostics), and <see cref="DriverState.Degraded"/> /
|
||||||
/// <see cref="DriverState.Reconnecting"/> / <see cref="DriverState.Faulted"/> states may all
|
/// <see cref="DriverState.Reconnecting"/> / <see cref="DriverState.Faulted"/> states may all
|
||||||
/// carry a non-null message. Callers must not key behaviour on the LastError-null ↔ Healthy
|
/// carry a non-null message. Callers must not key behaviour on the LastError-null ↔ Healthy
|
||||||
/// pairing (Core.Abstractions-008).
|
/// pairing.
|
||||||
/// </param>
|
/// </param>
|
||||||
public sealed record DriverHealth(
|
public sealed record DriverHealth(
|
||||||
DriverState State,
|
DriverState State,
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// supervision with process-level recycle is in play.
|
/// supervision with process-level recycle is in play.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Per <c>docs/v2/driver-stability.md</c> §2-4 and <c>docs/v2/plan.md</c> decisions #63-74.
|
|
||||||
///
|
|
||||||
/// <list type="bullet">
|
/// <list type="bullet">
|
||||||
/// <item><b>A</b> — managed, known-good SDK; low blast radius. In-process. Fast retries.
|
/// <item><b>A</b> — managed, known-good SDK; low blast radius. In-process. Fast retries.
|
||||||
/// Examples: OPC UA Client (OPCFoundation stack), S7 (S7NetPlus).</item>
|
/// Examples: OPC UA Client (OPCFoundation stack), S7 (S7NetPlus).</item>
|
||||||
@@ -18,7 +16,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// </list>
|
/// </list>
|
||||||
///
|
///
|
||||||
/// <para>Process-kill protections (<c>MemoryRecycle</c>, <c>ScheduledRecycleScheduler</c>) are
|
/// <para>Process-kill protections (<c>MemoryRecycle</c>, <c>ScheduledRecycleScheduler</c>) are
|
||||||
/// Tier C only per decisions #73-74 and #145 — killing an in-process Tier A/B driver also kills
|
/// Tier C only — killing an in-process Tier A/B driver also kills
|
||||||
/// every OPC UA session and every co-hosted driver, blast-radius worse than the leak.</para>
|
/// every OPC UA session and every co-hosted driver, blast-radius worse than the leak.</para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public enum DriverTier
|
public enum DriverTier
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// the registry to validate <c>DriverInstance.DriverType</c> values from the central config DB.
|
/// the registry to validate <c>DriverInstance.DriverType</c> values from the central config DB.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Per <c>docs/v2/plan.md</c> decisions #91 (JSON content validation in Admin app, not SQL CLR)
|
/// Per <c>docs/v2/plan.md</c> decisions on JSON content validation happening in the Admin app
|
||||||
/// and #111 (driver type → namespace kind mapping enforced by sp_ValidateDraft).
|
/// (not SQL CLR), and on driver type → namespace kind mapping being enforced by
|
||||||
/// The registry is the source of truth for both checks.
|
/// <c>sp_ValidateDraft</c>. The registry is the source of truth for both checks.
|
||||||
///
|
///
|
||||||
/// Thread-safety: registration is typically single-threaded at startup; lookups happen on
|
/// Thread-safety: registration is typically single-threaded at startup; lookups happen on
|
||||||
/// every config-apply (multi-threaded). The check-then-act inside <see cref="Register"/> is
|
/// every config-apply (multi-threaded). The check-then-act inside <see cref="Register"/> is
|
||||||
@@ -28,7 +28,7 @@ public sealed class DriverTypeRegistry
|
|||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The check-then-act (duplicate check → copy-on-write rebuild → swap) is performed under
|
/// The check-then-act (duplicate check → copy-on-write rebuild → swap) is performed under
|
||||||
/// <see cref="_writeLock"/> so concurrent <see cref="Register"/> calls cannot silently
|
/// <see cref="_writeLock"/> so concurrent <see cref="Register"/> calls cannot silently
|
||||||
/// discard each other's registrations — see Core.Abstractions-004.
|
/// discard each other's registrations.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="metadata">The driver type metadata to register.</param>
|
/// <param name="metadata">The driver type metadata to register.</param>
|
||||||
public void Register(DriverTypeMetadata metadata)
|
public void Register(DriverTypeMetadata metadata)
|
||||||
@@ -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 registered metadata for the driver type.</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 registered metadata, or <see langword="null"/> if the type is 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 snapshot collection of all registered driver type metadata.</returns>
|
||||||
public IReadOnlyCollection<DriverTypeMetadata> All() => _types.Values.ToList();
|
public IReadOnlyCollection<DriverTypeMetadata> All() => _types.Values.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +89,8 @@ public sealed class DriverTypeRegistry
|
|||||||
/// <param name="DeviceConfigJsonSchema">JSON Schema for <c>DeviceConfig</c> (multi-device drivers); null if the driver has no device layer.</param>
|
/// <param name="DeviceConfigJsonSchema">JSON Schema for <c>DeviceConfig</c> (multi-device drivers); null if the driver has no device layer.</param>
|
||||||
/// <param name="TagConfigJsonSchema">JSON Schema for <c>TagConfig</c>; required for every driver since every driver has tags.</param>
|
/// <param name="TagConfigJsonSchema">JSON Schema for <c>TagConfig</c>; required for every driver since every driver has tags.</param>
|
||||||
/// <param name="Tier">
|
/// <param name="Tier">
|
||||||
/// Stability tier per <c>docs/v2/driver-stability.md</c> §2-4 and <c>docs/v2/plan.md</c>
|
/// Stability tier per <c>docs/v2/driver-stability.md</c> §2-4 and the tiering decisions in
|
||||||
/// decisions #63-74. Drives the shared resilience pipeline defaults
|
/// <c>docs/v2/plan.md</c>. Drives the shared resilience pipeline defaults
|
||||||
/// (<see cref="Tier"/> × capability → <c>CapabilityPolicy</c>), the <c>MemoryTracking</c>
|
/// (<see cref="Tier"/> × capability → <c>CapabilityPolicy</c>), the <c>MemoryTracking</c>
|
||||||
/// hybrid-formula constants, and whether process-level <c>MemoryRecycle</c> / scheduled-
|
/// hybrid-formula constants, and whether process-level <c>MemoryRecycle</c> / scheduled-
|
||||||
/// recycle protections apply (Tier C only). Every registered driver type must declare one.
|
/// recycle protections apply (Tier C only). Every registered driver type must declare one.
|
||||||
@@ -100,7 +103,7 @@ public sealed record DriverTypeMetadata(
|
|||||||
string TagConfigJsonSchema,
|
string TagConfigJsonSchema,
|
||||||
DriverTier Tier);
|
DriverTier Tier);
|
||||||
|
|
||||||
/// <summary>Bitmask of namespace kinds a driver type may populate. Per decision #111.</summary>
|
/// <summary>Bitmask of namespace kinds a driver type may populate.</summary>
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum NamespaceKindCompatibility
|
public enum NamespaceKindCompatibility
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public sealed class EquipmentTagRefResolver<TDef> where TDef : class
|
|||||||
private readonly Func<string, TDef?> _parseRef;
|
private readonly Func<string, TDef?> _parseRef;
|
||||||
private readonly ConcurrentDictionary<string, TDef?> _cache = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, TDef?> _cache = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>Initializes a new instance of the <see cref="EquipmentTagRefResolver{TDef}"/> class.</summary>
|
||||||
/// <param name="byName">Authored tag-table lookup (returns null on miss).</param>
|
/// <param name="byName">Authored tag-table lookup (returns null on miss).</param>
|
||||||
/// <param name="parseRef">Parses an equipment-tag reference (TagConfig JSON) into a transient def, or null.</param>
|
/// <param name="parseRef">Parses an equipment-tag reference (TagConfig JSON) into a transient def, or null.</param>
|
||||||
public EquipmentTagRefResolver(Func<string, TDef?> byName, Func<string, TDef?> parseRef)
|
public EquipmentTagRefResolver(Func<string, TDef?> byName, Func<string, TDef?> parseRef)
|
||||||
|
|||||||
@@ -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>The raw historical samples for the tag over the requested range.</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>One aggregated sample per interval bucket over the requested range.</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>One sample per requested timestamp, in the same order as <paramref name="timestampsUtc"/>.</returns>
|
||||||
Task<HistoryReadResult> ReadAtTimeAsync(
|
Task<HistoryReadResult> ReadAtTimeAsync(
|
||||||
string fullReference,
|
string fullReference,
|
||||||
IReadOnlyList<DateTime> timestampsUtc,
|
IReadOnlyList<DateTime> timestampsUtc,
|
||||||
@@ -77,7 +80,7 @@ public interface IHistorianDataSource : IDisposable
|
|||||||
/// Note on parameter types — <paramref name="maxEvents"/> is <see cref="int"/> (not
|
/// Note on parameter types — <paramref name="maxEvents"/> is <see cref="int"/> (not
|
||||||
/// <see cref="uint"/>) so callers can pass <c>0</c> or a negative value as a "use the
|
/// <see cref="uint"/>) so callers can pass <c>0</c> or a negative value as a "use the
|
||||||
/// backend's default cap" sentinel; see <c>WonderwareHistorianClient</c> /
|
/// backend's default cap" sentinel; see <c>WonderwareHistorianClient</c> /
|
||||||
/// <c>HistorianDataSource</c> and Core.Abstractions-006 for the rationale. The sibling
|
/// <c>HistorianDataSource</c> for the rationale. The sibling
|
||||||
/// <see cref="ReadRawAsync"/> / <see cref="ReadProcessedAsync"/> use
|
/// <see cref="ReadRawAsync"/> / <see cref="ReadProcessedAsync"/> use
|
||||||
/// <c>uint maxValuesPerNode</c> because their OPC UA HistoryRead surface has no
|
/// <c>uint maxValuesPerNode</c> because their OPC UA HistoryRead surface has no
|
||||||
/// equivalent "use default" sentinel.
|
/// equivalent "use default" sentinel.
|
||||||
@@ -85,8 +88,7 @@ public interface IHistorianDataSource : IDisposable
|
|||||||
/// This surface declares <see cref="ReadAtTimeAsync"/> and <see cref="ReadEventsAsync"/>
|
/// This surface declares <see cref="ReadAtTimeAsync"/> and <see cref="ReadEventsAsync"/>
|
||||||
/// as required members — a server-side historian owns the full read surface, unlike
|
/// as required members — a server-side historian owns the full read surface, unlike
|
||||||
/// <see cref="IHistoryProvider"/> where the same two methods are optional default-impl
|
/// <see cref="IHistoryProvider"/> where the same two methods are optional default-impl
|
||||||
/// methods so legacy drivers can stay raw-only. The asymmetry is intentional
|
/// methods so legacy drivers can stay raw-only. The asymmetry is intentional.
|
||||||
/// (Core.Abstractions-008).
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
/// <param name="sourceName">The source name to filter events, or null to return events from all sources.</param>
|
/// <param name="sourceName">The source name to filter events, or null to return events from all sources.</param>
|
||||||
/// <param name="startUtc">The start of the time range in UTC.</param>
|
/// <param name="startUtc">The start of the time range in UTC.</param>
|
||||||
@@ -96,9 +98,10 @@ public interface IHistorianDataSource : IDisposable
|
|||||||
/// default cap. When the backend cap truncates the result, implementations MUST set
|
/// default cap. When the backend cap truncates the result, implementations MUST set
|
||||||
/// <see cref="HistoricalEventsResult.ContinuationPoint"/> to a non-null token so
|
/// <see cref="HistoricalEventsResult.ContinuationPoint"/> to a non-null token so
|
||||||
/// callers can detect truncation and page — a null continuation point means all
|
/// callers can detect truncation and page — a null continuation point means all
|
||||||
/// matching events were returned. (Core.Abstractions-009.)
|
/// matching events were returned.
|
||||||
/// </param>
|
/// </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>The matching historical event records, with a continuation point if the result was truncated.</returns>
|
||||||
Task<HistoricalEventsResult> ReadEventsAsync(
|
Task<HistoricalEventsResult> ReadEventsAsync(
|
||||||
string? sourceName,
|
string? sourceName,
|
||||||
DateTime startUtc,
|
DateTime startUtc,
|
||||||
@@ -110,5 +113,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 health snapshot for the historian backend.</returns>
|
||||||
HistorianHealthSnapshot GetHealthSnapshot();
|
HistorianHealthSnapshot GetHealthSnapshot();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public interface IHistorizationOutbox : IDisposable
|
|||||||
/// <summary>Appends <paramref name="entry"/> to the tail of the durable buffer.</summary>
|
/// <summary>Appends <paramref name="entry"/> to the tail of the durable buffer.</summary>
|
||||||
/// <param name="entry">The value record to buffer.</param>
|
/// <param name="entry">The value record to buffer.</param>
|
||||||
/// <param name="ct">Cancellation token.</param>
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
ValueTask AppendAsync(HistorizationOutboxEntry entry, CancellationToken ct);
|
ValueTask AppendAsync(HistorizationOutboxEntry entry, CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -24,6 +25,7 @@ public interface IHistorizationOutbox : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="max">Maximum number of entries to return; must be positive.</param>
|
/// <param name="max">Maximum number of entries to return; must be positive.</param>
|
||||||
/// <param name="ct">Cancellation token.</param>
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>Up to <paramref name="max"/> oldest un-acked entries in FIFO order.</returns>
|
||||||
ValueTask<IReadOnlyList<HistorizationOutboxEntry>> PeekBatchAsync(int max, CancellationToken ct);
|
ValueTask<IReadOnlyList<HistorizationOutboxEntry>> PeekBatchAsync(int max, CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -32,9 +34,11 @@ public interface IHistorizationOutbox : IDisposable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="id">The <see cref="HistorizationOutboxEntry.Id"/> to ack.</param>
|
/// <param name="id">The <see cref="HistorizationOutboxEntry.Id"/> to ack.</param>
|
||||||
/// <param name="ct">Cancellation token.</param>
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||||
ValueTask RemoveAsync(Guid id, CancellationToken ct);
|
ValueTask RemoveAsync(Guid id, CancellationToken ct);
|
||||||
|
|
||||||
/// <summary>Current number of un-acked entries held in the buffer.</summary>
|
/// <summary>Current number of un-acked entries held in the buffer.</summary>
|
||||||
/// <param name="ct">Cancellation token.</param>
|
/// <param name="ct">Cancellation token.</param>
|
||||||
|
/// <returns>The current number of un-acked entries in the buffer.</returns>
|
||||||
ValueTask<int> CountAsync(CancellationToken ct);
|
ValueTask<int> CountAsync(CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// <para>
|
/// <para>
|
||||||
/// A process-wide singleton via <see cref="Instance"/> (private ctor): it carries no state
|
/// A process-wide singleton via <see cref="Instance"/> (private ctor): it carries no state
|
||||||
/// and is immutable, so one shared instance is safe to assign as the node-manager's
|
/// and is immutable, so one shared instance is safe to assign as the node-manager's
|
||||||
/// <c>HistorianDataSource</c> default until the Host wires a real source post-start (Task 5).
|
/// <c>HistorianDataSource</c> default until the Host wires a real source post-start.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class NullHistorianDataSource : IHistorianDataSource
|
public sealed class NullHistorianDataSource : IHistorianDataSource
|
||||||
@@ -55,10 +55,7 @@ public sealed class NullHistorianDataSource : IHistorianDataSource
|
|||||||
int maxEvents,
|
int maxEvents,
|
||||||
CancellationToken cancellationToken) => Task.FromResult(EmptyEvents);
|
CancellationToken cancellationToken) => Task.FromResult(EmptyEvents);
|
||||||
|
|
||||||
/// <summary>
|
/// <inheritdoc />
|
||||||
/// Returns a fully-disabled snapshot — no connections open, every counter zero, every nullable
|
|
||||||
/// field null, no cluster nodes. Pure; never blocks.
|
|
||||||
/// </summary>
|
|
||||||
public HistorianHealthSnapshot GetHealthSnapshot() => new(
|
public HistorianHealthSnapshot GetHealthSnapshot() => new(
|
||||||
TotalQueries: 0,
|
TotalQueries: 0,
|
||||||
TotalSuccesses: 0,
|
TotalSuccesses: 0,
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// as it discovers nodes — no buffering of the whole tree.
|
/// as it discovers nodes — no buffering of the whole tree.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Per <c>docs/v2/plan.md</c> decision #52 — drivers register nodes via this builder
|
/// Drivers register nodes via this builder rather than returning a tree object,
|
||||||
/// rather than returning a tree object. Supports incremental / large address spaces
|
/// supporting incremental / large address spaces without forcing the driver to
|
||||||
/// without forcing the driver to buffer the whole tree.
|
/// buffer the whole tree.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IAddressSpaceBuilder
|
public interface IAddressSpaceBuilder
|
||||||
{
|
{
|
||||||
@@ -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 the new 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>A handle for the registered variable, used by Core for subscription routing.</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>The sink that receives lifecycle transitions for this alarm 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 handle identifying the created 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);
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// <see cref="IHostConnectivityProbe"/>) are composable — a driver implements only what its
|
/// <see cref="IHostConnectivityProbe"/>) are composable — a driver implements only what its
|
||||||
/// backend actually supports.
|
/// backend actually supports.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
|
||||||
/// Per <c>docs/v2/plan.md</c> decisions #4 (composable capability interfaces) and #53
|
|
||||||
/// (capability discovery via <c>is</c> checks — no redundant flag enum).
|
|
||||||
/// </remarks>
|
|
||||||
public interface IDriver
|
public interface IDriver
|
||||||
{
|
{
|
||||||
/// <summary>Stable logical ID of this driver instance, sourced from the central config DB.</summary>
|
/// <summary>Stable logical ID of this driver instance, sourced from the central config DB.</summary>
|
||||||
@@ -23,6 +19,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 +34,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 driver's current health snapshot.</returns>
|
||||||
DriverHealth GetHealth();
|
DriverHealth GetHealth();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,6 +56,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 memory footprint in bytes.</returns>
|
||||||
long GetMemoryFootprint();
|
long GetMemoryFootprint();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -63,5 +64,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// JSON editor with schema-driven validation against the registered JSON schema.
|
/// JSON editor with schema-driven validation against the registered JSON schema.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Per <c>docs/v2/plan.md</c> decision #27 — driver-specific config editors are deferred
|
/// Driver-specific config editors are deferred
|
||||||
/// to each driver's implementation phase; v2.0 ships with the generic JSON editor as the
|
/// to each driver's implementation phase; v2.0 ships with the generic JSON editor as the
|
||||||
/// default. This interface is the future plug-point so phase-specific editors can land
|
/// default. This interface is the future plug-point so phase-specific editors can land
|
||||||
/// incrementally.
|
/// incrementally.
|
||||||
|
|||||||
@@ -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 the driver instance belongs to.</param>
|
||||||
|
/// <param name="driverInstanceId">The driver instance the snapshot describes.</param>
|
||||||
|
/// <param name="health">The current health snapshot.</param>
|
||||||
|
/// <param name="errorCount5Min">The number of errors observed in the trailing 5-minute window.</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">The driver-specific configuration JSON to probe with.</param>
|
||||||
|
/// <param name="timeout">The maximum time to allow the probe to run.</param>
|
||||||
|
/// <param name="ct">Cancellation token for the operation.</param>
|
||||||
|
/// <returns>The probe outcome, including success/failure and latency.</returns>
|
||||||
Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct);
|
Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// hard fault is detected (memory breach, wedge, scheduled recycle window).
|
/// hard fault is detected (memory breach, wedge, scheduled recycle window).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Per <c>docs/v2/plan.md</c> decisions #68, #73-74, and #145. Tier A/B drivers do NOT have
|
/// Tier A/B drivers do NOT have a supervisor because they run in-process — recycling would
|
||||||
/// a supervisor because they run in-process — recycling would kill every OPC UA session and
|
/// kill every OPC UA session and every co-hosted driver. The Core.Stability layer only invokes
|
||||||
/// every co-hosted driver. The Core.Stability layer only invokes this interface for Tier C
|
/// this interface for Tier C instances after asserting the tier via
|
||||||
/// instances after asserting the tier via <see cref="DriverTypeMetadata.Tier"/>.
|
/// <see cref="DriverTypeMetadata.Tier"/>.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IDriverSupervisor
|
public interface IDriverSupervisor
|
||||||
{
|
{
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// a raw-only driver compiles without forcing it to provide at-time / event surfaces it
|
/// a raw-only driver compiles without forcing it to provide at-time / event surfaces it
|
||||||
/// has no backend for. The sibling server-side surface, <see cref="IHistorianDataSource"/>,
|
/// has no backend for. The sibling server-side surface, <see cref="IHistorianDataSource"/>,
|
||||||
/// declares both methods as required because a registered historian owns the full read
|
/// declares both methods as required because a registered historian owns the full read
|
||||||
/// surface; the asymmetry is intentional (Core.Abstractions-008).
|
/// surface; the asymmetry is intentional.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IHistoryProvider
|
public interface IHistoryProvider
|
||||||
{
|
{
|
||||||
@@ -91,7 +91,7 @@ public interface IHistoryProvider
|
|||||||
/// reads use for <c>maxValuesPerNode</c>) because callers and downstream historian
|
/// reads use for <c>maxValuesPerNode</c>) because callers and downstream historian
|
||||||
/// adapters historically treat <c>maxEvents <= 0</c> as a sentinel meaning
|
/// adapters historically treat <c>maxEvents <= 0</c> as a sentinel meaning
|
||||||
/// "use the backend's default cap" (see <c>WonderwareHistorianClient</c> /
|
/// "use the backend's default cap" (see <c>WonderwareHistorianClient</c> /
|
||||||
/// <c>HistorianDataSource</c>). The asymmetry is intentional — Core.Abstractions-006.
|
/// <c>HistorianDataSource</c>). The asymmetry is intentional.
|
||||||
///
|
///
|
||||||
/// <b>Continuation contract when using the sentinel:</b> When <c>maxEvents <= 0</c>
|
/// <b>Continuation contract when using the sentinel:</b> When <c>maxEvents <= 0</c>
|
||||||
/// the backend applies its own cap. If the backend's cap truncates the result
|
/// the backend applies its own cap. If the backend's cap truncates the result
|
||||||
@@ -99,9 +99,9 @@ public interface IHistoryProvider
|
|||||||
/// <see cref="HistoricalEventsResult.ContinuationPoint"/> to a non-null token so
|
/// <see cref="HistoricalEventsResult.ContinuationPoint"/> to a non-null token so
|
||||||
/// callers can detect truncation and page. A null <c>ContinuationPoint</c> means all
|
/// callers can detect truncation and page. A null <c>ContinuationPoint</c> means all
|
||||||
/// matching events were returned — callers rely on this to decide whether to page.
|
/// matching events were returned — callers rely on this to decide whether to page.
|
||||||
/// (Core.Abstractions-009.)
|
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="cancellationToken">Request cancellation.</param>
|
/// <param name="cancellationToken">Request cancellation.</param>
|
||||||
|
/// <returns>A task that returns the historical events result containing matching event records and optional continuation point.</returns>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Default implementation throws. Drivers whose backend can serve historical events
|
/// Default implementation throws. Drivers whose backend can serve historical events
|
||||||
/// override: Galaxy (Wonderware Alarm & Events log) and the OPC UA Client driver
|
/// override: Galaxy (Wonderware Alarm & Events log) and the OPC UA Client driver
|
||||||
|
|||||||
@@ -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>The current connectivity status of each known host.</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>
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// <para>Multi-host drivers (Modbus with N PLCs, hypothetical AB CIP across a rack, etc.)
|
/// <para>Multi-host drivers (Modbus with N PLCs, hypothetical AB CIP across a rack, etc.)
|
||||||
/// implement this so the Phase 6.1 resilience pipeline can be keyed on
|
/// implement this so the resilience pipeline can be keyed on
|
||||||
/// <c>(DriverInstanceId, ResolvedHostName, DriverCapability)</c> per decision #144. One
|
/// <c>(DriverInstanceId, ResolvedHostName, DriverCapability)</c>. One
|
||||||
/// dead PLC behind a multi-device Modbus driver then trips only its own breaker; healthy
|
/// dead PLC behind a multi-device Modbus driver then trips only its own breaker; healthy
|
||||||
/// siblings keep serving.</para>
|
/// siblings keep serving.</para>
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Reads are idempotent — Polly retry pipelines can safely retry on transient failures
|
/// Reads are idempotent — Polly retry pipelines can safely retry on transient failures
|
||||||
/// (per <c>docs/v2/plan.md</c> decisions #34 and #44).
|
/// (per <c>docs/v2/plan.md</c>).
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public interface IReadable
|
public interface IReadable
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// symbol-version-changed) implement this to tell Core when to re-run discovery.
|
/// symbol-version-changed) implement this to tell Core when to re-run discovery.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Per <c>docs/v2/plan.md</c> decision #54 — static drivers (Modbus, S7, etc. whose tags
|
/// Per <c>docs/v2/plan.md</c> — static drivers (Modbus, S7, etc. whose tags
|
||||||
/// only change via a published config generation) don't implement <c>IRediscoverable</c>.
|
/// only change via a published config generation) don't implement <c>IRediscoverable</c>.
|
||||||
/// The Core just sees absence of the interface and skips change-detection wiring for that driver.
|
/// The Core just sees absence of the interface and skips change-detection wiring for that driver.
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ public enum DiscoveryRediscoverPolicy
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Driver capability for discovering tags and hierarchy from the backend.
|
/// Driver capability for discovering tags and hierarchy from the backend.
|
||||||
/// Streams discovered nodes into <see cref="IAddressSpaceBuilder"/> rather than
|
/// Streams discovered nodes into <see cref="IAddressSpaceBuilder"/> rather than
|
||||||
/// buffering the entire tree (decision #52 — supports incremental / large address spaces).
|
/// buffering the entire tree (supports incremental / large address spaces).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface ITagDiscovery
|
public interface ITagDiscovery
|
||||||
{
|
{
|
||||||
@@ -25,6 +25,7 @@ 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 operation.</returns>
|
||||||
Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken);
|
Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken);
|
||||||
|
|
||||||
/// <summary>Post-connect re-discovery policy. Default preserves the original retry-until-stable behavior.</summary>
|
/// <summary>Post-connect re-discovery policy. Default preserves the original retry-until-stable behavior.</summary>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
/// historian-only adapter, for example) can omit this.
|
/// historian-only adapter, for example) can omit this.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// Per <c>docs/v2/plan.md</c> decisions #44 + #45 — <b>writes are NOT auto-retried by default</b>.
|
/// Per <c>docs/v2/plan.md</c>, <b>writes are NOT auto-retried by default</b>.
|
||||||
/// A timeout may fire after the device already accepted the command; replaying non-idempotent
|
/// A timeout may fire after the device already accepted the command; replaying non-idempotent
|
||||||
/// field actions (pulses, alarm acks, recipe steps, counter increments) can cause duplicate
|
/// field actions (pulses, alarm acks, recipe steps, counter increments) can cause duplicate
|
||||||
/// operations. Per-tag opt-in via <c>Tag.WriteIdempotent = true</c> in the central config DB
|
/// operations. Per-tag opt-in via <c>Tag.WriteIdempotent = true</c> in the central config DB
|
||||||
@@ -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>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);
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Every OPC UA operation surface the Phase 6.2 authorization evaluator gates, per
|
/// Every OPC UA operation surface the Phase 6.2 authorization evaluator gates, per
|
||||||
/// <c>docs/v2/implementation/phase-6-2-authorization-runtime.md</c> §Stream C and
|
/// <c>docs/v2/implementation/phase-6-2-authorization-runtime.md</c> §Stream C.
|
||||||
/// decision #143. The evaluator maps each operation onto the corresponding
|
/// The evaluator maps each operation onto the corresponding
|
||||||
/// <c>NodePermissions</c> bit(s) to decide whether the calling session is allowed.
|
/// <c>NodePermissions</c> bit(s) to decide whether the calling session is allowed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user