Auto: focas-f3a — cnc_rdalmhistry alarm-history extension
Adds FocasAlarmProjection with two modes (ActiveOnly default, ActivePlusHistory) that polls cnc_rdalmhistry on connect + on a configurable cadence (5 min default, HistoryDepth=100 capped at 250). Emits historic events via IAlarmSource with SourceTimestampUtc set from the CNC's reported timestamp; dedup keyed on (OccurrenceTime, AlarmNumber, AlarmType). Ships the ODBALMHIS packed-buffer decoder + encoder in Wire/FocasAlarmHistoryDecoder.cs and threads ReadAlarmHistoryAsync through IFocasClient (default no-op so existing transport variants stay back-compat). FocasDriver now implements IAlarmSource. 13 new unit tests cover: mode switch, dedup, distinct-timestamp emission, type-as-key behaviour, OccurrenceTime passthrough (not Now), HistoryDepth clamp/fallback, and decoder round-trip. All 341 FOCAS unit tests still pass. Docs: docs/drivers/FOCAS.md (new), docs/v2/focas-deployment.md (new), docs/v2/implementation/focas-wire-protocol.md (new), docs/v2/implementation/focas-simulator-plan.md (new), docs/drivers/FOCAS-Test-Fixture.md (alarm-history bullet appended). Closes #267
This commit is contained in:
@@ -18,12 +18,13 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
|
||||
/// fail fast.
|
||||
/// </remarks>
|
||||
public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery, ISubscribable,
|
||||
IHostConnectivityProbe, IPerCallHostResolver, IDisposable, IAsyncDisposable
|
||||
IHostConnectivityProbe, IPerCallHostResolver, IAlarmSource, IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly FocasDriverOptions _options;
|
||||
private readonly string _driverInstanceId;
|
||||
private readonly IFocasClientFactory _clientFactory;
|
||||
private readonly PollGroupEngine _poll;
|
||||
private readonly FocasAlarmProjection _alarmProjection;
|
||||
private readonly Dictionary<string, DeviceState> _devices = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, FocasTagDefinition> _tagsByName = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly Dictionary<string, (string Host, string Field)> _statusNodesByName =
|
||||
@@ -119,6 +120,13 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
public event EventHandler<DataChangeEventArgs>? OnDataChange;
|
||||
public event EventHandler<HostStatusChangedEventArgs>? OnHostStatusChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Per <see cref="IAlarmSource"/> — the projection raises history events through here
|
||||
/// and a future PR's active-alarm poll will join the same channel (issue #267,
|
||||
/// plan PR F3-a).
|
||||
/// </summary>
|
||||
public event EventHandler<AlarmEventArgs>? OnAlarmEvent;
|
||||
|
||||
public FocasDriver(FocasDriverOptions options, string driverInstanceId,
|
||||
IFocasClientFactory? clientFactory = null)
|
||||
{
|
||||
@@ -130,6 +138,26 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
reader: ReadAsync,
|
||||
onChange: (handle, tagRef, snapshot) =>
|
||||
OnDataChange?.Invoke(this, new DataChangeEventArgs(handle, tagRef, snapshot)));
|
||||
_alarmProjection = new FocasAlarmProjection(
|
||||
options: _options.AlarmProjection,
|
||||
connectAsync: ConnectFirstDeviceAsync,
|
||||
emit: args => OnAlarmEvent?.Invoke(this, args),
|
||||
diagnosticPrefix: $"focas-alarm-{driverInstanceId}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bridge for the alarm projection — returns the first device's connected
|
||||
/// <see cref="IFocasClient"/> on demand. Multi-device alarm projection (one history
|
||||
/// poll per CNC) is a follow-up; today the projection targets the primary device,
|
||||
/// which is the only deployed shape per the F3-a plan.
|
||||
/// </summary>
|
||||
private async Task<IFocasClient?> ConnectFirstDeviceAsync(CancellationToken ct)
|
||||
{
|
||||
var device = _devices.Values.FirstOrDefault();
|
||||
if (device is null) return null;
|
||||
try { return await EnsureConnectedAsync(device, ct).ConfigureAwait(false); }
|
||||
catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; }
|
||||
catch { return null; }
|
||||
}
|
||||
|
||||
public string DriverInstanceId => _driverInstanceId;
|
||||
@@ -254,6 +282,7 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
|
||||
public async Task ShutdownAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await _alarmProjection.DisposeAsync().ConfigureAwait(false);
|
||||
await _poll.DisposeAsync().ConfigureAwait(false);
|
||||
foreach (var state in _devices.Values)
|
||||
{
|
||||
@@ -813,6 +842,36 @@ public sealed class FocasDriver : IDriver, IReadable, IWritable, ITagDiscovery,
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// ---- IAlarmSource (issue #267, plan PR F3-a) ----
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to FOCAS alarm events. When
|
||||
/// <see cref="FocasDriverOptions.AlarmProjection"/>'s mode is
|
||||
/// <see cref="FocasAlarmProjectionMode.ActivePlusHistory"/>, the projection polls
|
||||
/// <c>cnc_rdalmhistry</c> on connect + on the configured cadence and emits unseen
|
||||
/// entries through <see cref="OnAlarmEvent"/> with the CNC's reported timestamp.
|
||||
/// <see cref="FocasAlarmProjectionMode.ActiveOnly"/> (default) returns the handle
|
||||
/// for capability negotiation but skips the history poll. The active-alarm poll
|
||||
/// itself ships in a follow-up PR.
|
||||
/// </summary>
|
||||
public Task<IAlarmSubscriptionHandle> SubscribeAlarmsAsync(
|
||||
IReadOnlyList<string> sourceNodeIds, CancellationToken cancellationToken)
|
||||
=> _alarmProjection.SubscribeAsync(sourceNodeIds, cancellationToken);
|
||||
|
||||
public Task UnsubscribeAlarmsAsync(IAlarmSubscriptionHandle handle, CancellationToken cancellationToken)
|
||||
=> _alarmProjection.UnsubscribeAsync(handle, cancellationToken);
|
||||
|
||||
public Task AcknowledgeAsync(
|
||||
IReadOnlyList<AlarmAcknowledgeRequest> acknowledgements, CancellationToken cancellationToken)
|
||||
=> _alarmProjection.AcknowledgeAsync(acknowledgements, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Reset the alarm projection's dedup set. Called by the driver on reconnect so the
|
||||
/// first poll after reconnect re-emits the ring buffer (acceptable per issue #267
|
||||
/// since alarms are timestamped + clients can suppress repeats client-side).
|
||||
/// </summary>
|
||||
internal void ResetAlarmDedup() => _alarmProjection.ResetDedup();
|
||||
|
||||
// ---- IHostConnectivityProbe ----
|
||||
|
||||
public IReadOnlyList<HostConnectivityStatus> GetHostStatuses() =>
|
||||
|
||||
Reference in New Issue
Block a user