feat(daemon): GetVariables + read_sync — the standard mutable-read pattern

EquipmentRuntime::read_sync establishes THE pattern for reading mutable
engine state from gRPC/binding threads (Phase 0 item 6): post the read onto
the io thread (the model's single owner), wait on a future with a deadline,
nullopt => UNAVAILABLE at the RPC edge. Always truthful, no cache to
invalidate; milliseconds are irrelevant at SECS rates.

GetVariables: name resolution against the service snapshot (empty query =
all; unknown name => INVALID_ARGUMENT naming the offender), values read via
read_sync, converted by the new from_item reverse conversion (single-element
numeric arrays => scalars, multi-element => List; Boolean/Binary/text per
format; C2-as-integer and U8>2^63 wrap documented as TODOs).

Tests run the engine in run_async — the daemon's PRODUCTION threading mode,
previously untested — and round-trip through both conversions: SetVariables
(declared-format write) then GetVariables (read) over a real in-process
channel. Daemon suite 41 -> 61 assertions. daemon_interop.py gains a live
GetVariables round-trip check vs the running daemon (verified green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 19:33:50 +02:00
parent b0a4c331cf
commit 1daf120431
5 changed files with 204 additions and 6 deletions
+9 -6
View File
@@ -113,9 +113,9 @@ debts tax every later phase, and the most valuable tests aren't automated.
(SVIDs 1/2 `refresh()`, CEIDs 400/401) with YAML role bindings
(`control_state_svid:`, `cj_executing_ceid:` …). Gradual; aligns with the
capability structure GEM itself defines (S1F19) and enables vendor subsetting.
6. **Standardize the mutable-read pattern** for daemon RPCs: post-to-io +
future with deadline (always truthful; latency irrelevant at SECS rates).
First consumer: `GetVariables` (Phase A1) — set the precedent there.
6. **Standardize the mutable-read pattern** `EquipmentRuntime::read_sync`
(post-to-io + future with deadline; nullopt => UNAVAILABLE at the RPC edge).
Precedent set by `GetVariables`; every future mutable read copies it.
7. ⬜ Move `apps/equipment_service.hpp` into the library tree
(`include/secsgem/daemon/`) once Phase B grows it; add a TSan-built
`run_async` + concurrent-RPC daemon test (today's daemon tests only poll()).
@@ -126,9 +126,12 @@ debts tax every later phase, and the most valuable tests aren't automated.
RPC edge (was silently writing ASCII "").
### Phase A — finish the universal daemon surface (small, unblock vendors)
1. `GetVariables`needs the reverse `Item → proto Value` conversion
(read via post-to-io + future, or serve from a daemon-side cache of last
set values; decide and document).
1. `GetVariables``from_item` reverse conversion (scalar for 1-element
arrays, List otherwise; C2-as-text and U8>2^63 noted as TODOs) + reads via
`read_sync`. Tested under **run_async (production threading)** — write
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