feat(daemon): alarms by name + RequestControlState + WatchHealth (Phase A complete)
A2 — alarms: optional 'name:' on alarm config (a LOCAL key — SEMI only
defines numeric ALID + freetext ALTX; field appended last so existing
{id, text, category} brace-inits compile unchanged), parsed by the loader,
checked by the validator, shipped in equipment.yaml. SetAlarm/ClearAlarm
RPCs resolve config name OR stringified ALID via a constructor snapshot.
A3 — control state + health: RequestControlState fires operator events on
the io thread (read_sync) and reports what the E30 table actually did —
ACCEPT iff the equipment landed in the requested state, CANNOT_DO_NOW naming
the actual state otherwise (the shipped table has no operator path to
EquipmentOffline; the test pins that honesty). ATTEMPT_ONLINE is rejected as
transient. WatchHealth streams an immediate snapshot then pushes on link/
control-state changes via service observers (add_link_observer +
add_control_state_observer — the HandlerSlot work paying off), spool depth
sampled at the 500ms poll; ends on cancel or engine stop.
Tests: daemon suite 61 -> 101 assertions (alarm lifecycle by name/id/unknown,
WatchHealth initial + change push, all four RequestControlState semantics);
loader test for the alarm name (present + absent fallback); core 467/3055.
Interop now 15 checks incl. gRPC SetAlarm -> host receives S5F1 ALCD=0x84
ALID=1, and RequestControlState(HOST_OFFLINE) -> GetControlState confirms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+18
-10
@@ -29,7 +29,7 @@ host and stays conformant while the tool software restarts/upgrades/crashes.
|
||||
| Daemon interop vs **secsgem-py** reference host | ✅ | `interop/daemon_interop.py` (via `gemd` compose service): gRPC `SetVariables(ChamberPressure=2.5)` + `FireEvent` → host receives `S6F11 CEID 300` carrying `<F4 2.5>` — value *and declared format* flow gRPC→engine→HSMS→host |
|
||||
| Daemon interop vs **secs4j** (Java) | ⬜ | mirror the secsgem-py harness against `interop/secs4j` |
|
||||
| `Subscribe` host→tool command stream | ⬜ | design settled (HCACK-4, see below); not implemented |
|
||||
| Remaining universal RPCs (alarms, `RequestControlState`, `WatchHealth`) | ⬜ | see plan (Phase A) |
|
||||
| Universal RPC surface complete (vars/events/alarms/control-state/health) | ✅ | Phase A done; daemon tests 101 assertions, interop 15 checks |
|
||||
| Python client package (the "beautiful API") | ⬜ | thin wrapper over generated stubs |
|
||||
|
||||
## Known issues (found in the 2026-06-10 audit; honest list)
|
||||
@@ -39,10 +39,9 @@ host and stays conformant while the tool software restarts/upgrades/crashes.
|
||||
observer (`HandlerSlot` primary+observers pattern), so the mirror survives
|
||||
`register_default_handlers` claiming the primary slot. `control_state()` is
|
||||
now safe from any thread.
|
||||
- ⬜ **Alarms have no name key.** `equipment.yaml` alarms carry only numeric
|
||||
`id` + freetext `text` (matches SEMI: ALID/ALTX; there is no standard short
|
||||
name). The name-based `SetAlarm`/`ClearAlarm` RPCs need an optional local
|
||||
`name:` field in the alarm config (fallback: stringified id).
|
||||
- ✅ ~~**Alarms have no name key.**~~ Optional `name:` added to the alarm
|
||||
config (loader + validator + shipped equipment.yaml); daemon RPCs accept
|
||||
the name or the stringified ALID.
|
||||
- ⬜ **`pvd_tool` predates the behaviour hook.** It still hard-codes
|
||||
`if (rcmd=="START") recipe->start(...)` in a router handler. Migrate it to
|
||||
`commands.set_handler` so the flagship example showcases the intended seam.
|
||||
@@ -132,11 +131,20 @@ debts tax every later phase, and the most valuable tests aren't automated.
|
||||
through the API, read back through the API — plus empty-query-returns-all,
|
||||
INVALID_ARGUMENT on unknown names, and a live round-trip check in
|
||||
`daemon_interop.py`.
|
||||
2. ⬜ Alarm `name:` config field + `SetAlarm`/`ClearAlarm` RPCs + tests.
|
||||
3. ⬜ `RequestControlState` (operator online/offline) + control-state atomic
|
||||
mirror (fixes the known race) + `WatchHealth` stream (link state from the
|
||||
selected/closed handlers, spool depth, control state).
|
||||
4. ⬜ Extend `test_daemon_service.cpp` + `daemon_interop.py` for all of the above.
|
||||
2. ✅ Alarm `name:` config field (optional local key; `name` appended LAST on
|
||||
the Alarm struct so existing brace-inits compile unchanged) + `SetAlarm`/
|
||||
`ClearAlarm` RPCs (addressable by config name AND stringified ALID).
|
||||
Validated end-to-end: gRPC `SetAlarm(chiller_temp_high)` -> secsgem-py host
|
||||
receives `S5F1 ALCD=0x84 ALID=1`.
|
||||
3. ✅ `RequestControlState` — fires operator events on the io thread and
|
||||
reports what the E30 table actually did (ACCEPT iff landed in the requested
|
||||
state; the shipped table has NO operator path to EquipmentOffline and the
|
||||
test pins that honesty). ✅ `WatchHealth` — initial snapshot + push on
|
||||
link/control-state change (+ spool depth sampled at 500ms); unit-tested
|
||||
incl. the change push; link state still SELECTED/DISCONNECTED only
|
||||
(CONNECTED reserved, TODO in code). Interop covers RequestControlState;
|
||||
WatchHealth external check rides with Phase B.
|
||||
4. ✅ Done per-item above (daemon suite at 101 assertions; interop at 15 checks).
|
||||
|
||||
### Phase B — the command stream (the big one)
|
||||
5. ⬜ Implement `Subscribe`/`CompleteCommand` per the design above, including
|
||||
|
||||
Reference in New Issue
Block a user