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
+6
View File
@@ -153,6 +153,12 @@ def run(grpc_addr: str, hsms_host: str, hsms_port: int, session_id: int,
check("gRPC SetVariables(ChamberPressure=2.5) -> ACCEPT",
ack.code == pb.Ack.ACCEPT, pb.Ack.Code.Name(ack.code))
# Read back through the API: SetVariables -> engine -> GetVariables.
snap = stub.GetVariables(pb.VariableQuery(names=["ChamberPressure"]))
got = snap.values["ChamberPressure"].real
check("gRPC GetVariables round-trips ChamberPressure=2.5",
abs(got - 2.5) < 0.01, f"got {got}")
ack = stub.FireEvent(pb.Event(name="ProcessStarted"))
check("gRPC FireEvent(ProcessStarted) -> ACCEPT",
ack.code == pb.Ack.ACCEPT, pb.Ack.Code.Name(ack.code))