Merge b647-focas-alarms: FOCAS native alarms reach Part 9 conditions (ALARM: address) + respawn keeps acks host-side (#647)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -1942,12 +1942,11 @@ public sealed class OpcUaClientDriver : IDriver, ITagDiscovery, IReadable, IWrit
|
||||
|
||||
// Fresh prior = the host projector's own default (inactive, acked).
|
||||
var told = new AlarmToldState(target.Active, target.Acked, e.Message, e.OpcUaSeverity);
|
||||
var prior = _alarmStateByRawPath.TryGetValue(e.ConditionId, out var p)
|
||||
? p
|
||||
: new AlarmToldState(false, true, null, null);
|
||||
var priorKnown = _alarmStateByRawPath.TryGetValue(e.ConditionId, out var p);
|
||||
var prior = priorKnown ? p! : new AlarmToldState(false, true, null, null);
|
||||
_alarmStateByRawPath[e.ConditionId] = told;
|
||||
|
||||
var kinds = AlarmTransitions((prior.Active, prior.Acked), target);
|
||||
var kinds = AlarmTransitions((prior.Active, prior.Acked), target, priorKnown);
|
||||
if (kinds.Count == 0)
|
||||
{
|
||||
// Same state: a message/severity change still reaches the host; an identical copy does not.
|
||||
@@ -2066,18 +2065,34 @@ 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, Acknowledge ⇒ acked,
|
||||
/// <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
|
||||
/// 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>
|
||||
/// <param name="prior">The state last told to the host.</param>
|
||||
/// <param name="target">The upstream event's ActiveState/Id + AckedState/Id.</param>
|
||||
/// <param name="priorKnown">
|
||||
/// False when this driver instance has told the host nothing about the condition yet (first sight after a
|
||||
/// start or a deploy respawn), so <paramref name="prior"/> is only the projector's default. Gitea #647 review:
|
||||
/// the host now treats a Raise on an already-active condition as a re-statement that KEEPS its ack, so an
|
||||
/// upstream that says <i>unacknowledged</i> must then be told with <see cref="AlarmTransitionKind.Retrigger"/>
|
||||
/// — the upstream's ack state is authoritative, and the host may be holding an ack from before the respawn.
|
||||
/// </param>
|
||||
/// <returns>The kinds to emit, in order.</returns>
|
||||
internal static IReadOnlyList<AlarmTransitionKind> AlarmTransitions(
|
||||
(bool Active, bool Acked) prior, (bool Active, bool Acked) target)
|
||||
(bool Active, bool Acked) prior, (bool Active, bool Acked) target, bool priorKnown = true)
|
||||
{
|
||||
if (prior == target) return [];
|
||||
var kinds = Transitions(prior, target);
|
||||
return priorKnown || target.Acked
|
||||
? kinds
|
||||
: [.. kinds.Select(k => k == AlarmTransitionKind.Raise ? AlarmTransitionKind.Retrigger : k)];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AlarmTransitionKind> Transitions(
|
||||
(bool Active, bool Acked) prior, (bool Active, bool Acked) target)
|
||||
{
|
||||
return target switch
|
||||
{
|
||||
// Active+unacked: a fresh raise, or a re-arm of an active-acked condition.
|
||||
|
||||
Reference in New Issue
Block a user