FOCAS: native alarms never reach their Part 9 conditions #647

Closed
opened 2026-10-08 18:51:30 -04:00 by dohertj2 · 2 comments
Owner

Found during the #641 work (see reports/641.md, docs/drivers/FOCAS.md).

FocasDriver.SubscribeAsync treats its sourceNodeIds argument as a device-host filter, but the host passes RawPaths, so nothing is ever polled. Events it does raise carry a decorated ConditionId "{host}#{Type}:{AlarmNumber}" and no Kind, so they cannot match a materialised condition (ConditionId = RawPath).

Needs a design for mapping a CNC's flat per-device alarm list onto per-RawPath conditions (which raw tag owns which alarm number/type; how unmatched alarms surface). Read-only (no ack upstream). Out of scope of #641, which only fixed carry-over across rebuilds.

Found during the #641 work (see reports/641.md, docs/drivers/FOCAS.md). FocasDriver.SubscribeAsync treats its sourceNodeIds argument as a device-host filter, but the host passes RawPaths, so nothing is ever polled. Events it does raise carry a decorated ConditionId "{host}#{Type}:{AlarmNumber}" and no Kind, so they cannot match a materialised condition (ConditionId = RawPath). Needs a design for mapping a CNC's flat per-device alarm list onto per-RawPath conditions (which raw tag owns which alarm number/type; how unmatched alarms surface). Read-only (no ack upstream). Out of scope of #641, which only fixed carry-over across rebuilds.
dohertj2 added the driver/focas label 2026-10-08 18:51:30 -04:00
Author
Owner

Landed on master (482bafdf + 89961af6 + bac93744, merged e7dd7425), opus-reviewed twice. Design: an ALARM: address on a raw tag carrying the alarm object = one Part 9 condition, ConditionId = RawPath, with a Kind. Selectors *, <Type>, <Type>/<n>, <Type>/<lo>-<hi>, T<code>, OTHER. All 7 triage defects fixed (failed read ≠ empty list; told-record dedup; no throw when the projection is off; half-authored tags skipped + reported).

Review-driven host change (affects every native driver): a Raise on an already-Active condition with the same message is a re-statement — keeps the ack, writes no duplicate /alerts row — so a deploy that respawns a driver no longer un-acks active alarms (MTConnect had the same latent defect). OpcUaClient and the Galaxy snapshot now send Retrigger for an unacked condition with no prior record. Outages: transport fault ⇒ quality-only BadNoCommunication + Degraded health (alarm-only drivers can trip the watchdog); a CNC refusal of cnc_rdalmmsg2 ⇒ BadNotSupported, warn once, health untouched (no reconnect loop on a production control). Read-only: no FOCAS write path reachable; ack is server-side only.

Tests: FOCAS 506/506, Runtime 752/3 skip, full suite 12,994/0 failed. Left open for the live gate: fixture raise/clear/ack-survives-respawn/outage on docker-dev, AdminUI picker /run, passive read-only watch on the Makino. Known limit: the alarm-type names follow the 16i-era table; 31i codes unconfirmed (use ALARM:T<code>).

Landed on master (`482bafdf` + `89961af6` + `bac93744`, merged `e7dd7425`), opus-reviewed twice. Design: an `ALARM:` address on a raw tag carrying the `alarm` object = one Part 9 condition, ConditionId = RawPath, with a Kind. Selectors `*`, `<Type>`, `<Type>/<n>`, `<Type>/<lo>-<hi>`, `T<code>`, `OTHER`. All 7 triage defects fixed (failed read ≠ empty list; told-record dedup; no throw when the projection is off; half-authored tags skipped + reported). Review-driven host change (affects every native driver): a Raise on an already-Active condition with the **same message** is a re-statement — keeps the ack, writes no duplicate /alerts row — so a deploy that respawns a driver no longer un-acks active alarms (MTConnect had the same latent defect). OpcUaClient and the Galaxy snapshot now send Retrigger for an unacked condition with no prior record. Outages: transport fault ⇒ quality-only BadNoCommunication + Degraded health (alarm-only drivers can trip the watchdog); a CNC *refusal* of cnc_rdalmmsg2 ⇒ BadNotSupported, warn once, health untouched (no reconnect loop on a production control). Read-only: no FOCAS write path reachable; ack is server-side only. Tests: FOCAS 506/506, Runtime 752/3 skip, full suite 12,994/0 failed. **Left open for the live gate**: fixture raise/clear/ack-survives-respawn/outage on docker-dev, AdminUI picker /run, passive read-only watch on the Makino. Known limit: the alarm-type names follow the 16i-era table; 31i codes unconfirmed (use `ALARM:T<code>`).
Author
Owner

