Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd6c0b4d3d | |||
| c6d9b20d9f | |||
| 11de14d12e | |||
| aadbf49678 | |||
| 70d764b063 | |||
| 11bcff6af5 | |||
| de41963587 | |||
| a78b212c95 | |||
| 075c0e69da | |||
| b7f5e887ee | |||
| 933dd1a874 | |||
| c1619d95f5 | |||
| 8ba289f975 | |||
| d0777eee29 | |||
| 83856b7c27 | |||
| c4f315ec90 | |||
| 257caa7bd1 | |||
| 6534875476 | |||
| d2d7730830 | |||
| 2844180865 | |||
| d3ab2bfbaf | |||
| 88e773af36 | |||
| f35ebd7aaf | |||
| 0cbb82e466 | |||
| 7b6884031d | |||
| 7ff7a60ae0 | |||
| 8faa2bf23d | |||
| 2099713ed8 | |||
| c05ffc7b39 | |||
| 60017177cb | |||
| 26bae36f8b | |||
| 368390ea9d | |||
| 8f950722c6 |
@@ -79,11 +79,11 @@
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.15.3" />
|
||||
<PackageVersion Include="Polly.Core" Version="8.6.6" />
|
||||
<PackageVersion Include="S7netplus" Version="0.20.0" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.0" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog" Version="4.3.1" />
|
||||
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Extensions.Hosting" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Formatting.Compact" Version="3.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="9.0.0" />
|
||||
<PackageVersion Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
|
||||
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
|
||||
<PackageVersion Include="Shouldly" Version="4.3.0" />
|
||||
@@ -99,7 +99,15 @@
|
||||
<PackageVersion Include="ZB.MOM.WW.Health" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Health.Akka" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Health.EntityFrameworkCore" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Telemetry" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Telemetry.Serilog" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.MxGateway.Client" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.MxGateway.Contracts" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Configuration" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Auth.Abstractions" Version="0.1.1" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Auth.Ldap" Version="0.1.1" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Auth.AspNetCore" Version="0.1.1" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Audit" Version="0.1.0" />
|
||||
<PackageVersion Include="ZB.MOM.WW.Theme" Version="0.2.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<configuration>
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
|
||||
<add key="local-mxgw" value="./nuget-packages" />
|
||||
<add key="dohertj2-gitea" value="https://gitea.dohertylan.com/api/packages/dohertj2/nuget/index.json" />
|
||||
@@ -15,6 +16,13 @@
|
||||
<packageSource key="dohertj2-gitea">
|
||||
<package pattern="ZB.MOM.WW.Health" />
|
||||
<package pattern="ZB.MOM.WW.Health.*" />
|
||||
<package pattern="ZB.MOM.WW.Telemetry" />
|
||||
<package pattern="ZB.MOM.WW.Telemetry.*" />
|
||||
<package pattern="ZB.MOM.WW.Configuration" />
|
||||
<package pattern="ZB.MOM.WW.Auth" />
|
||||
<package pattern="ZB.MOM.WW.Auth.*" />
|
||||
<package pattern="ZB.MOM.WW.Audit" />
|
||||
<package pattern="ZB.MOM.WW.Theme" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
|
||||
@@ -65,7 +65,7 @@ Running record of v2 dev services on the Windows dev VM. Updated on every instal
|
||||
|---------|---------------------|---------|-----------|------------------------|---------------|--------|
|
||||
| **Central config DB** | Docker container `otopcua-mssql` on the Linux Docker host (image `mcr.microsoft.com/mssql/server:2022-latest`) | 16.0.4250.1 (RTM-CU24-GDR, KB5083252) | `10.100.0.35:14330` → `1433` (container) — port 14330 retained from the previous local-container setup so connection-string ports don't churn | User `sa` / Password `OtOpcUaDev_2026!` | Docker named volume `otopcua-mssql-data` on the Docker host | ✅ Running on Docker host (`/opt/otopcua-mssql/`) since 2026-04-28; carries `project=lmxopcua` label |
|
||||
| Dev Galaxy (AVEVA System Platform) | Local install on this dev box — full ArchestrA + Historian + OI-Server stack | v1 baseline | Local COM via MXAccess (`C:\Program Files (x86)\ArchestrA\Framework\bin\ArchestrA.MXAccess.dll`); Historian via `aaH*` services; SuiteLink via `slssvc` | Windows Auth | Galaxy repository DB `ZB` on local SQL Server (separate instance from `otopcua-mssql` — legacy v1 Galaxy DB, not related to v2 config DB) | ✅ **Fully available — Phase 2 lift unblocked.** 27 ArchestrA / AVEVA / Wonderware services running incl. `aaBootstrap`, `aaGR` (Galaxy Repository), `aaLogger`, `aaUserValidator`, `aaPim`, `ArchestrADataStore`, `AsbServiceManager`, `AutoBuild_Service`; full Historian set (`aahClientAccessPoint`, `aahGateway`, `aahInSight`, `aahSearchIndexer`, `aahSupervisor`, `InSQLStorage`, `InSQLConfiguration`, `InSQLEventSystem`, `InSQLIndexing`, `InSQLIOServer`, `InSQLManualStorage`, `InSQLSystemDriver`, `HistorianSearch-x64`); `slssvc` (Wonderware SuiteLink); `OI-Gateway` install present at `C:\Program Files (x86)\Wonderware\OI-Server\OI-Gateway\` (decision #142 AppServer-via-OI-Gateway smoke test now also unblocked) |
|
||||
| GLAuth (LDAP) | Local install at `C:\publish\glauth\` | v2.4.0 | `localhost:3893` (LDAP) / `3894` (LDAPS, disabled) | Direct-bind `cn={user},dc=lmxopcua,dc=local` per `auth.md`; users `readonly`/`writeop`/`writetune`/`writeconfig`/`alarmack`/`admin`/`serviceaccount` (passwords in `glauth.cfg` as SHA-256) | `C:\publish\glauth\` | ✅ Running (NSSM service `GLAuth`). Phase 1 Admin uses GroupToRole map `ReadOnly→ConfigViewer`, `WriteOperate→ConfigEditor`, `AlarmAck→FleetAdmin`. v2-rebrand to `dc=otopcua,dc=local` is a future cosmetic change |
|
||||
| GLAuth (LDAP) | Local install at `C:\publish\glauth\` | v2.4.0 | `localhost:3893` (LDAP) / `3894` (LDAPS, disabled) | Direct-bind `cn={user},dc=zb,dc=local` per `auth.md`; users `readonly`/`writeop`/`writetune`/`writeconfig`/`alarmack`/`admin`/`serviceaccount` (passwords in `glauth.cfg` as SHA-256) | `C:\publish\glauth\` | ✅ Running (NSSM service `GLAuth`). Phase 1 Admin uses GroupToRole map `ReadOnly→ConfigViewer`, `WriteOperate→ConfigEditor`, `AlarmAck→FleetAdmin`. Dev base DN unified to `dc=zb,dc=local` (Task 1.6) |
|
||||
| OPC Foundation reference server | Not yet built | — | `10.100.0.35:62541` (target) | `user1` / `password1` (reference-server defaults) | — | Pending (needed for Phase 5 OPC UA Client driver testing) |
|
||||
| FOCAS TCP stub | Not yet built | — | `10.100.0.35:8193` (target) | n/a | — | Pending (built in Phase 5; runs on Docker host) |
|
||||
| Modbus simulator (`otopcua-pymodbus:3.13.0`) | Docker compose at `/opt/otopcua-modbus/` on Docker host | pinned 3.13.0 | `10.100.0.35:5020` | n/a | n/a | Stack staged; bring up with `lmxopcua-fix up modbus <profile>` from this VM |
|
||||
|
||||
@@ -104,8 +104,8 @@ Anonymous OPC UA sessions are denied writes against `Operate`-classified tags by
|
||||
"Enabled": true,
|
||||
"Server": "localhost",
|
||||
"Port": 3893,
|
||||
"SearchBase": "dc=lmxopcua,dc=local",
|
||||
"ServiceAccountDn": "cn=serviceaccount,dc=lmxopcua,dc=local",
|
||||
"SearchBase": "dc=zb,dc=local",
|
||||
"ServiceAccountDn": "cn=serviceaccount,dc=zb,dc=local",
|
||||
"ServiceAccountPassword": "serviceaccount123",
|
||||
"GroupToRole": {
|
||||
"ReadOnly": "ReadOnly",
|
||||
|
||||
@@ -67,11 +67,13 @@ public abstract class CommandBase : ICommand
|
||||
/// Executes the command-specific workflow against the configured OPC UA endpoint.
|
||||
/// </summary>
|
||||
/// <param name="console">The CLI console used for output and cancellation handling.</param>
|
||||
/// <returns>A value task that represents the asynchronous command execution.</returns>
|
||||
public abstract ValueTask ExecuteAsync(IConsole console);
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="ConnectionSettings" /> from the common command options.
|
||||
/// </summary>
|
||||
/// <returns>A <see cref="ConnectionSettings"/> populated from the current command option values.</returns>
|
||||
protected ConnectionSettings CreateConnectionSettings()
|
||||
{
|
||||
var securityMode = SecurityModeMapper.FromString(Security);
|
||||
@@ -97,6 +99,7 @@ public abstract class CommandBase : ICommand
|
||||
/// and returns both the service and the connection info.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts connection setup for the command.</param>
|
||||
/// <returns>A tuple of the connected <see cref="IOpcUaClientService"/> and the resulting <see cref="ConnectionInfo"/>.</returns>
|
||||
protected async Task<(IOpcUaClientService Service, ConnectionInfo Info)> CreateServiceAndConnectAsync(
|
||||
CancellationToken ct)
|
||||
{
|
||||
|
||||
+1
-3
@@ -12,9 +12,7 @@ internal sealed class DefaultApplicationConfigurationFactory : IApplicationConfi
|
||||
{
|
||||
private static readonly ILogger Logger = Log.ForContext<DefaultApplicationConfigurationFactory>();
|
||||
|
||||
/// <summary>Creates an OPC UA application configuration from the provided connection settings.</summary>
|
||||
/// <param name="settings">The connection settings to use.</param>
|
||||
/// <param name="ct">Token to cancel the operation.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct)
|
||||
{
|
||||
// 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>();
|
||||
|
||||
/// <summary>Selects an OPC UA endpoint matching the requested security mode.</summary>
|
||||
/// <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>
|
||||
/// <inheritdoc />
|
||||
public EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
||||
MessageSecurityMode requestedMode)
|
||||
{
|
||||
@@ -53,6 +50,7 @@ internal static class EndpointSelector
|
||||
/// Thrown when no endpoint matches <paramref name="requestedMode"/>; the message lists the
|
||||
/// security mode + policy combinations the server returned so operators can diagnose mismatches.
|
||||
/// </exception>
|
||||
/// <returns>The best matching <see cref="EndpointDescription"/> with its URL rewritten to the requested host.</returns>
|
||||
public static EndpointDescription SelectBest(
|
||||
IEnumerable<EndpointDescription> allEndpoints,
|
||||
string endpointUrl,
|
||||
|
||||
+1
@@ -13,5 +13,6 @@ internal interface IApplicationConfigurationFactory
|
||||
/// </summary>
|
||||
/// <param name="settings">The connection settings to configure.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the validated <see cref="ApplicationConfiguration"/>.</returns>
|
||||
Task<ApplicationConfiguration> CreateAsync(ConnectionSettings settings, CancellationToken ct = default);
|
||||
}
|
||||
@@ -14,6 +14,7 @@ internal interface IEndpointDiscovery
|
||||
/// <param name="config">The OPC UA application configuration.</param>
|
||||
/// <param name="endpointUrl">The endpoint URL to discover.</param>
|
||||
/// <param name="requestedMode">The requested message security mode.</param>
|
||||
/// <returns>The best matching endpoint description for the requested security mode.</returns>
|
||||
EndpointDescription SelectEndpoint(ApplicationConfiguration config, string endpointUrl,
|
||||
MessageSecurityMode requestedMode);
|
||||
}
|
||||
@@ -58,6 +58,7 @@ internal interface ISessionAdapter : IDisposable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that resolves to the current <see cref="DataValue"/> for the node.</returns>
|
||||
Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -66,6 +67,7 @@ internal interface ISessionAdapter : IDisposable
|
||||
/// <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="ct">The cancellation token that aborts the write if the client cancels the request.</param>
|
||||
/// <returns>A task that resolves to the OPC UA <see cref="StatusCode"/> for the write operation.</returns>
|
||||
Task<StatusCode> WriteValueAsync(NodeId nodeId, DataValue value, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -75,6 +77,7 @@ internal interface ISessionAdapter : IDisposable
|
||||
/// <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="ct">The cancellation token that aborts the browse request.</param>
|
||||
/// <returns>A task that resolves to a tuple of an optional continuation point and the returned references.</returns>
|
||||
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseAsync(
|
||||
NodeId nodeId, uint nodeClassMask = 0, CancellationToken ct = default);
|
||||
|
||||
@@ -83,6 +86,7 @@ internal interface ISessionAdapter : IDisposable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that resolves to a tuple of an optional next continuation point and the returned references.</returns>
|
||||
Task<(byte[]? ContinuationPoint, ReferenceDescriptionCollection References)> BrowseNextAsync(
|
||||
byte[] continuationPoint, CancellationToken ct = default);
|
||||
|
||||
@@ -91,6 +95,7 @@ internal interface ISessionAdapter : IDisposable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that resolves to <see langword="true"/> if the node has at least one child; otherwise <see langword="false"/>.</returns>
|
||||
Task<bool> HasChildrenAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -101,6 +106,7 @@ internal interface ISessionAdapter : IDisposable
|
||||
/// <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="ct">The cancellation token that aborts the history read.</param>
|
||||
/// <returns>A task that resolves to the ordered list of raw historical data values.</returns>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
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="intervalMs">The processing interval, in milliseconds, for each aggregate bucket.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the aggregate history read.</param>
|
||||
/// <returns>A task that resolves to the ordered list of processed aggregate data values.</returns>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
NodeId aggregateId, double intervalMs, CancellationToken ct = default);
|
||||
|
||||
@@ -121,6 +128,7 @@ internal interface ISessionAdapter : IDisposable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that resolves to the newly created <see cref="ISubscriptionAdapter"/>.</returns>
|
||||
Task<ISubscriptionAdapter> CreateSubscriptionAsync(int publishingIntervalMs, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -130,11 +138,13 @@ internal interface ISessionAdapter : IDisposable
|
||||
/// <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="ct">The cancellation token that aborts the method invocation.</param>
|
||||
/// <returns>A task that resolves to the list of output arguments returned by the method, or <see langword="null"/> if none.</returns>
|
||||
Task<IList<object>?> CallMethodAsync(NodeId objectId, NodeId methodId, object[] inputArguments, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Closes the underlying session gracefully before the adapter is disposed or replaced during failover.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ internal interface ISubscriptionAdapter : IDisposable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task RemoveMonitoredItemAsync(uint clientHandle, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -46,11 +47,13 @@ internal interface ISubscriptionAdapter : IDisposable
|
||||
/// Requests a condition refresh for this subscription.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Removes all monitored items and deletes the subscription.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public static class ClientStoragePaths
|
||||
/// one-shot legacy-folder migration before returning so callers that depend on this
|
||||
/// path (PKI store, settings file) find their existing state at the canonical name.
|
||||
/// </summary>
|
||||
/// <returns>The absolute path to the client's top-level folder under LocalApplicationData.</returns>
|
||||
public static string GetRoot()
|
||||
{
|
||||
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>
|
||||
/// <returns>The absolute path to the PKI store subfolder.</returns>
|
||||
public static string GetPkiPath() => Path.Combine(GetRoot(), "pki");
|
||||
|
||||
/// <summary>
|
||||
@@ -45,6 +47,7 @@ public static class ClientStoragePaths
|
||||
/// folder existed + was moved to canonical, false when no migration was needed or
|
||||
/// canonical was already present.
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> when the legacy folder was found and moved; <see langword="false"/> when no migration was needed.</returns>
|
||||
public static bool TryRunLegacyMigration()
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
|
||||
@@ -24,12 +24,14 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A <see cref="ConnectionInfo"/> describing the active session after a successful connect.</returns>
|
||||
Task<ConnectionInfo> ConnectAsync(ConnectionSettings settings, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Disconnects from the active OPC UA endpoint and tears down subscriptions owned by the client.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
@@ -37,6 +39,7 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="nodeId">The node whose value should be retrieved.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the read request.</param>
|
||||
/// <returns>The current <see cref="DataValue"/> including value, status code, and timestamps.</returns>
|
||||
Task<DataValue> ReadValueAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -45,6 +48,7 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// <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="ct">The cancellation token that aborts the write request.</param>
|
||||
/// <returns>The OPC UA <see cref="StatusCode"/> returned by the server for the write operation.</returns>
|
||||
Task<StatusCode> WriteValueAsync(NodeId nodeId, object value, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -52,6 +56,7 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>The list of child nodes discovered under the specified parent.</returns>
|
||||
Task<IReadOnlyList<BrowseResult>> BrowseAsync(NodeId? parentNodeId = null, CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -60,6 +65,7 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// <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="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);
|
||||
|
||||
/// <summary>
|
||||
@@ -67,6 +73,7 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task UnsubscribeAsync(NodeId nodeId, CancellationToken ct = default);
|
||||
|
||||
/// <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="intervalMs">The publishing interval in milliseconds for the alarm subscription.</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);
|
||||
|
||||
/// <summary>
|
||||
/// Removes the active alarm subscription.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
/// Requests retained alarm conditions again so a client can repopulate its alarm list after reconnecting.
|
||||
/// </summary>
|
||||
/// <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);
|
||||
|
||||
/// <summary>
|
||||
@@ -111,6 +121,7 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// <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="ct">The cancellation token that aborts the history read.</param>
|
||||
/// <returns>The raw historical <see cref="DataValue"/> samples in the requested range.</returns>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadRawAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
int maxValues = 1000, CancellationToken ct = default);
|
||||
|
||||
@@ -123,6 +134,7 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// <param name="aggregate">The aggregate function the operator selected for processed history.</param>
|
||||
/// <param name="intervalMs">The processing interval, in milliseconds, for each aggregate bucket.</param>
|
||||
/// <param name="ct">The cancellation token that aborts the processed history request.</param>
|
||||
/// <returns>The processed historical <see cref="DataValue"/> samples computed by the requested aggregate.</returns>
|
||||
Task<IReadOnlyList<DataValue>> HistoryReadAggregateAsync(NodeId nodeId, DateTime startTime, DateTime endTime,
|
||||
AggregateType aggregate, double intervalMs = 3600000, CancellationToken ct = default);
|
||||
|
||||
@@ -130,6 +142,7 @@ public interface IOpcUaClientService : IDisposable
|
||||
/// Reads redundancy status data such as redundancy mode, service level, and partner endpoint URIs.
|
||||
/// </summary>
|
||||
/// <param name="ct">The cancellation token that aborts redundancy inspection.</param>
|
||||
/// <returns>A <see cref="RedundancyInfo"/> snapshot containing redundancy mode, service level, and partner endpoint URIs.</returns>
|
||||
Task<RedundancyInfo> GetRedundancyInfoAsync(CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -73,13 +73,13 @@ public sealed class OpcUaClientService : IOpcUaClientService
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when subscribed node values change.</summary>
|
||||
public event EventHandler<DataChangedEventArgs>? DataChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when an alarm event is received from the server.</summary>
|
||||
public event EventHandler<AlarmEventArgs>? AlarmEvent;
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>Raised when the connection state changes.</summary>
|
||||
public event EventHandler<ConnectionStateChangedEventArgs>? ConnectionStateChanged;
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -7,8 +7,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||
/// </summary>
|
||||
public sealed class AvaloniaUiDispatcher : IUiDispatcher
|
||||
{
|
||||
/// <summary>Posts an action to the Avalonia UI thread for execution.</summary>
|
||||
/// <param name="action">The action to execute on the UI thread.</param>
|
||||
/// <inheritdoc />
|
||||
public void Post(Action action)
|
||||
{
|
||||
Dispatcher.UIThread.Post(action);
|
||||
|
||||
@@ -6,6 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||
public interface ISettingsService
|
||||
{
|
||||
/// <summary>Loads user settings from persistent storage.</summary>
|
||||
/// <returns>The persisted <see cref="UserSettings"/>, or a default instance if none are saved.</returns>
|
||||
UserSettings Load();
|
||||
/// <summary>Saves user settings to persistent storage.</summary>
|
||||
/// <param name="settings">The settings to save.</param>
|
||||
|
||||
@@ -19,8 +19,7 @@ public sealed class JsonSettingsService : ISettingsService
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
/// <summary>Loads user settings from the settings file.</summary>
|
||||
/// <returns>The loaded user settings, or a new default instance if load fails.</returns>
|
||||
/// <inheritdoc />
|
||||
public UserSettings Load()
|
||||
{
|
||||
try
|
||||
@@ -37,8 +36,7 @@ public sealed class JsonSettingsService : ISettingsService
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Saves user settings to the settings file.</summary>
|
||||
/// <param name="settings">The user settings to save.</param>
|
||||
/// <inheritdoc />
|
||||
public void Save(UserSettings settings)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -6,8 +6,7 @@ namespace ZB.MOM.WW.OtOpcUa.Client.UI.Services;
|
||||
/// </summary>
|
||||
public sealed class SynchronousUiDispatcher : IUiDispatcher
|
||||
{
|
||||
/// <summary>Executes the action synchronously on the calling thread.</summary>
|
||||
/// <param name="action">The action to execute.</param>
|
||||
/// <inheritdoc />
|
||||
public void Post(Action action)
|
||||
{
|
||||
action();
|
||||
|
||||
@@ -195,6 +195,7 @@ public partial class AlarmsViewModel : ObservableObject
|
||||
/// <summary>
|
||||
/// Returns the monitored node ID for persistence, or null if not subscribed.
|
||||
/// </summary>
|
||||
/// <returns>The monitored node ID string, or null if not currently subscribed.</returns>
|
||||
public string? GetAlarmSourceNodeId()
|
||||
{
|
||||
return IsSubscribed ? MonitoredNodeIdText : null;
|
||||
|
||||
@@ -30,6 +30,7 @@ public class BrowseTreeViewModel : ObservableObject
|
||||
/// <summary>
|
||||
/// Loads root nodes by browsing with a null parent.
|
||||
/// </summary>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task LoadRootsAsync()
|
||||
{
|
||||
var results = await _service.BrowseAsync();
|
||||
|
||||
@@ -143,6 +143,7 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task AddSubscriptionForNodeAsync(string nodeIdStr, int intervalMs = 1000)
|
||||
{
|
||||
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="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>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task AddSubscriptionRecursiveAsync(string nodeIdStr, string nodeClass, int intervalMs = 1000)
|
||||
{
|
||||
return AddSubscriptionRecursiveAsync(nodeIdStr, nodeClass, intervalMs, maxDepth: 10, currentDepth: 0);
|
||||
@@ -211,6 +213,7 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
/// <summary>
|
||||
/// Returns the node IDs of all active subscriptions for persistence.
|
||||
/// </summary>
|
||||
/// <returns>The list of node ID strings for all currently active subscriptions.</returns>
|
||||
public List<string> GetSubscribedNodeIds()
|
||||
{
|
||||
return ActiveSubscriptions.Select(s => s.NodeId).ToList();
|
||||
@@ -220,6 +223,7 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
/// Restores subscriptions from a saved list of node IDs.
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
foreach (var nodeId in nodeIds)
|
||||
@@ -232,6 +236,7 @@ public partial class SubscriptionsViewModel : ObservableObject
|
||||
/// </summary>
|
||||
/// <param name="nodeIdStr">The node ID the operator wants to write.</param>
|
||||
/// <param name="rawValue">The raw text value entered by the operator.</param>
|
||||
/// <returns>A tuple of (success flag, operator-readable message) describing the outcome of the write.</returns>
|
||||
public async Task<(bool Success, string Message)> ValidateAndWriteAsync(string nodeIdStr, string rawValue)
|
||||
{
|
||||
try
|
||||
|
||||
@@ -43,20 +43,16 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
_subscriber = system.ActorOf(Props.Create(() => new SubscriberActor(this)), "clusterroleinfo-subscriber");
|
||||
}
|
||||
|
||||
/// <summary>Gets the local cluster node identifier.</summary>
|
||||
/// <inheritdoc />
|
||||
public CommonsNodeId LocalNode => _localNode;
|
||||
|
||||
/// <summary>Gets the set of roles assigned to the local node.</summary>
|
||||
/// <inheritdoc />
|
||||
public IReadOnlySet<string> LocalRoles => _localRoles;
|
||||
|
||||
/// <summary>Checks if the local node has a specific role.</summary>
|
||||
/// <param name="role">The role name to check.</param>
|
||||
/// <returns>True if the local node has the specified role; otherwise false.</returns>
|
||||
/// <inheritdoc />
|
||||
public bool HasRole(string role) => _localRoles.Contains(role);
|
||||
|
||||
/// <summary>Gets all cluster members that have a specific role.</summary>
|
||||
/// <param name="role">The role name.</param>
|
||||
/// <returns>A read-only list of node IDs with the specified role.</returns>
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<CommonsNodeId> MembersWithRole(string role)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -68,9 +64,7 @@ public sealed class ClusterRoleInfo : IClusterRoleInfo, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the current leader node for a specific role.</summary>
|
||||
/// <param name="role">The role name.</param>
|
||||
/// <returns>The node ID of the current role leader, or null if no leader is elected.</returns>
|
||||
/// <inheritdoc />
|
||||
public CommonsNodeId? RoleLeader(string role)
|
||||
{
|
||||
lock (_lock)
|
||||
|
||||
@@ -9,6 +9,7 @@ public static class RoleParser
|
||||
|
||||
/// <summary>Parses a comma-separated string of role names into a validated array.</summary>
|
||||
/// <param name="raw">The raw role string to parse.</param>
|
||||
/// <returns>An array of validated, distinct, lower-cased role names; empty array when the input is null or whitespace.</returns>
|
||||
public static string[] Parse(string? raw)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(raw)) return Array.Empty<string>();
|
||||
|
||||
@@ -18,6 +18,7 @@ public static class ServiceCollectionExtensions
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to configure.</param>
|
||||
/// <param name="configuration">The application configuration containing cluster options.</param>
|
||||
/// <returns>The same <see cref="IServiceCollection"/> for chaining.</returns>
|
||||
public static IServiceCollection AddOtOpcUaCluster(this IServiceCollection services, IConfiguration configuration)
|
||||
{
|
||||
services.AddOptions<AkkaClusterOptions>()
|
||||
@@ -45,6 +46,7 @@ public static class ServiceCollectionExtensions
|
||||
/// </summary>
|
||||
/// <param name="builder">The Akka configuration builder to configure.</param>
|
||||
/// <param name="serviceProvider">The service provider for resolving cluster options.</param>
|
||||
/// <returns>The same <see cref="AkkaConfigurationBuilder"/> for chaining.</returns>
|
||||
public static AkkaConfigurationBuilder WithOtOpcUaClusterBootstrap(
|
||||
this AkkaConfigurationBuilder builder,
|
||||
IServiceProvider serviceProvider)
|
||||
|
||||
@@ -16,14 +16,22 @@ public interface IBrowseSession : IAsyncDisposable
|
||||
DateTime LastUsedUtc { get; }
|
||||
|
||||
/// <summary>Returns the top-level browse nodes.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the list of top-level browse nodes.</returns>
|
||||
Task<IReadOnlyList<BrowseNode>> RootAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Returns the direct children of the node identified by
|
||||
/// <paramref name="nodeId"/>.</summary>
|
||||
/// <param name="nodeId">The identifier of the node whose children to expand.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the list of direct child nodes.</returns>
|
||||
Task<IReadOnlyList<BrowseNode>> ExpandAsync(string nodeId, CancellationToken cancellationToken);
|
||||
|
||||
/// <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
|
||||
/// the attribute side-panel after the user selects an object.</summary>
|
||||
/// <param name="nodeId">The identifier of the node whose attributes to retrieve.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the list of attribute descriptors for the node.</returns>
|
||||
Task<IReadOnlyList<AttributeInfo>> AttributesAsync(string nodeId, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -15,5 +15,6 @@ public interface IDriverBrowser
|
||||
/// <param name="configJson">Driver options serialized as JSON; same shape the runtime
|
||||
/// driver would consume.</param>
|
||||
/// <param name="cancellationToken">Cancellation for the connect phase only.</param>
|
||||
/// <returns>A task containing the opened browse session.</returns>
|
||||
Task<IBrowseSession> OpenAsync(string configJson, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public interface IAlarmActorStateStore
|
||||
/// <summary>Saves the alarm actor state snapshot.</summary>
|
||||
/// <param name="snapshot">The state snapshot to persist.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -41,14 +42,10 @@ public sealed class NullAlarmActorStateStore : IAlarmActorStateStore
|
||||
{
|
||||
public static readonly NullAlarmActorStateStore Instance = new();
|
||||
private NullAlarmActorStateStore() { }
|
||||
/// <summary>Always returns null, indicating no persisted state.</summary>
|
||||
/// <param name="alarmId">The alarm identifier (unused).</param>
|
||||
/// <param name="ct">Cancellation token (unused).</param>
|
||||
/// <inheritdoc />
|
||||
public Task<AlarmActorStateSnapshot?> LoadAsync(string alarmId, CancellationToken ct) =>
|
||||
Task.FromResult<AlarmActorStateSnapshot?>(null);
|
||||
/// <summary>Completes immediately without persisting anything.</summary>
|
||||
/// <param name="snapshot">The state snapshot (ignored).</param>
|
||||
/// <param name="ct">Cancellation token (unused).</param>
|
||||
/// <inheritdoc />
|
||||
public Task SaveAsync(AlarmActorStateSnapshot snapshot, CancellationToken ct) =>
|
||||
Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -43,11 +43,7 @@ public sealed class NullVirtualTagEvaluator : IVirtualTagEvaluator
|
||||
{
|
||||
public static readonly NullVirtualTagEvaluator Instance = new();
|
||||
private NullVirtualTagEvaluator() { }
|
||||
/// <summary>Returns <see cref="VirtualTagEvalResult.NoChange"/> for every evaluation.</summary>
|
||||
/// <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>
|
||||
/// <inheritdoc />
|
||||
public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary<string, object?> dependencies)
|
||||
=> VirtualTagEvalResult.NoChange;
|
||||
}
|
||||
|
||||
@@ -23,5 +23,6 @@ public interface IAdminOperationsClient
|
||||
/// <typeparam name="T">Expected reply type.</typeparam>
|
||||
/// <param name="message">The message to send.</param>
|
||||
/// <param name="ct">Cancellation token (caller-controlled timeout).</param>
|
||||
/// <returns>A task that resolves to the reply of type <typeparamref name="T"/>.</returns>
|
||||
Task<T> AskAsync<T>(object message, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -11,5 +11,6 @@ public interface IFleetDiagnosticsClient
|
||||
/// <summary>Gets diagnostics for the specified node.</summary>
|
||||
/// <param name="nodeId">The node ID to retrieve diagnostics for.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that resolves to the diagnostics snapshot for the specified node.</returns>
|
||||
Task<NodeDiagnosticsSnapshot> GetDiagnosticsAsync(NodeId nodeId, CancellationToken ct);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Commons.Messages.Audit;
|
||||
|
||||
/// <summary>
|
||||
/// Cluster-broadcast audit event consumed by the <c>AuditWriterActor</c> singleton, which
|
||||
/// batches and idempotently inserts into <c>ConfigAuditLog</c>.
|
||||
/// </summary>
|
||||
public sealed record AuditEvent(
|
||||
Guid EventId,
|
||||
string Category,
|
||||
string Action,
|
||||
string Actor,
|
||||
DateTime OccurredAtUtc,
|
||||
string? DetailsJson,
|
||||
NodeId SourceNode,
|
||||
CorrelationId CorrelationId);
|
||||
@@ -69,6 +69,7 @@ public static class OtOpcUaTelemetry
|
||||
/// null when no listener is attached so the call site stays cheap on undecorated builds.
|
||||
/// </summary>
|
||||
/// <param name="deploymentId">The deployment identifier to tag the span with.</param>
|
||||
/// <returns>The started <see cref="Activity"/>, or null when no listener is attached.</returns>
|
||||
public static Activity? StartDeployApplySpan(string deploymentId)
|
||||
{
|
||||
var activity = ActivitySource.StartActivity("otopcua.deploy.apply", ActivityKind.Internal);
|
||||
@@ -77,6 +78,7 @@ public static class OtOpcUaTelemetry
|
||||
}
|
||||
|
||||
/// <summary>Span wrapping a full OPC UA address-space rebuild (Phase7 plan → apply).</summary>
|
||||
/// <returns>The started <see cref="Activity"/>, or null when no listener is attached.</returns>
|
||||
public static Activity? StartAddressSpaceRebuildSpan()
|
||||
=> ActivitySource.StartActivity("otopcua.opcua.address_space_rebuild", ActivityKind.Internal);
|
||||
}
|
||||
|
||||
@@ -22,37 +22,22 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink
|
||||
public void SetSink(IOpcUaAddressSpaceSink? sink) =>
|
||||
_inner = sink ?? NullOpcUaAddressSpaceSink.Instance;
|
||||
|
||||
/// <summary>Writes a value to the OPC UA address space through the inner sink.</summary>
|
||||
/// <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>
|
||||
/// <inheritdoc />
|
||||
public void WriteValue(string nodeId, object? value, OpcUaQuality quality, DateTime sourceTimestampUtc)
|
||||
=> _inner.WriteValue(nodeId, value, quality, sourceTimestampUtc);
|
||||
|
||||
/// <summary>Writes an alarm state through the inner sink.</summary>
|
||||
/// <param name="alarmNodeId">The node ID of the alarm condition.</param>
|
||||
/// <param name="active">Whether the alarm is active.</param>
|
||||
/// <param name="acknowledged">Whether the alarm has been acknowledged.</param>
|
||||
/// <param name="sourceTimestampUtc">The source timestamp in UTC.</param>
|
||||
/// <inheritdoc />
|
||||
public void WriteAlarmState(string alarmNodeId, bool active, bool acknowledged, DateTime sourceTimestampUtc)
|
||||
=> _inner.WriteAlarmState(alarmNodeId, active, acknowledged, sourceTimestampUtc);
|
||||
|
||||
/// <summary>Ensures a folder exists in the address space through the inner sink.</summary>
|
||||
/// <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>
|
||||
/// <inheritdoc />
|
||||
public void EnsureFolder(string folderNodeId, string? parentNodeId, string displayName)
|
||||
=> _inner.EnsureFolder(folderNodeId, parentNodeId, displayName);
|
||||
|
||||
/// <summary>Ensures a variable exists in the address space through the inner sink.</summary>
|
||||
/// <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>
|
||||
/// <inheritdoc />
|
||||
public void EnsureVariable(string variableNodeId, string? parentFolderNodeId, string displayName, string dataType)
|
||||
=> _inner.EnsureVariable(variableNodeId, parentFolderNodeId, displayName, dataType);
|
||||
|
||||
/// <summary>Rebuilds the address space through the inner sink.</summary>
|
||||
/// <inheritdoc />
|
||||
public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,6 @@ public sealed class DeferredServiceLevelPublisher : IServiceLevelPublisher
|
||||
public void SetInner(IServiceLevelPublisher? inner) =>
|
||||
_inner = inner ?? NullServiceLevelPublisher.Instance;
|
||||
|
||||
/// <summary>Publishes a service level value to the inner publisher.</summary>
|
||||
/// <param name="serviceLevel">The service level to publish.</param>
|
||||
/// <inheritdoc />
|
||||
public void Publish(byte serviceLevel) => _inner.Publish(serviceLevel);
|
||||
}
|
||||
|
||||
@@ -3,15 +3,18 @@ namespace ZB.MOM.WW.OtOpcUa.Commons.Types;
|
||||
public readonly record struct CorrelationId(Guid Value)
|
||||
{
|
||||
/// <summary>Creates a new CorrelationId with a randomly generated GUID.</summary>
|
||||
/// <returns>A new <see cref="CorrelationId"/> backed by a random GUID.</returns>
|
||||
public static CorrelationId NewId() => new(Guid.NewGuid());
|
||||
/// <inheritdoc />
|
||||
public override string ToString() => Value.ToString("N");
|
||||
/// <summary>Parses a lowercase hex string without hyphens into a CorrelationId.</summary>
|
||||
/// <param name="s">The string to parse.</param>
|
||||
/// <returns>A <see cref="CorrelationId"/> parsed from the supplied string.</returns>
|
||||
public static CorrelationId Parse(string s) => new(Guid.ParseExact(s, "N"));
|
||||
/// <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="id">The resulting CorrelationId if parsing succeeds.</param>
|
||||
/// <returns><see langword="true"/> if parsing succeeded; otherwise <see langword="false"/>.</returns>
|
||||
public static bool TryParse(string? s, out CorrelationId id)
|
||||
{
|
||||
if (Guid.TryParseExact(s, "N", out var g)) { id = new CorrelationId(g); return true; }
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Akka"/>
|
||||
<PackageReference Include="ZB.MOM.WW.Audit"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -41,4 +41,10 @@ public sealed class ConfigAuditLog
|
||||
/// <summary>Correlation ID from <c>AuditEvent.CorrelationId</c> so an audit row joins to its
|
||||
/// originating request/workflow. Nullable for the same backfill reason as <see cref="EventId"/>.</summary>
|
||||
public Guid? CorrelationId { get; set; }
|
||||
|
||||
/// <summary>Normalized outcome from <c>AuditEvent.Outcome</c> (the canonical
|
||||
/// <c>ZB.MOM.WW.Audit.AuditOutcome</c>: <c>Success</c> | <c>Failure</c> | <c>Denied</c>),
|
||||
/// stored as its enum member name. Nullable so pre-Outcome rows backfill cleanly and the
|
||||
/// bespoke stored-procedure audit path (which does not derive an outcome) writes NULL.</summary>
|
||||
public string? Outcome { get; set; }
|
||||
}
|
||||
|
||||
@@ -7,20 +7,31 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Enums;
|
||||
/// <see cref="Entities.NodeAcl"/> joined against LDAP group memberships directly.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Per <c>docs/v2/plan.md</c> decision #150 the two concerns share zero runtime code path:
|
||||
/// the control plane (Admin UI) consumes <see cref="Entities.LdapGroupRoleMapping"/>; the
|
||||
/// 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
|
||||
/// admin-role claim path.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Task 1.7 standardized the member names on the canonical control-plane role vocabulary
|
||||
/// (<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>DriverOperator</c> string role likewise became <c>Operator</c>. These members persist
|
||||
/// as their string names (EF <c>HasConversion<string></c>); the rename is paired with
|
||||
/// a data migration (<c>CanonicalizeAdminRoles</c>) that rewrites existing rows. This is a
|
||||
/// rename, not a permission change — enforcement semantics are preserved.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public enum AdminRole
|
||||
{
|
||||
/// <summary>Read-only Admin UI access — can view cluster state, drafts, publish history.</summary>
|
||||
ConfigViewer,
|
||||
/// <summary>Read-only Admin UI access — can view cluster state, drafts, publish history. (Canonical: Viewer; was ConfigViewer.)</summary>
|
||||
Viewer,
|
||||
|
||||
/// <summary>Can author drafts + submit for publish.</summary>
|
||||
ConfigEditor,
|
||||
/// <summary>Can author drafts + submit for publish. (Canonical: Designer; was ConfigEditor.)</summary>
|
||||
Designer,
|
||||
|
||||
/// <summary>Full Admin UI privileges including publish + fleet-admin actions.</summary>
|
||||
FleetAdmin,
|
||||
/// <summary>Full Admin UI privileges including publish + fleet-admin actions. (Canonical: Administrator; was FleetAdmin.)</summary>
|
||||
Administrator,
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@ public interface ILocalConfigCache
|
||||
/// <summary>Stores a generation snapshot in the local cache.</summary>
|
||||
/// <param name="snapshot">The generation snapshot to store.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default);
|
||||
/// <summary>Removes old generations, keeping only the most recent N.</summary>
|
||||
/// <param name="clusterId">The cluster identifier.</param>
|
||||
/// <param name="keepLatest">The number of latest generations to keep.</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);
|
||||
}
|
||||
|
||||
@@ -45,9 +45,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the most recent snapshot for the specified cluster.</summary>
|
||||
/// <param name="clusterId">The cluster ID.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task<GenerationSnapshot?> GetMostRecentAsync(string clusterId, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -58,9 +56,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
return Task.FromResult<GenerationSnapshot?>(snapshot);
|
||||
}
|
||||
|
||||
/// <summary>Stores a snapshot in the cache.</summary>
|
||||
/// <param name="snapshot">The snapshot to store.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public async Task PutAsync(GenerationSnapshot snapshot, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
@@ -89,10 +85,7 @@ public sealed class LiteDbConfigCache : ILocalConfigCache, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Removes old generation snapshots, keeping only the latest ones.</summary>
|
||||
/// <param name="clusterId">The cluster ID.</param>
|
||||
/// <param name="keepLatest">Number of latest generations to keep.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <inheritdoc />
|
||||
public Task PruneOldGenerationsAsync(string clusterId, int keepLatest = 10, CancellationToken ct = default)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
+1755
File diff suppressed because it is too large
Load Diff
+39
@@ -0,0 +1,39 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Task 1.7 — canonicalizes the control-plane admin role VALUES persisted in the
|
||||
/// <c>LdapGroupRoleMapping.Role</c> column. The column stores the <c>AdminRole</c> enum
|
||||
/// member name as a string (EF <c>HasConversion<string></c>, <c>nvarchar(32)</c>);
|
||||
/// renaming the enum members (<c>ConfigViewer → Viewer</c>, <c>ConfigEditor → Designer</c>,
|
||||
/// <c>FleetAdmin → Administrator</c>) therefore requires rewriting existing rows so the C#
|
||||
/// enum and the stored strings stay in sync.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is a pure DATA migration: the schema (column type, length, indexes) is unchanged,
|
||||
/// so the model snapshot is byte-identical to the prior migration. The new canonical strings
|
||||
/// ("Viewer" = 6, "Designer" = 8, "Administrator" = 13 chars) all fit the existing
|
||||
/// <c>nvarchar(32)</c> column. Enforcement semantics are preserved — it is a rename only.
|
||||
/// </remarks>
|
||||
public partial class CanonicalizeAdminRoles : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'Viewer' WHERE [Role] = N'ConfigViewer';");
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'Designer' WHERE [Role] = N'ConfigEditor';");
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'Administrator' WHERE [Role] = N'FleetAdmin';");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'FleetAdmin' WHERE [Role] = N'Administrator';");
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'ConfigEditor' WHERE [Role] = N'Designer';");
|
||||
migrationBuilder.Sql("UPDATE [LdapGroupRoleMapping] SET [Role] = N'ConfigViewer' WHERE [Role] = N'Viewer';");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1759
File diff suppressed because it is too large
Load Diff
+35
@@ -0,0 +1,35 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
{
|
||||
/// <summary>
|
||||
/// Task 2.2 — adds the nullable <c>Outcome</c> column to <c>ConfigAuditLog</c> for the
|
||||
/// canonical <c>ZB.MOM.WW.Audit.AuditOutcome</c> (stored as its enum member name,
|
||||
/// <c>nvarchar(16)</c>, mirroring how <c>AdminRole</c> is persisted). Purely additive:
|
||||
/// nullable with no backfill, so existing rows and the bespoke stored-procedure audit
|
||||
/// path (which does not derive an outcome) keep writing NULL.
|
||||
/// </summary>
|
||||
public partial class AddConfigAuditLogOutcome : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Outcome",
|
||||
table: "ConfigAuditLog",
|
||||
type: "nvarchar(16)",
|
||||
maxLength: 16,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Outcome",
|
||||
table: "ConfigAuditLog");
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
@@ -186,6 +186,10 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Migrations
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<string>("Outcome")
|
||||
.HasMaxLength(16)
|
||||
.HasColumnType("nvarchar(16)");
|
||||
|
||||
b.Property<string>("Principal")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
|
||||
@@ -445,6 +445,9 @@ public sealed class OtOpcUaConfigDbContext(DbContextOptions<OtOpcUaConfigDbConte
|
||||
e.Property(x => x.DetailsJson).HasColumnType("nvarchar(max)");
|
||||
e.Property(x => x.EventId);
|
||||
e.Property(x => x.CorrelationId);
|
||||
// Stored as the AuditOutcome enum member name (mirrors AdminRole's string storage):
|
||||
// "Success" | "Failure" | "Denied" all fit nvarchar(16). Nullable for legacy + SP-path rows.
|
||||
e.Property(x => x.Outcome).HasMaxLength(16);
|
||||
|
||||
e.HasIndex(x => new { x.ClusterId, x.Timestamp })
|
||||
.IsDescending(false, true)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Configuration.Queries;
|
||||
|
||||
/// <summary>
|
||||
/// Shared query for the cluster-scoped audit view. Audit rows reach <c>ConfigAuditLog</c> by two
|
||||
/// paths that stamp different columns:
|
||||
/// <list type="bullet">
|
||||
/// <item>the bespoke stored-procedure path stamps <c>ClusterId</c> directly;</item>
|
||||
/// <item>the structured <c>AuditWriterActor</c> path stamps <c>NodeId</c> (leaving
|
||||
/// <c>ClusterId</c> null).</item>
|
||||
/// </list>
|
||||
/// A cluster-scoped view must surface both, so this query matches rows whose <c>ClusterId</c>
|
||||
/// equals the cluster <em>or</em> whose <c>NodeId</c> belongs to a node in the cluster
|
||||
/// (membership from <see cref="ClusterNode"/>: <c>NodeId → ClusterId</c>).
|
||||
/// </summary>
|
||||
public static class ClusterAuditQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the newest <paramref name="pageSize"/> audit rows visible for
|
||||
/// <paramref name="clusterId"/>, newest first. Executes one query to resolve the cluster's
|
||||
/// node IDs, then one filtered query against <c>ConfigAuditLog</c>.
|
||||
/// </summary>
|
||||
/// <param name="db">The config database context.</param>
|
||||
/// <param name="clusterId">The cluster whose audit rows to fetch.</param>
|
||||
/// <param name="pageSize">Maximum number of rows to return.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>The matching audit rows, newest first.</returns>
|
||||
public static async Task<List<ConfigAuditLog>> ForClusterAsync(
|
||||
OtOpcUaConfigDbContext db, string clusterId, int pageSize, CancellationToken ct = default)
|
||||
{
|
||||
var nodeIds = await db.ClusterNodes.AsNoTracking()
|
||||
.Where(n => n.ClusterId == clusterId)
|
||||
.Select(n => n.NodeId)
|
||||
.ToListAsync(ct);
|
||||
|
||||
return await db.ConfigAuditLogs.AsNoTracking()
|
||||
.Where(a => a.ClusterId == clusterId
|
||||
|| (a.ClusterId == null && a.NodeId != null && nodeIds.Contains(a.NodeId)))
|
||||
.OrderByDescending(a => a.Timestamp)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(ct);
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,13 @@ public interface ILdapGroupRoleMappingService
|
||||
/// </remarks>
|
||||
/// <param name="ldapGroups">The LDAP groups to search for.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task resolving to the list of mappings whose LDAP group matches any of the provided groups.</returns>
|
||||
Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
|
||||
IEnumerable<string> ldapGroups, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Enumerate every mapping; Admin UI listing only.</summary>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task resolving to all LDAP group role mappings.</returns>
|
||||
Task<IReadOnlyList<LdapGroupRoleMapping>> ListAllAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Create a new grant.</summary>
|
||||
@@ -39,11 +41,13 @@ public interface ILdapGroupRoleMappingService
|
||||
/// </exception>
|
||||
/// <param name="row">The LDAP group role mapping to create.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task resolving to the newly created <see cref="LdapGroupRoleMapping"/> with any DB-assigned values populated.</returns>
|
||||
Task<LdapGroupRoleMapping> CreateAsync(LdapGroupRoleMapping row, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Delete a mapping by its surrogate key.</summary>
|
||||
/// <param name="id">The unique identifier of the mapping to delete.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous delete operation.</returns>
|
||||
Task DeleteAsync(Guid id, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,10 +10,7 @@ namespace ZB.MOM.WW.OtOpcUa.Configuration.Services;
|
||||
/// </summary>
|
||||
public sealed class LdapGroupRoleMappingService(OtOpcUaConfigDbContext db) : ILdapGroupRoleMappingService
|
||||
{
|
||||
/// <summary>Gets LDAP group role mappings for the specified groups.</summary>
|
||||
/// <param name="ldapGroups">The LDAP group names to query.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>The matching role mappings.</returns>
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<LdapGroupRoleMapping>> GetByGroupsAsync(
|
||||
IEnumerable<string> ldapGroups, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -21,6 +21,7 @@ public static class DraftValidator
|
||||
/// Validates a draft snapshot and returns all validation errors found in a single pass.
|
||||
/// </summary>
|
||||
/// <param name="draft">The draft snapshot to validate.</param>
|
||||
/// <returns>A read-only list of all validation errors found; empty if the draft is valid.</returns>
|
||||
public static IReadOnlyList<ValidationError> Validate(DraftSnapshot draft)
|
||||
{
|
||||
var errors = new List<ValidationError>();
|
||||
@@ -147,6 +148,7 @@ public static class DraftValidator
|
||||
|
||||
/// <summary>Decision #125: EquipmentId = 'EQ-' + lowercase first 12 hex chars of the UUID.</summary>
|
||||
/// <param name="uuid">The equipment UUID to derive the ID from.</param>
|
||||
/// <returns>The derived equipment ID string in the form <c>EQ-xxxxxxxxxxxx</c>.</returns>
|
||||
public static string DeriveEquipmentId(Guid uuid) =>
|
||||
"EQ-" + uuid.ToString("N")[..12].ToLowerInvariant();
|
||||
|
||||
@@ -203,6 +205,7 @@ public static class DraftValidator
|
||||
/// </remarks>
|
||||
/// <param name="cluster">The server cluster to validate.</param>
|
||||
/// <param name="clusterNodes">The cluster nodes to validate against the cluster configuration.</param>
|
||||
/// <returns>A read-only list of all validation errors found; empty if the topology is valid.</returns>
|
||||
public static IReadOnlyList<ValidationError> ValidateClusterTopology(
|
||||
ServerCluster cluster,
|
||||
IReadOnlyList<ClusterNode> clusterNodes)
|
||||
|
||||
@@ -55,6 +55,7 @@ public sealed class DriverTypeRegistry
|
||||
|
||||
/// <summary>Look up a driver type by name. Throws if unknown.</summary>
|
||||
/// <param name="driverType">The driver type name to look up.</param>
|
||||
/// <returns>The <see cref="DriverTypeMetadata"/> registered for the specified type name.</returns>
|
||||
public DriverTypeMetadata Get(string driverType)
|
||||
{
|
||||
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>
|
||||
/// <param name="driverType">The driver type name to look up.</param>
|
||||
/// <returns>The matching <see cref="DriverTypeMetadata"/>, or <c>null</c> if not registered.</returns>
|
||||
public DriverTypeMetadata? TryGet(string driverType)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
||||
@@ -76,6 +78,7 @@ public sealed class DriverTypeRegistry
|
||||
}
|
||||
|
||||
/// <summary>Snapshot of all registered driver types.</summary>
|
||||
/// <returns>A read-only collection of all currently registered driver type metadata entries.</returns>
|
||||
public IReadOnlyCollection<DriverTypeMetadata> All() => _types.Values.ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ public interface IHistorianDataSource : IDisposable
|
||||
/// <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="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task resolving to a <see cref="HistoryReadResult"/> containing the raw samples.</returns>
|
||||
Task<HistoryReadResult> ReadRawAsync(
|
||||
string fullReference,
|
||||
DateTime startUtc,
|
||||
@@ -46,6 +47,7 @@ public interface IHistorianDataSource : IDisposable
|
||||
/// <param name="interval">The interval for bucketing samples.</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>
|
||||
/// <returns>A task resolving to a <see cref="HistoryReadResult"/> containing the processed interval samples.</returns>
|
||||
Task<HistoryReadResult> ReadProcessedAsync(
|
||||
string fullReference,
|
||||
DateTime startUtc,
|
||||
@@ -63,6 +65,7 @@ public interface IHistorianDataSource : IDisposable
|
||||
/// <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="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task resolving to a <see cref="HistoryReadResult"/> with one sample per requested timestamp.</returns>
|
||||
Task<HistoryReadResult> ReadAtTimeAsync(
|
||||
string fullReference,
|
||||
IReadOnlyList<DateTime> timestampsUtc,
|
||||
@@ -93,6 +96,7 @@ public interface IHistorianDataSource : IDisposable
|
||||
/// <param name="endUtc">The end of the time range in UTC.</param>
|
||||
/// <param name="maxEvents">The maximum number of events to return, or a non-positive value to use the default backend cap.</param>
|
||||
/// <param name="cancellationToken">A cancellation token that can be used to cancel the operation.</param>
|
||||
/// <returns>A task resolving to a <see cref="HistoricalEventsResult"/> containing historical alarm and event records.</returns>
|
||||
Task<HistoricalEventsResult> ReadEventsAsync(
|
||||
string? sourceName,
|
||||
DateTime startUtc,
|
||||
@@ -104,5 +108,6 @@ public interface IHistorianDataSource : IDisposable
|
||||
/// Point-in-time health snapshot for diagnostics and dashboards. Pure
|
||||
/// observation; never blocks on backend I/O.
|
||||
/// </summary>
|
||||
/// <returns>The current <see cref="HistorianHealthSnapshot"/> for this data source.</returns>
|
||||
HistorianHealthSnapshot GetHealthSnapshot();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ public interface IAddressSpaceBuilder
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A child builder scoped to inside this folder.</returns>
|
||||
IAddressSpaceBuilder Folder(string browseName, string displayName);
|
||||
|
||||
/// <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="displayName">Human-readable display name. May equal <paramref name="browseName"/>.</param>
|
||||
/// <param name="attributeInfo">Driver-side metadata for the variable.</param>
|
||||
/// <returns>An opaque handle for the registered variable.</returns>
|
||||
IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo);
|
||||
|
||||
/// <summary>
|
||||
@@ -56,6 +58,7 @@ public interface IVariableHandle
|
||||
/// <c>Acknowledge</c>, <c>Deactivate</c>).
|
||||
/// </summary>
|
||||
/// <param name="info">The alarm condition information.</param>
|
||||
/// <returns>A sink that receives alarm lifecycle transitions for this condition.</returns>
|
||||
IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ public interface IAlarmSource
|
||||
/// </summary>
|
||||
/// <param name="sourceNodeIds">The driver node IDs to subscribe to.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to an opaque <see cref="IAlarmSubscriptionHandle"/> for the new subscription.</returns>
|
||||
Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds,
|
||||
CancellationToken cancellationToken);
|
||||
@@ -20,11 +21,13 @@ public interface IAlarmSource
|
||||
/// <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="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken);
|
||||
|
||||
/// <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="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
@@ -23,6 +23,7 @@ public interface IDriver
|
||||
/// <summary>Initialize the driver from its <c>DriverConfig</c> JSON; open connections; prepare for first use.</summary>
|
||||
/// <param name="driverConfigJson">The driver configuration as JSON.</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);
|
||||
|
||||
/// <summary>
|
||||
@@ -37,13 +38,16 @@ public interface IDriver
|
||||
/// </remarks>
|
||||
/// <param name="driverConfigJson">The driver configuration as JSON.</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);
|
||||
|
||||
/// <summary>Stop the driver, close connections, release resources. Called on shutdown or driver removal.</summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task ShutdownAsync(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Current health snapshot, polled by Core for the status dashboard and ServiceLevel.</summary>
|
||||
/// <returns>The current driver health snapshot.</returns>
|
||||
DriverHealth GetHealth();
|
||||
|
||||
/// <summary>
|
||||
@@ -56,6 +60,7 @@ public interface IDriver
|
||||
/// allocation tracking". Tier C drivers (process-isolated) report through the same
|
||||
/// interface but the cache-flush is internal to their host.
|
||||
/// </remarks>
|
||||
/// <returns>The approximate driver-attributable memory footprint in bytes.</returns>
|
||||
long GetMemoryFootprint();
|
||||
|
||||
/// <summary>
|
||||
@@ -63,5 +68,6 @@ public interface IDriver
|
||||
/// Required-for-correctness state must NOT be flushed.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task FlushOptionalCachesAsync(CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -34,12 +34,8 @@ public sealed class NullDriverFactory : IDriverFactory
|
||||
public static readonly NullDriverFactory Instance = new();
|
||||
private NullDriverFactory() { }
|
||||
|
||||
/// <summary>Creates a driver (always returns null in this null implementation).</summary>
|
||||
/// <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>
|
||||
/// <inheritdoc />
|
||||
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>();
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ public interface IDriverHealthPublisher
|
||||
/// Publishes a health snapshot for one driver instance. Implementations must be
|
||||
/// non-blocking and tolerant of being called from any thread.
|
||||
/// </summary>
|
||||
/// <param name="clusterId">The cluster identifier the driver instance belongs to.</param>
|
||||
/// <param name="driverInstanceId">The unique identifier of the driver instance.</param>
|
||||
/// <param name="health">The current health state of the driver instance.</param>
|
||||
/// <param name="errorCount5Min">Number of errors recorded in the past 5 minutes.</param>
|
||||
void Publish(
|
||||
string clusterId,
|
||||
string driverInstanceId,
|
||||
|
||||
@@ -17,6 +17,10 @@ public interface IDriverProbe
|
||||
/// timeout cancellation. Never throw on connection failure; instead return a result
|
||||
/// with <c>Ok = false</c> + a message.
|
||||
/// </summary>
|
||||
/// <param name="configJson">Driver configuration JSON; same shape the runtime driver consumes.</param>
|
||||
/// <param name="timeout">Maximum duration for the probe attempt.</param>
|
||||
/// <param name="ct">Cancellation token for the probe operation.</param>
|
||||
/// <returns>A task containing the probe result with success status and optional latency.</returns>
|
||||
Task<DriverProbeResult> ProbeAsync(string configJson, TimeSpan timeout, CancellationToken ct);
|
||||
}
|
||||
|
||||
|
||||
@@ -22,5 +22,6 @@ public interface IDriverSupervisor
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
Task RecycleAsync(string reason, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ public interface IHistoryProvider
|
||||
/// <c>HistorianDataSource</c>). The asymmetry is intentional — Core.Abstractions-006.
|
||||
/// </param>
|
||||
/// <param name="cancellationToken">Request cancellation.</param>
|
||||
/// <returns>A task that resolves to the historical events result for the requested window.</returns>
|
||||
/// <remarks>
|
||||
/// Default implementation throws. Only drivers with an event historian (Galaxy via the
|
||||
/// Wonderware Alarm & Events log) override. Modbus / the OPC UA Client driver stay
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface IHostConnectivityProbe
|
||||
/// Snapshot of host-level connectivity. The Core uses this to drive Bad-quality
|
||||
/// fan-out scoped to the affected host's subtree (not the whole driver namespace).
|
||||
/// </summary>
|
||||
/// <returns>A snapshot list of per-host connectivity statuses.</returns>
|
||||
IReadOnlyList<HostConnectivityStatus> GetHostStatuses();
|
||||
|
||||
/// <summary>Fired when a host transitions Running ↔ Stopped (or similar lifecycle change).</summary>
|
||||
|
||||
@@ -13,5 +13,6 @@ public interface ITagDiscovery
|
||||
/// </summary>
|
||||
/// <param name="builder">The address space builder to stream discovered nodes into.</param>
|
||||
/// <param name="cancellationToken">A cancellation token for the discovery operation.</param>
|
||||
/// <returns>A task that represents the asynchronous discovery operation.</returns>
|
||||
Task DiscoverAsync(IAddressSpaceBuilder builder, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ public interface IWritable
|
||||
/// </summary>
|
||||
/// <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>
|
||||
/// <returns>A task that resolves to one <see cref="WriteResult"/> per requested write, in the same order.</returns>
|
||||
Task<IReadOnlyList<WriteResult>> WriteAsync(
|
||||
IReadOnlyList<WriteRequest> writes,
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
@@ -41,6 +41,7 @@ public sealed class PollGroupEngine : IAsyncDisposable
|
||||
/// <summary>Default floor for publishing intervals — matches the Modbus 100 ms cap.</summary>
|
||||
public static readonly TimeSpan DefaultMinInterval = TimeSpan.FromMilliseconds(100);
|
||||
|
||||
/// <summary>Initializes a new poll-group engine with the supplied reader, change callback, interval floor, and optional error sink.</summary>
|
||||
/// <param name="reader">Driver-supplied batch reader; snapshots MUST be returned in the same
|
||||
/// order as the input references.</param>
|
||||
/// <param name="onChange">Callback invoked per changed tag — the driver forwards to its own
|
||||
@@ -68,6 +69,7 @@ public sealed class PollGroupEngine : IAsyncDisposable
|
||||
/// <summary>Register a new polled subscription and start its background loop.</summary>
|
||||
/// <param name="fullReferences">The list of tag references to poll.</param>
|
||||
/// <param name="publishingInterval">The desired polling interval; will be clamped to the configured minimum.</param>
|
||||
/// <returns>A subscription handle that can be passed to <see cref="Unsubscribe"/> to cancel the loop.</returns>
|
||||
public ISubscriptionHandle Subscribe(IReadOnlyList<string> fullReferences, TimeSpan publishingInterval)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(fullReferences);
|
||||
@@ -207,6 +209,7 @@ public sealed class PollGroupEngine : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>Cancel every active subscription and await all loop tasks. Idempotent.</summary>
|
||||
/// <returns>A value task that represents the asynchronous dispose operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// Cancel all loops first so they can all start winding down in parallel.
|
||||
@@ -253,7 +256,7 @@ public sealed class PollGroupEngine : IAsyncDisposable
|
||||
|
||||
private sealed record PollSubscriptionHandle(long Id) : ISubscriptionHandle
|
||||
{
|
||||
/// <summary>Gets a diagnostic identifier for this subscription.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => $"poll-sub-{Id}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,9 +26,11 @@ public interface IAlarmHistorianSink
|
||||
/// <summary>Durably enqueue the event. Returns as soon as the queue row is committed.</summary>
|
||||
/// <param name="evt">The alarm historian event to enqueue.</param>
|
||||
/// <param name="cancellationToken">A cancellation token for async operations.</param>
|
||||
/// <returns>A task that represents the asynchronous enqueue operation.</returns>
|
||||
Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>Snapshot of current queue depth + drain health.</summary>
|
||||
/// <returns>A snapshot of the current queue depth and drain state.</returns>
|
||||
HistorianSinkStatus GetStatus();
|
||||
}
|
||||
|
||||
@@ -97,6 +99,7 @@ public interface IAlarmHistorianWriter
|
||||
/// <summary>Push a batch of events to the historian. Returns one outcome per event, same order.</summary>
|
||||
/// <param name="batch">The batch of alarm historian events to write.</param>
|
||||
/// <param name="cancellationToken">A cancellation token for async operations.</param>
|
||||
/// <returns>A task that resolves to one write outcome per event, in the same order as the batch.</returns>
|
||||
Task<IReadOnlyList<HistorianWriteOutcome>> WriteBatchAsync(
|
||||
IReadOnlyList<AlarmHistorianEvent> batch, CancellationToken cancellationToken);
|
||||
}
|
||||
|
||||
@@ -255,6 +255,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
/// </remarks>
|
||||
/// <param name="evt">The alarm historian event to enqueue.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task EnqueueAsync(AlarmHistorianEvent evt, CancellationToken cancellationToken)
|
||||
{
|
||||
if (evt is null) throw new ArgumentNullException(nameof(evt));
|
||||
@@ -345,6 +346,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
/// connections per tick, each paying the open + PRAGMA cost.
|
||||
/// </remarks>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task DrainOnceAsync(CancellationToken ct)
|
||||
{
|
||||
if (_disposed) return;
|
||||
@@ -490,7 +492,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Gets the current status of the historian sink including queue depth and drain state.</summary>
|
||||
/// <inheritdoc />
|
||||
public HistorianSinkStatus GetStatus()
|
||||
{
|
||||
// Core.AlarmHistorian-008: read the non-dead-lettered count from the in-memory
|
||||
@@ -534,6 +536,7 @@ public sealed class SqliteStoreAndForwardSink : IAlarmHistorianSink, IDisposable
|
||||
}
|
||||
|
||||
/// <summary>Operator action from Admin UI — retry every dead-lettered row. Non-cascading: they rejoin the regular queue + get a fresh backoff.</summary>
|
||||
/// <returns>The number of rows moved back to the active queue.</returns>
|
||||
public int RetryDeadLettered()
|
||||
{
|
||||
using var conn = OpenConnection();
|
||||
|
||||
@@ -97,6 +97,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// copy under the gate. (Core.ScriptedAlarms-013.)
|
||||
/// </remarks>
|
||||
/// <param name="alarmId">The alarm identifier to look up.</param>
|
||||
/// <returns>The live read-cache dictionary for the alarm, or <see langword="null"/> if not yet allocated.</returns>
|
||||
internal IReadOnlyDictionary<string, DataValueSnapshot>? TryGetScratchReadCacheForTest(string alarmId)
|
||||
=> _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.ReadCache : null;
|
||||
|
||||
@@ -113,6 +114,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// (Core.ScriptedAlarms-013.)
|
||||
/// </remarks>
|
||||
/// <param name="alarmId">The alarm identifier to look up.</param>
|
||||
/// <returns>The reusable <see cref="AlarmPredicateContext"/> for the alarm, or <see langword="null"/> if not yet allocated.</returns>
|
||||
internal AlarmPredicateContext? TryGetScratchContextForTest(string alarmId)
|
||||
=> _scratchByAlarmId.TryGetValue(alarmId, out var s) ? s.Context : null;
|
||||
private readonly ConcurrentDictionary<string, DataValueSnapshot> _valueCache
|
||||
@@ -175,6 +177,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="definitions">The alarm definitions to load.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task LoadAsync(IReadOnlyList<ScriptedAlarmDefinition> definitions, CancellationToken ct)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(ScriptedAlarmEngine));
|
||||
@@ -306,10 +309,12 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// unknown alarm. Mainly used for diagnostics + the Admin UI status page.
|
||||
/// </summary>
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <returns>The current <see cref="AlarmConditionState"/> for the alarm, or <see langword="null"/> if the alarm is unknown.</returns>
|
||||
public AlarmConditionState? GetState(string alarmId)
|
||||
=> _alarms.TryGetValue(alarmId, out var s) ? s.Condition : null;
|
||||
|
||||
/// <summary>Gets the current persisted state for all loaded alarms.</summary>
|
||||
/// <returns>A snapshot collection of all current alarm condition states.</returns>
|
||||
public IReadOnlyCollection<AlarmConditionState> GetAllStates()
|
||||
=> _alarms.Values.Select(a => a.Condition).ToArray();
|
||||
|
||||
@@ -318,6 +323,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="user">The user performing the acknowledgment.</param>
|
||||
/// <param name="comment">An optional comment to attach to the acknowledgment.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task AcknowledgeAsync(string alarmId, string user, string? comment, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAcknowledge(cur, user, comment, _clock()));
|
||||
|
||||
@@ -326,6 +332,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="user">The user performing the confirmation.</param>
|
||||
/// <param name="comment">An optional comment to attach to the confirmation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task ConfirmAsync(string alarmId, string user, string? comment, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyConfirm(cur, user, comment, _clock()));
|
||||
|
||||
@@ -333,6 +340,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="user">The user performing the shelve operation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task OneShotShelveAsync(string alarmId, string user, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyOneShotShelve(cur, user, _clock()));
|
||||
|
||||
@@ -341,6 +349,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="user">The user performing the shelve operation.</param>
|
||||
/// <param name="unshelveAtUtc">The UTC time at which the shelve will automatically expire.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task TimedShelveAsync(string alarmId, string user, DateTime unshelveAtUtc, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyTimedShelve(cur, user, unshelveAtUtc, _clock()));
|
||||
|
||||
@@ -348,6 +357,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="user">The user performing the unshelve operation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task UnshelveAsync(string alarmId, string user, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyUnshelve(cur, user, _clock()));
|
||||
|
||||
@@ -355,6 +365,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="user">The user performing the enable operation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task EnableAsync(string alarmId, string user, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyEnable(cur, user, _clock()));
|
||||
|
||||
@@ -362,6 +373,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="alarmId">The alarm identifier.</param>
|
||||
/// <param name="user">The user performing the disable operation.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task DisableAsync(string alarmId, string user, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyDisable(cur, user, _clock()));
|
||||
|
||||
@@ -370,6 +382,7 @@ public sealed class ScriptedAlarmEngine : IDisposable
|
||||
/// <param name="user">The user adding the comment.</param>
|
||||
/// <param name="text">The comment text.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task AddCommentAsync(string alarmId, string user, string text, CancellationToken ct)
|
||||
=> ApplyAsync(alarmId, ct, cur => Part9StateMachine.ApplyAddComment(cur, user, text, _clock()));
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ public static class DependencyExtractor
|
||||
/// paths, or a list of rejection messages if non-literal paths were used.
|
||||
/// </summary>
|
||||
/// <param name="scriptSource">The script source code to analyze.</param>
|
||||
/// <returns>The extracted dependency paths, or rejection messages for unsupported patterns.</returns>
|
||||
public static DependencyExtractionResult Extract(string scriptSource)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(scriptSource))
|
||||
|
||||
@@ -41,6 +41,7 @@ public abstract class ScriptContext
|
||||
/// right upstream tags at load time.
|
||||
/// </remarks>
|
||||
/// <param name="path">The literal tag path to read.</param>
|
||||
/// <returns>The current <see cref="DataValueSnapshot"/> for the tag, including value, quality, and timestamp.</returns>
|
||||
public abstract DataValueSnapshot GetTag(string path);
|
||||
|
||||
/// <summary>
|
||||
@@ -81,6 +82,7 @@ public abstract class ScriptContext
|
||||
/// <param name="current">The current value to check.</param>
|
||||
/// <param name="previous">The previous value to compare against.</param>
|
||||
/// <param name="tolerance">The minimum difference threshold for a change to be detected.</param>
|
||||
/// <returns><see langword="true"/> when the absolute difference between current and previous exceeds tolerance.</returns>
|
||||
public static bool Deadband(double current, double previous, double tolerance)
|
||||
=> Math.Abs(current - previous) > tolerance;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ public sealed class ScriptEvaluator<TContext, TResult> : IDisposable
|
||||
|
||||
/// <summary>Compiles user script source into an evaluator.</summary>
|
||||
/// <param name="scriptSource">The user script source code to compile.</param>
|
||||
/// <returns>A compiled <see cref="ScriptEvaluator{TContext, TResult}"/> ready to invoke.</returns>
|
||||
public static ScriptEvaluator<TContext, TResult> Compile(string scriptSource)
|
||||
{
|
||||
if (scriptSource is null) throw new ArgumentNullException(nameof(scriptSource));
|
||||
@@ -173,6 +174,7 @@ public sealed class ScriptEvaluator<TContext, TResult> : IDisposable
|
||||
/// <summary>Runs the script against an already-constructed context.</summary>
|
||||
/// <param name="context">The script context.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that resolves to the script's return value.</returns>
|
||||
public Task<TResult> RunAsync(TContext context, CancellationToken ct = default)
|
||||
{
|
||||
if (_disposed) throw new ObjectDisposedException(nameof(ScriptEvaluator<TContext, TResult>));
|
||||
|
||||
@@ -43,6 +43,7 @@ public static class ScriptSandbox
|
||||
/// to resolve <c>ctx.GetTag(...)</c> calls.
|
||||
/// </summary>
|
||||
/// <param name="contextType">The concrete script context type to use for compilation.</param>
|
||||
/// <returns>The sandbox configuration for compiling scripts with the given context type.</returns>
|
||||
public static SandboxConfig Build(Type contextType)
|
||||
{
|
||||
if (contextType is null) throw new ArgumentNullException(nameof(contextType));
|
||||
|
||||
@@ -156,6 +156,7 @@ public sealed class DependencyGraph
|
||||
/// dependencies. Throws <see cref="DependencyCycleException"/> if any cycle
|
||||
/// exists. Implemented via Kahn's algorithm.
|
||||
/// </summary>
|
||||
/// <returns>A list of node IDs in topological evaluation order.</returns>
|
||||
public IReadOnlyList<string> TopologicalSort()
|
||||
{
|
||||
// Kahn's framing: edge u -> v means "u must come before v". For dependencies,
|
||||
@@ -205,6 +206,7 @@ public sealed class DependencyGraph
|
||||
/// Empty list means the graph is a DAG. Useful for surfacing every cycle in one
|
||||
/// rejection pass so operators see all of them, not just one at a time.
|
||||
/// </summary>
|
||||
/// <returns>A list of strongly-connected components that form cycles; empty if the graph is acyclic.</returns>
|
||||
public IReadOnlyList<IReadOnlyList<string>> DetectCycles()
|
||||
{
|
||||
// Iterative Tarjan's SCC. Avoids recursion so deep graphs don't StackOverflow.
|
||||
|
||||
@@ -30,6 +30,7 @@ public interface ITagUpstreamSource
|
||||
/// when the path isn't configured.
|
||||
/// </summary>
|
||||
/// <param name="path">The tag path to read.</param>
|
||||
/// <returns>The last-known value and quality snapshot for the tag.</returns>
|
||||
DataValueSnapshot ReadTag(string path);
|
||||
|
||||
/// <summary>
|
||||
@@ -40,5 +41,6 @@ public interface ITagUpstreamSource
|
||||
/// </summary>
|
||||
/// <param name="path">The tag path to subscribe to.</param>
|
||||
/// <param name="observer">The callback to invoke when the value changes.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> that cancels the subscription when disposed.</returns>
|
||||
IDisposable SubscribeTag(string path, Action<string, DataValueSnapshot> observer);
|
||||
}
|
||||
|
||||
@@ -198,6 +198,7 @@ public sealed class VirtualTagEngine : IDisposable
|
||||
/// default. Also called after a config reload.
|
||||
/// </summary>
|
||||
/// <param name="ct">Cancellation token to stop evaluation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task EvaluateAllAsync(CancellationToken ct = default)
|
||||
{
|
||||
EnsureLoaded();
|
||||
@@ -212,6 +213,7 @@ public sealed class VirtualTagEngine : IDisposable
|
||||
/// <summary>Evaluate a single tag — used by the timer trigger + test hooks.</summary>
|
||||
/// <param name="path">Path of the virtual tag to evaluate.</param>
|
||||
/// <param name="ct">Cancellation token to stop evaluation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public Task EvaluateOneAsync(string path, CancellationToken ct = default)
|
||||
{
|
||||
EnsureLoaded();
|
||||
@@ -226,6 +228,7 @@ public sealed class VirtualTagEngine : IDisposable
|
||||
/// evaluation result.
|
||||
/// </summary>
|
||||
/// <param name="path">Path of the tag to read.</param>
|
||||
/// <returns>The most recently cached value and quality for the tag path.</returns>
|
||||
public DataValueSnapshot Read(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
@@ -242,6 +245,7 @@ public sealed class VirtualTagEngine : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="path">Path of the tag to subscribe to.</param>
|
||||
/// <param name="observer">Callback invoked with the tag path and new value on each evaluation.</param>
|
||||
/// <returns>An <see cref="IDisposable"/> that cancels the subscription when disposed.</returns>
|
||||
public IDisposable Subscribe(string path, Action<string, DataValueSnapshot> observer)
|
||||
{
|
||||
// Race-safe pattern paired with Unsub.Dispose: if Unsub.Dispose removed the
|
||||
|
||||
@@ -19,6 +19,7 @@ public sealed record AuthorizationDecision(
|
||||
public bool IsAllowed => Verdict == AuthorizationVerdict.Allow;
|
||||
|
||||
/// <summary>Convenience constructor for the common "no grants matched" outcome.</summary>
|
||||
/// <returns>An <see cref="AuthorizationDecision"/> with <see cref="AuthorizationVerdict.NotGranted"/> and empty provenance.</returns>
|
||||
public static AuthorizationDecision NotGranted() => new(AuthorizationVerdict.NotGranted, []);
|
||||
|
||||
/// <summary>Allow with the list of grants that matched.</summary>
|
||||
|
||||
@@ -22,5 +22,6 @@ public interface IPermissionEvaluator
|
||||
/// <param name="session">The user session containing resolved LDAP groups and roles.</param>
|
||||
/// <param name="operation">The OPC UA operation being requested.</param>
|
||||
/// <param name="scope">The node address scope being accessed.</param>
|
||||
/// <returns>An <see cref="AuthorizationDecision"/> indicating whether the operation is allowed.</returns>
|
||||
AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ public sealed class PermissionTrie
|
||||
/// </summary>
|
||||
/// <param name="scope">The node scope to match permissions for.</param>
|
||||
/// <param name="ldapGroups">The user's LDAP group memberships.</param>
|
||||
/// <returns>The list of grants that apply to the given scope for any of the session's LDAP groups.</returns>
|
||||
public IReadOnlyList<MatchedGrant> CollectMatches(NodeScope scope, IEnumerable<string> ldapGroups)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(scope);
|
||||
|
||||
@@ -41,6 +41,7 @@ public static class PermissionTrieBuilder
|
||||
/// Core-011 production hazard. The callback fires only when <paramref name="scopePaths"/>
|
||||
/// is non-null (a null lookup is the explicit deterministic-test fallback mode).
|
||||
/// </param>
|
||||
/// <returns>An immutable <see cref="PermissionTrie"/> for the given cluster and generation.</returns>
|
||||
public static PermissionTrie Build(
|
||||
string clusterId,
|
||||
long generationId,
|
||||
|
||||
@@ -34,6 +34,7 @@ public sealed class PermissionTrieCache
|
||||
|
||||
/// <summary>Get the current-generation trie for a cluster; null when nothing installed.</summary>
|
||||
/// <param name="clusterId">The cluster identifier.</param>
|
||||
/// <returns>The current-generation trie, or null if nothing is installed for the cluster.</returns>
|
||||
public PermissionTrie? GetTrie(string clusterId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(clusterId);
|
||||
@@ -43,6 +44,7 @@ public sealed class PermissionTrieCache
|
||||
/// <summary>Get a specific (cluster, generation) trie; null if that pair isn't cached.</summary>
|
||||
/// <param name="clusterId">The cluster identifier.</param>
|
||||
/// <param name="generationId">The generation identifier.</param>
|
||||
/// <returns>The trie for the specified cluster and generation, or null if not cached.</returns>
|
||||
public PermissionTrie? GetTrie(string clusterId, long generationId)
|
||||
{
|
||||
if (!_byCluster.TryGetValue(clusterId, out var entry)) return null;
|
||||
@@ -51,6 +53,7 @@ public sealed class PermissionTrieCache
|
||||
|
||||
/// <summary>The generation id the <see cref="GetTrie(string)"/> shortcut currently serves for a cluster.</summary>
|
||||
/// <param name="clusterId">The cluster identifier.</param>
|
||||
/// <returns>The current generation ID, or null if no trie is installed for the cluster.</returns>
|
||||
public long? CurrentGenerationId(string clusterId)
|
||||
=> _byCluster.TryGetValue(clusterId, out var entry) ? entry.Current.GenerationId : null;
|
||||
|
||||
@@ -111,11 +114,13 @@ public sealed class PermissionTrieCache
|
||||
|
||||
/// <summary>Creates a cluster entry from a single trie.</summary>
|
||||
/// <param name="trie">The permission trie to create the entry from.</param>
|
||||
/// <returns>A new <see cref="ClusterEntry"/> containing the single trie as the current generation.</returns>
|
||||
public static ClusterEntry FromSingle(PermissionTrie trie) =>
|
||||
new(trie, new Dictionary<long, PermissionTrie> { [trie.GenerationId] = trie });
|
||||
|
||||
/// <summary>Creates a new entry with an additional trie, updating current if it's newer.</summary>
|
||||
/// <param name="trie">The new permission trie to add.</param>
|
||||
/// <returns>A new <see cref="ClusterEntry"/> with the trie added and the current pointer updated if the new generation is newer.</returns>
|
||||
public ClusterEntry WithAdditional(PermissionTrie trie)
|
||||
{
|
||||
var next = new Dictionary<long, PermissionTrie>(Tries) { [trie.GenerationId] = trie };
|
||||
|
||||
@@ -24,11 +24,7 @@ public sealed class TriePermissionEvaluator : IPermissionEvaluator
|
||||
_timeProvider = timeProvider ?? TimeProvider.System;
|
||||
}
|
||||
|
||||
/// <summary>Authorizes an operation against the user's session and node scope.</summary>
|
||||
/// <param name="session">The user's authorization session.</param>
|
||||
/// <param name="operation">The OPC UA operation to authorize.</param>
|
||||
/// <param name="scope">The target node scope.</param>
|
||||
/// <returns>An authorization decision indicating whether the operation is allowed.</returns>
|
||||
/// <inheritdoc />
|
||||
public AuthorizationDecision Authorize(UserAuthorizationState session, OpcUaOperation operation, NodeScope scope)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(session);
|
||||
|
||||
@@ -64,6 +64,7 @@ public sealed record UserAuthorizationState
|
||||
/// whenever this is true.
|
||||
/// </summary>
|
||||
/// <param name="utcNow">The current UTC time.</param>
|
||||
/// <returns><c>true</c> when the state exceeds its maximum staleness ceiling.</returns>
|
||||
public bool IsStale(DateTime utcNow) => utcNow - MembershipResolvedUtc > AuthCacheMaxStaleness;
|
||||
|
||||
/// <summary>
|
||||
@@ -72,6 +73,7 @@ public sealed record UserAuthorizationState
|
||||
/// call still evaluates against the cached memberships.
|
||||
/// </summary>
|
||||
/// <param name="utcNow">The current UTC time.</param>
|
||||
/// <returns><c>true</c> when a background refresh should be initiated but the current cached memberships are still usable.</returns>
|
||||
public bool NeedsRefresh(DateTime utcNow) =>
|
||||
!IsStale(utcNow) && utcNow - MembershipResolvedUtc > MembershipFreshnessInterval;
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ public sealed class DriverFactoryRegistry
|
||||
/// missing-assembly deployment doesn't take down the whole server.
|
||||
/// </summary>
|
||||
/// <param name="driverType">The driver type to look up.</param>
|
||||
/// <returns>The registered factory delegate, or <see langword="null"/> if no factory was registered for the type.</returns>
|
||||
public Func<string, string, IDriver>? TryGet(string driverType)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
||||
@@ -75,6 +76,7 @@ public sealed class DriverFactoryRegistry
|
||||
/// case upstream; we don't double-surface that failure here.
|
||||
/// </summary>
|
||||
/// <param name="driverType">The driver type to look up.</param>
|
||||
/// <returns>The registered <see cref="DriverTier"/>, or <see cref="DriverTier.A"/> if the type is unknown.</returns>
|
||||
public DriverTier GetTier(string driverType)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverType);
|
||||
|
||||
@@ -20,16 +20,13 @@ public sealed class DriverFactoryRegistryAdapter : IDriverFactory
|
||||
_registry = registry;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to create a driver instance by type and configuration.</summary>
|
||||
/// <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>
|
||||
/// <inheritdoc />
|
||||
public IDriver? TryCreate(string driverType, string driverInstanceId, string driverConfigJson)
|
||||
{
|
||||
var factory = _registry.TryGet(driverType);
|
||||
return factory?.Invoke(driverInstanceId, driverConfigJson);
|
||||
}
|
||||
|
||||
/// <summary>Gets the collection of supported driver type names.</summary>
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyCollection<string> SupportedTypes => _registry.RegisteredTypes;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ public sealed class DriverHost : IAsyncDisposable
|
||||
|
||||
/// <summary>Gets the health status of a registered driver.</summary>
|
||||
/// <param name="driverInstanceId">The driver instance identifier to query.</param>
|
||||
/// <returns>The driver health if the driver is registered; otherwise null.</returns>
|
||||
public DriverHealth? GetHealth(string driverInstanceId)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -33,6 +34,7 @@ public sealed class DriverHost : IAsyncDisposable
|
||||
/// startup. Returns null when the driver is not registered.
|
||||
/// </summary>
|
||||
/// <param name="driverInstanceId">The driver instance identifier to look up.</param>
|
||||
/// <returns>The driver instance if registered; otherwise null.</returns>
|
||||
public IDriver? GetDriver(string driverInstanceId)
|
||||
{
|
||||
lock (_lock)
|
||||
@@ -47,6 +49,7 @@ public sealed class DriverHost : IAsyncDisposable
|
||||
/// <param name="driver">The driver instance to register.</param>
|
||||
/// <param name="driverConfigJson">The configuration JSON for the driver.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task RegisterAsync(IDriver driver, string driverConfigJson, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(driver);
|
||||
@@ -70,6 +73,7 @@ public sealed class DriverHost : IAsyncDisposable
|
||||
/// <summary>Unregisters a driver and calls shutdown.</summary>
|
||||
/// <param name="driverInstanceId">The driver instance identifier to unregister.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
public async Task UnregisterAsync(string driverInstanceId, CancellationToken ct)
|
||||
{
|
||||
IDriver? driver;
|
||||
@@ -84,6 +88,7 @@ public sealed class DriverHost : IAsyncDisposable
|
||||
}
|
||||
|
||||
/// <summary>Disposes the driver host and all registered drivers.</summary>
|
||||
/// <returns>A value task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
List<IDriver> snapshot;
|
||||
|
||||
@@ -27,6 +27,7 @@ public static class DriverHealthReport
|
||||
{
|
||||
/// <summary>Compute the fleet-wide readiness verdict from per-driver states.</summary>
|
||||
/// <param name="drivers">The list of per-driver health snapshots to aggregate.</param>
|
||||
/// <returns>The fleet-wide <see cref="ReadinessVerdict"/> derived from all driver states.</returns>
|
||||
public static ReadinessVerdict Aggregate(IReadOnlyList<DriverHealthSnapshot> drivers)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(drivers);
|
||||
@@ -54,6 +55,7 @@ public static class DriverHealthReport
|
||||
/// return per the Stream C.1 state matrix.
|
||||
/// </summary>
|
||||
/// <param name="verdict">The readiness verdict to map to HTTP status.</param>
|
||||
/// <returns>The HTTP status code (200 or 503) corresponding to the verdict.</returns>
|
||||
public static int HttpStatus(ReadinessVerdict verdict) => verdict switch
|
||||
{
|
||||
ReadinessVerdict.Healthy => 200,
|
||||
|
||||
@@ -22,6 +22,7 @@ public static class LogContextEnricher
|
||||
/// <param name="driverType">The driver type name.</param>
|
||||
/// <param name="capability">The driver capability being invoked.</param>
|
||||
/// <param name="correlationId">The correlation ID for tracing the call.</param>
|
||||
/// <returns>A scope that pops the pushed properties when disposed.</returns>
|
||||
public static IDisposable Push(string driverInstanceId, string driverType, DriverCapability capability, string correlationId)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(driverInstanceId);
|
||||
@@ -40,6 +41,7 @@ public static class LogContextEnricher
|
||||
/// 12-hex-char slice of a GUID — long enough for log correlation, short enough to
|
||||
/// scan visually.
|
||||
/// </summary>
|
||||
/// <returns>A 12-character hex string suitable for log correlation.</returns>
|
||||
public static string NewCorrelationId() => Guid.NewGuid().ToString("N")[..12];
|
||||
|
||||
private sealed class CompositeScope : IDisposable
|
||||
|
||||
@@ -183,6 +183,7 @@ public static class EquipmentNodeWalker
|
||||
/// wants an opaque non-JSON reference.
|
||||
/// </remarks>
|
||||
/// <param name="tagConfig">The tag configuration JSON or string.</param>
|
||||
/// <returns>The value of the <c>FullName</c> field from the JSON, or the raw <paramref name="tagConfig"/> string as a fallback.</returns>
|
||||
internal static string ExtractFullName(string tagConfig)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(tagConfig)) return tagConfig;
|
||||
|
||||
@@ -49,6 +49,7 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
|
||||
/// </summary>
|
||||
/// <param name="builder">The address space builder to populate.</param>
|
||||
/// <param name="ct">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous address space build operation.</returns>
|
||||
public async Task BuildAddressSpaceAsync(IAddressSpaceBuilder builder, CancellationToken ct)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(builder);
|
||||
@@ -111,23 +112,15 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
|
||||
IAddressSpaceBuilder inner,
|
||||
ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IAddressSpaceBuilder
|
||||
{
|
||||
/// <summary>Adds a folder to the address space.</summary>
|
||||
/// <param name="browseName">The browse name of the folder node.</param>
|
||||
/// <param name="displayName">The display name of the folder node.</param>
|
||||
/// <inheritdoc />
|
||||
public IAddressSpaceBuilder Folder(string browseName, string displayName)
|
||||
=> new CapturingBuilder(inner.Folder(browseName, displayName), sinks);
|
||||
|
||||
/// <summary>Adds a variable to the address space.</summary>
|
||||
/// <param name="browseName">The browse name of the variable node.</param>
|
||||
/// <param name="displayName">The display name of the variable node.</param>
|
||||
/// <param name="attributeInfo">Metadata describing the variable's data type and properties.</param>
|
||||
/// <inheritdoc />
|
||||
public IVariableHandle Variable(string browseName, string displayName, DriverAttributeInfo attributeInfo)
|
||||
=> new CapturingHandle(inner.Variable(browseName, displayName, attributeInfo), sinks);
|
||||
|
||||
/// <summary>Adds a property to the address space.</summary>
|
||||
/// <param name="browseName">The browse name of the property node.</param>
|
||||
/// <param name="dataType">The OPC UA data type of the property.</param>
|
||||
/// <param name="value">The initial value of the property, or null.</param>
|
||||
/// <inheritdoc />
|
||||
public void AddProperty(string browseName, DriverDataType dataType, object? value)
|
||||
=> inner.AddProperty(browseName, dataType, value);
|
||||
}
|
||||
@@ -136,11 +129,10 @@ public class GenericDriverNodeManager(IDriver driver) : IDisposable
|
||||
IVariableHandle inner,
|
||||
ConcurrentDictionary<string, IAlarmConditionSink> sinks) : IVariableHandle
|
||||
{
|
||||
/// <summary>Gets the full reference for the variable.</summary>
|
||||
/// <inheritdoc />
|
||||
public string FullReference => inner.FullReference;
|
||||
|
||||
/// <summary>Marks the variable as an alarm condition and registers its sink.</summary>
|
||||
/// <param name="info">Configuration for the alarm condition.</param>
|
||||
/// <inheritdoc />
|
||||
public IAlarmConditionSink MarkAsAlarmCondition(AlarmConditionInfo info)
|
||||
{
|
||||
var sink = inner.MarkAsAlarmCondition(info);
|
||||
|
||||
@@ -59,6 +59,7 @@ public sealed class AlarmSurfaceInvoker
|
||||
/// </summary>
|
||||
/// <param name="sourceNodeIds">The source node IDs to subscribe to.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that resolves to one subscription handle per resolved host.</returns>
|
||||
public async Task<IReadOnlyList<IAlarmSubscriptionHandle>> SubscribeAsync(
|
||||
IReadOnlyList<string> sourceNodeIds,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -89,6 +90,7 @@ public sealed class AlarmSurfaceInvoker
|
||||
/// </summary>
|
||||
/// <param name="handle">The subscription handle to unsubscribe.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous unsubscribe operation.</returns>
|
||||
public ValueTask UnsubscribeAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(handle);
|
||||
@@ -110,6 +112,7 @@ public sealed class AlarmSurfaceInvoker
|
||||
/// </summary>
|
||||
/// <param name="acknowledgements">The alarm acknowledgement requests.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
/// <returns>A task that represents the asynchronous acknowledgement operation.</returns>
|
||||
public async Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements,
|
||||
CancellationToken cancellationToken)
|
||||
@@ -166,7 +169,7 @@ public sealed class AlarmSurfaceInvoker
|
||||
public IAlarmSubscriptionHandle Inner { get; } = inner;
|
||||
/// <summary>Gets the resolved host name.</summary>
|
||||
public string Host { get; } = host;
|
||||
/// <summary>Gets the diagnostic ID from the inner handle.</summary>
|
||||
/// <inheritdoc />
|
||||
public string DiagnosticId => Inner.DiagnosticId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public sealed class CapabilityInvoker
|
||||
/// <param name="hostName">The host name for logging and status tracking.</param>
|
||||
/// <param name="callSite">The async function to execute.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The result produced by <paramref name="callSite"/> after executing through the pipeline.</returns>
|
||||
public async ValueTask<TResult> ExecuteAsync<TResult>(
|
||||
DriverCapability capability,
|
||||
string hostName,
|
||||
@@ -86,6 +87,7 @@ public sealed class CapabilityInvoker
|
||||
/// <param name="hostName">The host name for logging and status tracking.</param>
|
||||
/// <param name="callSite">The async function to execute.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>A value task that represents the asynchronous operation.</returns>
|
||||
public async ValueTask ExecuteAsync(
|
||||
DriverCapability capability,
|
||||
string hostName,
|
||||
@@ -121,6 +123,7 @@ public sealed class CapabilityInvoker
|
||||
/// <param name="isIdempotent">Whether the write operation is idempotent.</param>
|
||||
/// <param name="callSite">The async function to execute.</param>
|
||||
/// <param name="cancellationToken">Cancellation token for the operation.</param>
|
||||
/// <returns>The result produced by <paramref name="callSite"/> after executing through the write pipeline.</returns>
|
||||
public async ValueTask<TResult> ExecuteWriteAsync<TResult>(
|
||||
string hostName,
|
||||
bool isIdempotent,
|
||||
|
||||
@@ -50,6 +50,7 @@ public static class DriverResilienceOptionsParser
|
||||
/// <param name="tier">The driver tier for default resilience options.</param>
|
||||
/// <param name="resilienceConfigJson">The optional JSON configuration string to parse.</param>
|
||||
/// <param name="parseDiagnostic">An out parameter containing diagnostic information if parsing fails.</param>
|
||||
/// <returns>The effective resilience options; tier defaults when the JSON is null or malformed.</returns>
|
||||
public static DriverResilienceOptions ParseOrDefaults(
|
||||
DriverTier tier,
|
||||
string? resilienceConfigJson,
|
||||
|
||||
@@ -54,6 +54,7 @@ public sealed class DriverResiliencePipelineBuilder
|
||||
/// </param>
|
||||
/// <param name="capability">Which capability surface is being called.</param>
|
||||
/// <param name="options">Per-driver-instance options (tier + per-capability overrides).</param>
|
||||
/// <returns>The cached or newly created <see cref="ResiliencePipeline"/> for the given key.</returns>
|
||||
public ResiliencePipeline GetOrCreate(
|
||||
string driverInstanceId,
|
||||
string hostName,
|
||||
|
||||
@@ -128,10 +128,12 @@ public sealed class DriverResilienceStatusTracker
|
||||
/// <summary>Snapshot of a specific (instance, host) pair; null if no counters recorded yet.</summary>
|
||||
/// <param name="driverInstanceId">The driver instance identifier.</param>
|
||||
/// <param name="hostName">The host name.</param>
|
||||
/// <returns>The current <see cref="ResilienceStatusSnapshot"/> for the pair, or <see langword="null"/> if no counters have been recorded.</returns>
|
||||
public ResilienceStatusSnapshot? TryGet(string driverInstanceId, string hostName) =>
|
||||
_status.TryGetValue(new StatusKey(driverInstanceId, hostName), out var snapshot) ? snapshot : null;
|
||||
|
||||
/// <summary>Copy of every currently-tracked (instance, host, snapshot) triple. Safe under concurrent writes.</summary>
|
||||
/// <returns>A snapshot list of all currently tracked driver instance and host resilience states.</returns>
|
||||
public IReadOnlyList<(string DriverInstanceId, string HostName, ResilienceStatusSnapshot Snapshot)> Snapshot() =>
|
||||
_status.Select(kvp => (kvp.Key.DriverInstanceId, kvp.Key.HostName, kvp.Value)).ToList();
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ public sealed class MemoryTracking
|
||||
|
||||
/// <summary>Tier-default multiplier/floor constants per decision #146.</summary>
|
||||
/// <param name="tier">The driver tier.</param>
|
||||
/// <returns>A tuple with the growth multiplier and the minimum floor bytes for the specified tier.</returns>
|
||||
public static (int Multiplier, long FloorBytes) GetTierConstants(DriverTier tier) => tier switch
|
||||
{
|
||||
DriverTier.A => (Multiplier: 3, FloorBytes: 50L * 1024 * 1024),
|
||||
@@ -73,6 +74,7 @@ public sealed class MemoryTracking
|
||||
/// </summary>
|
||||
/// <param name="footprintBytes">The current memory footprint in bytes.</param>
|
||||
/// <param name="utcNow">The current UTC time.</param>
|
||||
/// <returns>The <see cref="MemoryTrackingAction"/> classifying this sample against the soft/hard thresholds.</returns>
|
||||
public MemoryTrackingAction Sample(long footprintBytes, DateTime utcNow)
|
||||
{
|
||||
if (_phase == TrackingPhase.WarmingUp)
|
||||
|
||||
@@ -60,6 +60,7 @@ public abstract class AbCipCommandBase : DriverCommandBase
|
||||
/// probe loop would race the operator's own reads.
|
||||
/// </summary>
|
||||
/// <param name="tags">The list of tag definitions to include in the options.</param>
|
||||
/// <returns>A fully-configured <see cref="AbCipDriverOptions"/> with probe and alarm projection disabled.</returns>
|
||||
protected AbCipDriverOptions BuildOptions(IReadOnlyList<AbCipTagDefinition> tags) => new()
|
||||
{
|
||||
Devices = [new AbCipDeviceOptions(
|
||||
|
||||
@@ -66,6 +66,7 @@ public sealed class ReadCommand : AbCipCommandBase
|
||||
/// </summary>
|
||||
/// <param name="tagPath">The symbolic tag path.</param>
|
||||
/// <param name="type">The data type.</param>
|
||||
/// <returns>A combined tag-name string in <c>path:type</c> form.</returns>
|
||||
internal static string SynthesiseTagName(string tagPath, AbCipDataType type)
|
||||
=> $"{tagPath}:{type}";
|
||||
}
|
||||
|
||||
@@ -58,6 +58,7 @@ public sealed class ReadCommand : AbLegacyCommandBase
|
||||
/// <summary>Tag-name key the driver uses internally. Address+type is already unique.</summary>
|
||||
/// <param name="address">The PCCC file address.</param>
|
||||
/// <param name="type">The data type of the address.</param>
|
||||
/// <returns>A combined tag name string in the form <c>address:type</c>.</returns>
|
||||
internal static string SynthesiseTagName(string address, AbLegacyDataType type)
|
||||
=> $"{address}:{type}";
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ public static class SnapshotFormatter
|
||||
/// </summary>
|
||||
/// <param name="tagName">The tag name to include in the output.</param>
|
||||
/// <param name="snapshot">The data value snapshot to format.</param>
|
||||
/// <returns>A multi-line string representation of the tag and its value.</returns>
|
||||
public static string Format(string tagName, DataValueSnapshot snapshot)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(snapshot);
|
||||
@@ -42,6 +43,7 @@ public static class SnapshotFormatter
|
||||
/// </summary>
|
||||
/// <param name="tagName">The tag name to include in the output.</param>
|
||||
/// <param name="result">The write result to format.</param>
|
||||
/// <returns>A single-line string showing the tag name and write status.</returns>
|
||||
public static string FormatWrite(string tagName, WriteResult result)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(result);
|
||||
@@ -54,6 +56,7 @@ public static class SnapshotFormatter
|
||||
/// </summary>
|
||||
/// <param name="tagNames">The list of tag names to include as rows.</param>
|
||||
/// <param name="snapshots">The list of data value snapshots to format.</param>
|
||||
/// <returns>An aligned table string with tag, value, status, and source-time columns.</returns>
|
||||
public static string FormatTable(
|
||||
IReadOnlyList<string> tagNames, IReadOnlyList<DataValueSnapshot> snapshots)
|
||||
{
|
||||
|
||||
@@ -52,6 +52,7 @@ public sealed class ReadCommand : FocasCommandBase
|
||||
/// <summary>Constructs a tag name from address and data type.</summary>
|
||||
/// <param name="address">The FOCAS address.</param>
|
||||
/// <param name="type">The data type.</param>
|
||||
/// <returns>A synthesized tag name string combining the address and data type.</returns>
|
||||
internal static string SynthesiseTagName(string address, FocasDataType type)
|
||||
=> $"{address}:{type}";
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ public abstract class FocasCommandBase : DriverCommandBase
|
||||
/// as <c>BadCommunicationError</c>.
|
||||
/// </summary>
|
||||
/// <param name="tags">The tag definitions to include in the driver options.</param>
|
||||
/// <returns>A <see cref="FocasDriverOptions"/> configured with the CNC target and the supplied tag list.</returns>
|
||||
protected FocasDriverOptions BuildOptions(IReadOnlyList<FocasTagDefinition> tags) => new()
|
||||
{
|
||||
Devices = [new FocasDeviceOptions(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user