Merge b640-641: native alarm state survives rebuild; projector pruned; AbCip ConditionId=RawPath (#641)
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
@@ -54,6 +54,58 @@ 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, Comment + ClientUserId, 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 (or the node's newer `Time`, if a client method call
|
||||
reported in the restore→complete window — that window can yield a second, state-correct event) — 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. AbCip's ALMD
|
||||
last-seen `InFaulted` record is likewise driver-owned and pruned to the set, so a re-subscribe (every
|
||||
reconnect / set change) does not re-raise a still-faulted alarm.
|
||||
- A routed **client** acknowledgement marks the projector's prior state acknowledged
|
||||
(`NativeAlarmProjector.MarkAcknowledged`), so a later driver Clear does not project it unacknowledged.
|
||||
|
||||
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. The capture
|
||||
is keyed by RawPath only, so a tag deleted and re-created under the same RawPath in one deploy inherits the old
|
||||
condition's state until its next transition.
|
||||
|
||||
## Condition event identity fields (what a client reads on the wire)
|
||||
|
||||
|
||||
@@ -264,6 +264,10 @@ analog alarms are a follow-up) and **disabled by default** — shops running FT
|
||||
Alarm & Events should keep it off and take alarms through the native route, since
|
||||
the projection semantics don't exactly mirror Rockwell FT A&E.
|
||||
|
||||
Each event carries `ConditionId` = the subscribed reference (the alarm tag's RawPath — the key the host
|
||||
routes native transitions on) and `Kind` = `Raise` / `Clear`. Before Gitea #641 the projection sent
|
||||
`"{ref}#active"` with no kind, so the host dropped every ALMD transition as unrouted.
|
||||
|
||||
## Testing
|
||||
|
||||
- **Unit tests** — `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Tests/` cover the driver, host-address parser, UDT planner, and alarm projection via fake tag runtimes.
|
||||
|
||||
@@ -363,6 +363,13 @@ Every alarm transition fires `OnAlarmEvent` with:
|
||||
Cleared alarms fire a second event with `" (cleared)"` appended to the message
|
||||
so downstream consumers can ignore the clear if they only care about raises.
|
||||
|
||||
> ⚠️ **Not routable to a v3 raw condition (found under Gitea #641).** The server routes a native transition
|
||||
> only when `ConditionId` equals an alarm-bearing raw tag's RawPath, and the host subscribes those RawPaths —
|
||||
> but this projection filters devices by **host address** (so a RawPath list matches no device and nothing is
|
||||
> polled) and emits `"{host}#{Type}:{AlarmNumber}"` with no `Kind`. FOCAS native alarms therefore reach no
|
||||
> Part 9 condition today. Fixing it needs a mapping decision (the CNC exposes one flat alarm list per device,
|
||||
> not one condition per tag), so it is tracked separately rather than patched.
|
||||
|
||||
## Handle recycling
|
||||
|
||||
FANUC CNCs have a finite FWLIB handle pool (~5–10 concurrent connections) and
|
||||
|
||||
@@ -475,11 +475,12 @@ and records a state only when an alarm handler was attached to receive it. Conse
|
||||
condition the driver owns `BadNoCommunication` on the connectivity edge (#477) and keeps its Active
|
||||
state; the reconnect edge re-annotates Good, after which the driver re-asserts any still-`Unavailable`
|
||||
condition's Bad quality on its re-subscribe.
|
||||
- A re-subscribe with a **changed** alarm set clears the whole told record: only an alarm-bearing raw-tag
|
||||
delta changes the set, the server applies that as a full rebuild, and a rebuild recreates every
|
||||
condition node inactive — so every active condition is re-asserted onto its new node. The **same** set
|
||||
(a reconnect) keeps the record. (A rebuild that leaves this driver's alarm set unchanged triggers no
|
||||
re-subscribe; that is a host-wide gap shared with every native source.)
|
||||
- A re-subscribe with a **changed** alarm set drops the told record of only the conditions that **left**
|
||||
the set. The server carries every native condition's state (acknowledgement included) across a full
|
||||
rebuild itself (Gitea #641 — see `docs/AlarmTracking.md`), so re-raising a surviving condition would
|
||||
only un-acknowledge it; a condition that left and later comes back was materialised fresh and is told
|
||||
again. The **same** set (a reconnect) keeps the whole record. A rebuild that leaves the alarm set
|
||||
unchanged triggers no re-subscribe, and needs none — the server-side carry-over covers it.
|
||||
- An alarm-only driver (no value tags) still runs the `/sample` pump.
|
||||
|
||||
**EVENT controlled vocabularies as OPC UA enumerations — not built (parked, low priority).** EVENT values (`Execution`,
|
||||
|
||||
Reference in New Issue
Block a user