Merge hw3-alarms (#656 atomic ActiveAcknowledged + re-statement flag for native alarms)

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 00:14:47 -04:00
20 changed files with 1006 additions and 84 deletions
@@ -1953,8 +1953,11 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
if (prior == told) return;
kinds = [AlarmTransitionKind.Unspecified];
}
// #656: on first sight (a start or deploy respawn) this instance is RE-STATING upstream's current state,
// not reporting a transition it watched — flagged so the host writes no duplicate history row when its
// condition already shows exactly this. History only; the host still projects the state.
foreach (var kind in kinds)
OnAlarmEvent?.Invoke(this, e with { Kind = kind });
OnAlarmEvent?.Invoke(this, e with { Kind = kind, IsRestatement = !priorKnown });
}
}
@@ -2066,7 +2069,7 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
/// <summary>
/// #619 review — the <see cref="AlarmTransitionKind"/> sequence that moves the host's
/// <c>NativeAlarmProjector</c> (which knows only kinds: Raise ⇒ active+unacked — or, on an already-active condition, a re-statement keeping the ack (#647) — Acknowledge ⇒ acked,
/// Clear ⇒ inactive keeping acked) from <paramref name="prior"/> to <paramref name="target"/>. A single
/// ActiveAcknowledged ⇒ active+acked in one step (#656), Clear ⇒ inactive keeping acked) from <paramref name="prior"/> to <paramref name="target"/>. A single
/// upstream event can need two (an alarm that went active and was acknowledged between two notifications).
/// No state change ⇒ empty (the caller decides whether a message/severity change warrants an Unspecified).
/// </summary>
@@ -2097,10 +2100,11 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
{
// Active+unacked: a fresh raise, or a re-arm of an active-acked condition.
(true, false) => [prior.Active ? AlarmTransitionKind.Retrigger : AlarmTransitionKind.Raise],
// Active+acked: raise first when it was inactive (Raise always lands unacked).
// Active+acked from inactive: ONE atomic ActiveAcknowledged (Gitea #656) — Raise-then-Acknowledge showed
// clients an Active+UNacked intermediate event before the ack.
(true, true) => prior.Active
? [AlarmTransitionKind.Acknowledge]
: [AlarmTransitionKind.Raise, AlarmTransitionKind.Acknowledge],
: [AlarmTransitionKind.ActiveAcknowledged],
// Inactive+acked: Clear keeps the prior ack, so ack after it when that was unacked.
(false, true) => prior.Active
? prior.Acked ? [AlarmTransitionKind.Clear] : [AlarmTransitionKind.Clear, AlarmTransitionKind.Acknowledge]