From bd23f557f31c74a8a2334a4dd749726be45518dc Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 8 Oct 2026 17:09:24 -0400 Subject: [PATCH] docs(alarms): native condition state across a rebuild, projector prune, exact ConditionId routing (#641) Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe --- CLAUDE.md | 9 +++++++-- docs/AlarmTracking.md | 45 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1c68f757..026af80a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1136,10 +1136,15 @@ host logger factory (wired in `DriverFactoryBootstrap`; the FOCAS and TwinCAT fa active nativeCode (ties: ordinal nativeCode, then type); ack is server-side only. ⚠️ UNAVAILABLE never clears: an active condition stays active+acked with a quality-only `BadNoCommunication` (same as #481 transport loss). The driver dedups against what the server was last TOLD (level+nativeCode+nativeSeverity+qualifier — not text), records - only while a handler is attached, and clears that record when the subscribed alarm SET changes (a rebuild recreates - the nodes inactive) but not on a reconnect of the same set — a replayed `/current` must never re-raise (un-ack). + only while a handler is attached, and drops the record of only the conditions that LEFT the set when the subscribed + alarm SET changes (#641) — never on a reconnect of the same set; a replayed `/current` must never re-raise (un-ack). `AlarmEventArgs` gained optional `OpcUaSeverity` (exact Part 9 severity) and `QualityStatusCode`; a quality-only event goes out as `AlarmQualityUpdate` per node and never through the snapshot projection. +- **Native condition state survives a full rebuild (#641):** `RebuildAddressSpace` captures each NATIVE condition's + live node state (client acks included), the same-apply re-materialise restores it, and `CompleteAddressSpaceApply` + (called by `OpcUaPublishActor` at the end of every apply) reports ONE event per restored off-normal condition and + discards unclaimed captures. `NativeAlarmProjector` is pruned per apply, not cleared. Native routing is an exact + `ConditionId == RawPath` match — AbCip's `#active` suffix was fixed; FOCAS is still unroutable (`docs/drivers/FOCAS.md`). - **Driver removal (#609):** removing a driver releases its capability-invoker pipelines and resilience tracker, *then* evicts its telemetry snapshots. ⚠️ Both are **tombstoned**, not just cleared: the removed child stops asynchronously and its in-flight subscribe/connect still completes through the invoker (and diff --git a/docs/AlarmTracking.md b/docs/AlarmTracking.md index 5a021ac3..ec7344c4 100644 --- a/docs/AlarmTracking.md +++ b/docs/AlarmTracking.md @@ -54,6 +54,51 @@ same condition is wired as an **event notifier of every referencing equipment fo - The `alerts`-topic `AlarmTransitionEvent` carries the (possibly empty) **list of referencing equipment paths**, and the AdminUI `/alerts` page shows **one row per condition** (primary identity RawPath + condition NodeId) with the equipment list as display metadata. +- **Routing is an exact `ConditionId == RawPath` match** (`DriverHostActor.ForwardNativeAlarm`). A driver that + decorates the id has every transition dropped as "no alarm condition" — AbCip did (`"{ref}#active"`, fixed + under #641); FOCAS still does (see `drivers/FOCAS.md`). A native source must also set `Kind`: an + `Unspecified` kind projects no state change. + +### Native condition state survives a full rebuild (Gitea #641) + +A full `RebuildAddressSpace` recreates every condition inactive + acknowledged. For **scripted** alarms the +engine re-asserts every non-normal condition after the reload (#487). **Native** conditions had nothing +equivalent: the host re-subscribes a driver's alarms only when its alarm-ref *set* changes, and a +transition-only source (Galaxy, OpcUaClient) has nothing to replay anyway — so a rebuild that left the alarm +set unchanged (adding one `UnsTagReference` to an alarm-bearing raw tag is enough to force one, as are +`ChangedEquipment`, a scripted-alarm edit, a raw container rename, an alarm severity/type edit, or any +surgical fallback to `SafeRebuild()`) left every active native alarm reading **normal** until its next device +transition. + +The node manager now carries native state across the rebuild itself: + +1. `RebuildAddressSpace` captures each **native** condition's live state off the **node** — Active, Acked, + Confirmed, Severity, Message, Quality, Time, `ActiveState.TransitionTime` — before clearing. Reading the + node, not a host-side cache, is what keeps a **client's** acknowledgement: the SDK applies it to the node + and neither the driver nor `NativeAlarmProjector` ever sees it. +2. `MaterialiseAlarmCondition(isNative: true)` for the same id in the same apply restores it. **Quality is + always restored** (it is otherwise re-annotated only on a connectivity edge, so even a normal condition + would sit at `BadWaitingForInitialData`); the rest only when the condition was off-normal (active or + unacknowledged). A normal condition keeps what the materialise built — message and authored severity + included — exactly the #487 rule. Shelving/Enabled are not carried (native conditions refuse them, #587). +3. `IOpcUaAddressSpaceSink.CompleteAddressSpaceApply`, which `OpcUaPublishActor` calls at the end of + **every** apply (in a `finally`), reports **one** condition event per restored condition — after the + notifier wiring, so the raw folder, every referencing equipment folder and `i=2253` items all get it, + stamped with the original `Time`, not the deploy time — and **discards** every capture no materialise + claimed, so a later deploy re-adding the same RawPath gets a fresh node (no resurrection). A kind swap + (native ↔ scripted on one id) is never restored. Node-only: no `/alerts` row, no historian record. + +Two companions keep the host and driver in step with the carried node: + +- `DriverHostActor` **prunes** `NativeAlarmProjector` to the still-deployed conditions on each apply + (`RetainOnly`) instead of clearing it. Clearing made the next driver Acknowledge of an active alarm + project `Active=false`, and the next Clear of an unacknowledged one project `Acked=true` — after *any* + deploy, rebuild or not. +- MTConnect's "last told" record is pruned to the conditions that **left** the subscribed set rather than + cleared on any set change; re-raising a surviving (carried) condition would un-acknowledge it. + +Accepted residual: a condition that clears at the device while the rebuild runs is restored active until +the driver's Clear arrives — which it does, because the driver's own record still holds it active. ## Condition event identity fields (what a client reads on the wire)