feat(abcip): ALMA (ALARM_ANALOG) alarm projection (#611 item 1)
Detect ALMA by declared *InAlarm members; one condition per tag keyed on RawPath; severity/message from the most severe active limit; Raise/ActiveAcknowledged/Retrigger/Clear/Acknowledge; ack writes OperAckAll. Unit-tested over fake runtimes only (no ab_server ALMA support, Z35162 has none). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+34
-6
@@ -19,7 +19,7 @@ coverage map, see [AbServer-Test-Fixture.md](AbServer-Test-Fixture.md).
|
||||
|
||||
| Project | Role |
|
||||
|---------|------|
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/` | The driver — `AbCipDriver`, the libplctag runtime/enumerator/template-reader wrappers, the UDT read planner + template decoders, the host-address parser, and the ALMD alarm projection. |
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip/` | The driver — `AbCipDriver`, the libplctag runtime/enumerator/template-reader wrappers, the UDT read planner + template decoders, the host-address parser, and the ALMD/ALMA alarm projection. |
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Ab.LibplctagShared/` | Project-less shared source (`LibplctagOrphanRelease.cs`, #578/#580) compiled into both `Driver.AbCip` and `Driver.AbLegacy`. The `AbStatus` → OPC UA table is shared in `Driver.Ab.Transport` (`AbOpcUaStatusMap`). |
|
||||
| `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.AbCip.Contracts/` | `AbCipDriverOptions`, `AbCipDeviceOptions`, `AbCipTagDefinition` / `AbCipStructureMember`, and the `AbCipDataType` / `AbCipPlcFamily` enums bound from the driver's `DriverConfig` JSON. |
|
||||
|
||||
@@ -44,7 +44,7 @@ AB Legacy surface.
|
||||
| `ISubscribable` | `SubscribeAsync` driven by the shared `PollGroupEngine` | CIP has no push model — subscriptions become polling groups. |
|
||||
| `IHostConnectivityProbe` | `ProbeLoopAsync` + `GetHostStatuses` | One probe loop per device reading `Probe.ProbeTagPath`; no path configured ⇒ a warning is logged and the device stays `Unknown`. |
|
||||
| `IPerCallHostResolver` | `ResolveHost` | Routes each call to the tag's `DeviceHostAddress`, the breaker key for the resilience pipeline so one dead PLC trips only its own breaker. |
|
||||
| `IAlarmSource` | `AbCipAlarmProjection` (ALMD) | Opt-in via `EnableAlarmProjection`; off by default the subscribe path is a no-op so capability negotiation still works. |
|
||||
| `IAlarmSource` | `AbCipAlarmProjection` (ALMD + ALMA) | Opt-in via `EnableAlarmProjection`; off by default the subscribe path is a no-op so capability negotiation still works. |
|
||||
|
||||
## Addressing Model
|
||||
|
||||
@@ -80,7 +80,7 @@ the driver's `DriverConfig` JSON. Key fields:
|
||||
- AdminUI driver forms (AbCip/AbLegacy) overlay only the keys they edit onto the stored JSON, so unmodelled keys survive a save (#576). They also write durations as the integer-millisecond keys the factory binds (`timeoutMs`, `alarmPollIntervalMs`, `probe.intervalMs`, `probe.timeoutMs`); a legacy TimeSpan-string key (`timeout`: "00:00:05") is loaded and migrated on save.
|
||||
- **`Transport`** — absent (default ⇒ `Native`), `Native`, or `Libplctag` (fallback); see [Transport](#transport-transport-option).
|
||||
- **Discovery** — `EnableControllerBrowse` (`@tags` walk), `EnableDeclarationOnlyUdtGrouping` (whole-UDT read fast path) and `browsePageDelayMs` (native browse pacing, see [Native browse](#native-browse-paging-pacing-and-template-leases)).
|
||||
- **Alarms** — `EnableAlarmProjection` + `AlarmPollInterval` for the ALMD projection.
|
||||
- **Alarms** — `EnableAlarmProjection` + `AlarmPollInterval` for the ALMD/ALMA projection.
|
||||
|
||||
Full per-field descriptions live in `AbCipDriverOptions.cs`. The JSON skeleton is
|
||||
reproduced in [docs/v2/driver-specs.md §3](../v2/driver-specs.md).
|
||||
@@ -258,9 +258,9 @@ 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 at `AlarmPollInterval` and fires
|
||||
`OnAlarmEvent` on raise/clear transitions. It is **ALMD-only** in this pass (ALMA
|
||||
analog alarms are a follow-up) and **disabled by default** — shops running FT
|
||||
ALMD UDT's `InFaulted` + `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
|
||||
the projection semantics don't exactly mirror Rockwell FT A&E.
|
||||
|
||||
@@ -268,6 +268,34 @@ 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.
|
||||
|
||||
### 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).
|
||||
|
||||
- **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.
|
||||
- **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).
|
||||
|
||||
## 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.
|
||||
|
||||
Reference in New Issue
Block a user