fix(abcip): ALMA review round 1 + ALMD polls InAlarm (not InFaulted)
M1 drop-out is a no-row Unspecified+IsRestatement update (projector case); M2 ack judged over every declared limit; L1 ActiveAcknowledged for an already-acked new limit; L2 Bad Acked/severity carried/skipped; L3 host end-to-end test; L4 BOOL member-by-member tests + mixed ALMD/ALMA; L5 detection requires no plain Acked. ALMD active bit is InAlarm per Rockwell's ALMD reference; InFaulted is input health. Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+40
-21
@@ -258,7 +258,7 @@ The driver has two interchangeable wire transports, selected by one `DriverConfi
|
||||
## Alarm Projection
|
||||
|
||||
`IAlarmSource` is served by `AbCipAlarmProjection`, which polls each subscribed
|
||||
ALMD UDT's `InFaulted` + `Severity` members (or an ALMA's limit members, below) at
|
||||
ALMD UDT's `InAlarm` + `Severity` members (or an ALMA's limit members, below) at
|
||||
`AlarmPollInterval` and fires `OnAlarmEvent` on raise/clear transitions. It handles
|
||||
ALMD (digital) and ALMA (analog, `ALARM_ANALOG`, Gitea #611) and is **disabled by default** — shops running FT
|
||||
Alarm & Events should keep it off and take alarms through the native route, since
|
||||
@@ -268,33 +268,52 @@ Each event carries `ConditionId` = the subscribed reference (the alarm tag's Raw
|
||||
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.
|
||||
|
||||
**ALMD's active bit is `InAlarm`** (Rockwell's ALMD reference). Until hw4 the projection polled `InFaulted`, which
|
||||
is the *input-health* flag (Status.1, set when the program raises `InFault`), so a live ALMD would have raised on
|
||||
input faults and never on the alarm itself. Declare `InAlarm` (and `Severity`) on an ALMD tag.
|
||||
|
||||
### ALMA (analog alarms)
|
||||
|
||||
A subscribed ref is ALMA when its declared tag definition (`Structure` with `members`) carries any of
|
||||
`HHInAlarm` / `HInAlarm` / `LInAlarm` / `LLInAlarm` / `ROCPosInAlarm` / `ROCNegInAlarm`
|
||||
(`AbCipAlarmDetector.IsAlma`); anything else stays ALMD, so an ALMD with no declared members is unchanged.
|
||||
**Declare the members you want projected** — the projection reads only the limits declared (its
|
||||
`*InAlarm`, and where declared its `*Acked` and `*Severity`). Member names/types are from Rockwell's
|
||||
Studio 5000 "Analog Alarm (ALMA)" reference (BOOL status bits, DINT severities 1..1000).
|
||||
A subscribed ref is ALMA when its declared tag definition (`Structure` with `members`) carries a per-limit
|
||||
status bit — any of `HHInAlarm` / `HInAlarm` / `LInAlarm` / `LLInAlarm` / `ROCPosInAlarm` / `ROCNegInAlarm` — **and
|
||||
no plain `Acked`** (an ALMD member; `AbCipAlarmDetector.IsAlma`). Anything else stays ALMD, so an ALMD-shaped user
|
||||
UDT is not misclassified. ⚠️ **An ALMA authored without `members` cannot be told apart from an ALMD and is
|
||||
projected as an ALMD** (it would poll `InAlarm`/`Severity`, which an ALMA lacks, and never raise) — declare the members.
|
||||
**Declare the members you want projected**: the projection reads only the limits declared (their `*InAlarm`, and
|
||||
where declared their `*Acked` and `*Severity`). Names/types are from Rockwell's Studio 5000 "Analog Alarm (ALMA)"
|
||||
reference (BOOL status bits, DINT severities 1..1000). ⚠️ With `EnableDeclarationOnlyUdtGrouping` the whole-UDT
|
||||
decode computes offsets from the DECLARATION order, which for a partially declared ALMA is not the controller's
|
||||
layout — leave grouping off for ALMA tags (BOOL members are read member by member anyway).
|
||||
|
||||
- **One condition per ALMA tag**, `ConditionId` = the RawPath, `AlarmType` = `ALMA`. Active = any polled limit
|
||||
bit set. Severity (exact, `OpcUaSeverity`, plus the 4-bucket enum) and message (`ALMA <ref> HH alarm`,
|
||||
`H`, `L`, `LL`, `ROC+`, `ROC-`) come from the **most severe active limit** (ties: HH, LL, H, L, ROC+, ROC-).
|
||||
A missing/Bad severity member counts as 1.
|
||||
- **Transitions** (last-told state — the active-limit mask and all-acked — is driver-owned, so a re-subscribe
|
||||
never re-raises a still-active alarm): none→active `Raise` (or the atomic `ActiveAcknowledged` when every
|
||||
active limit's `*Acked` is already true — always so when the instruction's `AckRequired` is false);
|
||||
active→none `Clear`; a limit **newly** entering alarm while active `Retrigger` (new, unacknowledged
|
||||
occurrence); a limit merely dropping out `Unspecified` (message/severity update, state kept); the PLC
|
||||
acknowledging every active limit `Acknowledge`. A Bad read of any polled `*InAlarm` bit judges nothing that tick.
|
||||
- **Acknowledge** writes the ALMA's `OperAckAll` input (not `Acked`, which ALMA lacks); declare `OperAckAll`
|
||||
on the tag. The `*Acked` bits are instruction outputs and are never written.
|
||||
- **What the host is told** (last-told state — active-limit mask, acked, governing limit + severity — is
|
||||
driver-owned, so a re-subscribe never re-raises a still-active alarm):
|
||||
none→active `Raise`, or the atomic `ActiveAcknowledged` when every declared `*Acked` is true (always so when the
|
||||
instruction's `AckRequired` is false); active→none `Clear`; a limit **newly** entering while active `Retrigger`
|
||||
(a new unacknowledged occurrence) — or `ActiveAcknowledged` if nothing is pending ack, never
|
||||
Retrigger-then-Acknowledge; every declared `*Acked` bit going true while the condition was unacknowledged
|
||||
`Acknowledge`; a limit dropping out (the governing limit or its severity changes, no new occurrence) an
|
||||
`Unspecified` event flagged `IsRestatement`, which `NativeAlarmProjector` applies as a text/severity move with
|
||||
**no `/alerts` row**. A non-governing limit flickering emits nothing.
|
||||
- **"Acked" is judged over every declared limit, active or not.** In Logix an occurrence that returned to normal
|
||||
unacknowledged keeps its `*Acked` bit false, so it keeps the condition unacknowledged — a surviving, already-acked
|
||||
limit never turns it into a device "Acknowledged" for an excursion nobody acknowledged.
|
||||
- **Bad reads:** a Bad `*InAlarm` or severity read skips that tick (no false Clear, no text change); a Bad `*Acked`
|
||||
read carries the last-told ack state forward instead of recording "unacknowledged".
|
||||
- **Acknowledge** writes the ALMA's `OperAckAll` input (not `Acked`, which ALMA lacks); declare `OperAckAll` as a
|
||||
writable member. A failed acknowledge write is now logged at Warning (it used to vanish silently). The `*Acked` bits
|
||||
are instruction outputs and are never written.
|
||||
- **Not projected:** `Disabled`, `Suppressed` and `InFaulted` (input-health) — the event contract has no
|
||||
enable/suppress channel and ALMD does not use them either — and the aggregate `AnyInAlarmUnack` /
|
||||
per-limit `*InAlarmUnack` bits (derivable from the polled `*InAlarm` + `*Acked`).
|
||||
- **Evidence:** unit tests over fake runtimes only (`AbCipAlmaProjectionTests`). `ab_server` has no alarm
|
||||
instructions and the Z35162 press (CompactLogix 5069-L320ERS2) has no ALMA/ALMD, so there is **no wire-level
|
||||
or live gate** for ALMA (or ALMD); it needs a controller that actually runs the instruction (Logix Emulate tier).
|
||||
enable/suppress channel — and the aggregate `AnyInAlarmUnack` / per-limit `*InAlarmUnack` bits (derivable from the
|
||||
polled `*InAlarm` + `*Acked`).
|
||||
- **Evidence:** unit tests over fake runtimes (`AbCipAlmaProjectionTests`, BOOL members, member-by-member reads, plus a
|
||||
mixed ALMD+ALMA subscription) and a host-level test, `Runtime.Tests/AbCipAlmaNativeAlarmEndToEndTests`, that walks
|
||||
raise → escalate → drop-out → ack → unacked excursion → clear through `DriverHostActor` and asserts the Part 9
|
||||
updates and `/alerts` rows. `ab_server` has no alarm instructions and the Z35162 press (CompactLogix
|
||||
5069-L320ERS2) has no ALMA/ALMD, so there is **no wire-level or live gate** for ALMA or ALMD; it needs a controller
|
||||
that actually runs the instruction (Logix Emulate tier).
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ Depends on the ALMD UDT shape, which `ab_server` cannot emulate. The
|
||||
`OnAlarmEvent` raise/clear path + ack-write semantics are not exercised
|
||||
end-to-end.
|
||||
|
||||
Unit coverage: `AbCipAlarmProjectionTests` — fakes feed `InFaulted` /
|
||||
Unit coverage: `AbCipAlarmProjectionTests` — fakes feed `InAlarm` /
|
||||
`Severity` via `ValuesByOffset` + assert the emitted `AlarmEventArgs`.
|
||||
|
||||
### 3. Micro800 unconnected-only path
|
||||
|
||||
Reference in New Issue
Block a user