fix(opcua): carry native alarm condition state across a full rebuild (#641)

A full RebuildAddressSpace recreated every native Part 9 condition inactive +
acknowledged, and when the alarm-tag set was unchanged (e.g. one
UnsTagReference added to an alarm-bearing raw tag) nothing re-told the new node
its state: the host does not re-subscribe for an unchanged set, and Galaxy /
OpcUaClient have no snapshot to replay anyway. Active alarms read normal until
their next device transition.

OtOpcUaNodeManager now captures each NATIVE condition's live state (from the
node, so a client-applied ack is kept) before the rebuild clears, restores it
when the same id is re-materialised native in the same apply, and reports ONE
condition event per restored off-normal condition at the new
IOpcUaAddressSpaceSink.CompleteAddressSpaceApply (after notifier wiring).
Unclaimed captures are discarded there, so a later re-add is fresh. Quality is
carried silently for normal conditions too (it is only re-annotated on a
connectivity edge). Scripted conditions are untouched (#487 re-asserts them).
OpcUaPublishActor calls CompleteAddressSpaceApply at the end of every apply.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 17:02:12 -04:00
parent a8b07ae12d
commit ddd7678269
9 changed files with 611 additions and 3 deletions
@@ -69,6 +69,9 @@ public sealed class DeferredAddressSpaceSink : IOpcUaAddressSpaceSink, ISurgical
/// <inheritdoc />
public void RebuildAddressSpace() => _inner.RebuildAddressSpace();
/// <inheritdoc />
public void CompleteAddressSpaceApply() => _inner.CompleteAddressSpaceApply();
/// <inheritdoc />
public void RaiseNodesAddedModelChange(string affectedNodeId, AddressSpaceRealm realm) => _inner.RaiseNodesAddedModelChange(affectedNodeId, realm);
@@ -185,6 +185,19 @@ public interface IOpcUaAddressSpaceSink
/// </summary>
void RebuildAddressSpace();
/// <summary>
/// Gitea #641 — mark the END of one address-space apply pass (after <see cref="RebuildAddressSpace"/>, if
/// one ran, and every materialise / notifier-wiring call that followed it). An SDK-backed sink carries each
/// native condition's live Part 9 state across a rebuild and restores it when the same condition is
/// re-materialised; this is where it reports the ONE condition event per restored condition (now that
/// every notifier folder is wired) and DISCARDS any captured state no materialise claimed, so a later
/// deploy that re-adds the same id gets a fresh node rather than a resurrection. <c>OpcUaPublishActor</c>
/// calls it once at the end of every apply, rebuild or not. Idempotent; the default is a no-op.
/// </summary>
void CompleteAddressSpaceApply()
{
}
/// <summary>
/// Announce that nodes were added at runtime (discovered-node injection) under
/// <paramref name="affectedNodeId"/> so subscribed clients refresh their browse