Live-gated and closing (master ec375538).

  • Fixture: Driver.FOCAS.IntegrationTests 14/14 (incl. Alarm_selectors), none env-skipped.
  • docker-dev rig, against otopcua-focas-fixture: ALARM:Overtravel and ALARM:* produce one event per transition — Raise, Retrigger on ALARM:* when a second alarm joins, Clear.
  • Ack survives respawn: acked via Client.CLI on the Primary, then a driver-recreating deploy → still Active+Acked, no second ACTIVATED row on /alerts. (Ack on the non-Primary is refused 0x80AF0000, by design.)
  • Outage: fixture stopped → condition Quality BadNoCommunication, still Active+Acked, /hosts DEGRADED; pause/unpause recovery to Good with no un-ack / re-raise.
  • AdminUI /raw: tag editor loads ALARM:* with the native-alarm toggle; reversed range ALARM:Servo/10-5 rejected on Save; address builder composes ALARM:*.
  • Makino Z-34184 (production, read-only): ~10 min passive watch of ALARM:*, ALARM:OTHER, ALARM:Servo, ALARM:T6 — every cnc_rdalmmsg2 OK and empty, zero events, health Healthy. No alarm occurred, so the CNC-refusal (BadNotSupported) path and 30i type codes were not observed live.

Found and fixed along the way:

  • Startup race: the alarm poll and fixed-tree loop opened connections concurrently, each disposing the other's client (two TCP sessions on the CNC, ObjectDisposedException). Per-device connect gate in FocasDriver.EnsureConnectedAsync; a caller's own cancellation no longer opens the backoff window. (c940349f, 54a00fd7)
  • Client.CLI alarms never printed EventId/ConditionNodeId, so ack was unusable from the CLI. (c6ab783c)
  • CI race in FocasAlarmProjectionTests (quality-only events before PauseAsync) — test fix only. (f594b29a)
Live-gated and closing (master `ec375538`). - Fixture: `Driver.FOCAS.IntegrationTests` 14/14 (incl. `Alarm_selectors`), none env-skipped. - docker-dev rig, against `otopcua-focas-fixture`: `ALARM:Overtravel` and `ALARM:*` produce one event per transition — Raise, Retrigger on `ALARM:*` when a second alarm joins, Clear. - Ack survives respawn: acked via Client.CLI on the Primary, then a driver-recreating deploy → still Active+Acked, no second ACTIVATED row on `/alerts`. (Ack on the non-Primary is refused `0x80AF0000`, by design.) - Outage: fixture stopped → condition Quality `BadNoCommunication`, still Active+Acked, `/hosts` DEGRADED; pause/unpause recovery to Good with no un-ack / re-raise. - AdminUI `/raw`: tag editor loads `ALARM:*` with the native-alarm toggle; reversed range `ALARM:Servo/10-5` rejected on Save; address builder composes `ALARM:*`. - Makino Z-34184 (production, read-only): ~10 min passive watch of `ALARM:*`, `ALARM:OTHER`, `ALARM:Servo`, `ALARM:T6` — every `cnc_rdalmmsg2` OK and empty, zero events, health Healthy. No alarm occurred, so the CNC-refusal (`BadNotSupported`) path and 30i type codes were not observed live. Found and fixed along the way: - Startup race: the alarm poll and fixed-tree loop opened connections concurrently, each disposing the other's client (two TCP sessions on the CNC, `ObjectDisposedException`). Per-device connect gate in `FocasDriver.EnsureConnectedAsync`; a caller's own cancellation no longer opens the backoff window. (`c940349f`, `54a00fd7`) - Client.CLI `alarms` never printed `EventId`/`ConditionNodeId`, so `ack` was unusable from the CLI. (`c6ab783c`) - CI race in `FocasAlarmProjectionTests` (quality-only events before `PauseAsync`) — test fix only. (`f594b29a`)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#647