Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
12 KiB
Vendor Daemon & gRPC API — Status, Known Issues, and Plan to Fab-Readiness
This is a forward-looking roadmap, not a description of shipped behaviour. Every item carries a status marker. Do not read an item as "done" unless it says ✅. (Last full audit: 2026-06-10.)
Status legend: ✅ done · 🚧 in progress · ⬜ planned · ⚠️ risk/unknown
What this is
A vendor-facing daemon (secs_gemd) that runs the SECS/GEM engine as its
own process and exposes a small, name-based, language-agnostic API over gRPC,
so a tool's control software (in any language) can drive the equipment without
linking C++ or knowing SEMI. See proto/secsgem/v1/equipment.proto.
The point of the daemon model: it owns the durable HSMS relationship with the host and stays conformant while the tool software restarts/upgrades/crashes.
Current status (2026-06-10, end of day)
| Piece | Status | Notes |
|---|---|---|
proto/secsgem/v1/equipment.proto |
✅ | v1 surface designed: universal + carrier/recipe/job tiers, Subscribe stream, health |
HostCommandRegistry::set_handler behaviour hook |
✅ | the engine seam for command behaviour; tested |
EquipmentRuntime (engine owner) |
✅ | tested (test_runtime.cpp); secs_server runs entirely on it (live GEM300 demo passes) |
register_default_handlers (the 56 GEM handlers as a library fn) |
✅ | src/gem/default_handlers.cpp; tested (test_default_handlers.cpp) |
| gRPC/protobuf toolchain (Dockerfile + CMake codegen) | ✅ | grpc++ 1.51 / protoc 3.21; opt-in SECSGEM_DAEMON, graceful skip without grpc |
secs_gemd: SetVariables / FireEvent / GetControlState / GetVariables |
✅ | format-aware both directions (declared SECS-II formats on write, from_item on read) and thread-safe (snapshot maps + posted writes + read_sync reads). In-process gRPC tests incl. run_async production mode (test_daemon_service.cpp, 61 assertions) |
| 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) |
| Python client package (the "beautiful API") | ⬜ | thin wrapper over generated stubs |
Known issues (found in the 2026-06-10 audit; honest list)
- ✅
Fixed 2026-06-10: the runtime keeps an atomic control-state mirror updated via anGetControlStatecross-thread read.add_state_change_handlerobserver (HandlerSlotprimary+observers pattern), so the mirror survivesregister_default_handlersclaiming the primary slot.control_state()is now safe from any thread. - ⬜ Alarms have no name key.
equipment.yamlalarms carry only numericid+ freetexttext(matches SEMI: ALID/ALTX; there is no standard short name). The name-basedSetAlarm/ClearAlarmRPCs need an optional localname:field in the alarm config (fallback: stringified id). - ⬜
pvd_toolpredates the behaviour hook. It still hard-codesif (rcmd=="START") recipe->start(...)in a router handler. Migrate it tocommands.set_handlerso the flagship example showcases the intended seam. - ✅
Interop harnesses are manual.tools/run_interop.shruns all nine validation steps with one command (verified green); CI lanes added, pending first-push verification (Phase 0 item 2). - ⬜ TSan lane doesn't cover the daemon.
secs_gemd_testsshould also be built/run under-DSECSGEM_TSAN=ONonce the control-state mirror lands. - ⚠️ macOS bind-mount staleness can break Docker builds mid-edit (a build reading a half-synced source file). Not a product bug; re-run the build.
The Subscribe design (settled — implement to this)
S2F42 is an acknowledgement, not a completion: SEMI separates "I accept
your command" from "the work finished". The conformant, non-blocking flow:
- Host sends
S2F41 START. The engine'son_commandhandler (registered by the daemon) runs on the io thread. - If no tool client is subscribed → fall back to the YAML declarative ack.
If a tool is subscribed → push the command onto its
Subscribestream and returnHCACK=4(AcceptedWillFinishLater) immediately — never block the io thread or the T3 window on the tool. - The tool does the work and reports the outcome via
FireEvent(success event) /SetAlarm(failure) — exactly how secsgem-py applications and commercial gateways do it. CompleteCommandtherefore only correlates/audits the command lifecycle in v1. A synchronous gating mode (tool decides HCACK 0/2 before the S2F42 goes out) requires a deferred-reply mechanism in the engine — explicitly a v2 refinement, not needed for conformance.
Open sub-decisions to settle while implementing:
- Per-command routing (subscribe to specific RCMDs?) or one firehose? (v1: firehose.)
- Reconnect semantics: buffer commands while no subscriber (bounded queue + declarative fallback after timeout) or reject with HCACK 2? Must be decided and TESTED before calling the stream production-ready.
Plan — ordered next steps
Phase 0 — structural debts (from the 2026-06-10 design review; pay before sprinting)
The review's verdict: architecture and API bets are sound, but two structural debts tax every later phase, and the most valuable tests aren't automated.
- 🚧 Multi-observer callbacks (THE structural blocker — hit twice already).
HandlerSlot(primary slot keeps legacy set_ semantics; append-only add_ observers survive it) — done forControlStateMachine+ PJ/CJ stores, plus runtime atomic control-state mirror (race retired) andadd_link_observer(WatchHealth foundation). ⬜ Remaining: roll the same 3-line pattern onto the other single-slot classes (comm-state, EPT, exceptions, substrates, modules, carriers, E84) as each phase needs them — mechanical now that the type exists. - 🚧 CI the interop + conformance harnesses.
tools/run_interop.sh✅ — one command runs ALL nine validation steps (build, unit, daemon-unit, py-host 31 checks, conformance 47, daemon bridge, spool restart, tshark, secs4j 55) with a PASS/FAIL summary; verified green end-to-end 2026-06-10. CI ✅ added but UNVERIFIED until pushed: grpc deps +secs_gemd_testsin the build job (fails loudly if the daemon silently drops out), and a newpython-interoplane (py-host + conformance + daemon harness against localhost, no docker-in-docker). ⬜ Verify the lanes on the first push. - ✅ Fix
CompleteCommandproto comment — it described the rejected blocking model; now states the HCACK-4 contract. - 🚧 Table-driven handler conformance test — ✅
tests/test_handler_conformance.cpp: one ordered scenario drives 53 of the 56 handlers throughrouter.dispatchin-process (236 assertions), asserting paired replies, control-state landings, and the SxF0 abort fallback. Message-level golden frames: seeded with a hand-computed (E5-rules, not codec-derived) S1F13 pin — ⬜ extend to S5F1 + composed S6F11 (TODO in file). - ⬜ Decompose
register_default_handlersper GEM capability (it is a relocated main(), not a designed component) and replace magic constants (SVIDs 1/2refresh(), 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. - ✅ Standardize the mutable-read pattern —
EquipmentRuntime::read_sync(post-to-io + future with deadline; nullopt => UNAVAILABLE at the RPC edge). Precedent set byGetVariables; every future mutable read copies it. - ⬜ Move
apps/equipment_service.hppinto the library tree (include/secsgem/daemon/) once Phase B grows it; add a TSan-builtrun_async+ concurrent-RPC daemon test (today's daemon tests only poll()). - 🚧 Validate names are identifier-safe in
ConfigValidator(the Python client's kwargs API depends on it) — ⬜. Generalized format-compliance property test (iterates ALL configured variables via gRPC, asserts each keeps its declared wire format) — ✅, plus an unset-Valueguard at the RPC edge (was silently writing ASCII "").
Phase A — finish the universal daemon surface (small, unblock vendors)
- ✅
GetVariables—from_itemreverse conversion (scalar for 1-element arrays, List otherwise; C2-as-text and U8>2^63 noted as TODOs) + reads viaread_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 indaemon_interop.py. - ⬜ Alarm
name:config field +SetAlarm/ClearAlarmRPCs + tests. - ⬜
RequestControlState(operator online/offline) + control-state atomic mirror (fixes the known race) +WatchHealthstream (link state from the selected/closed handlers, spool depth, control state). - ⬜ Extend
test_daemon_service.cpp+daemon_interop.pyfor all of the above.
Phase B — the command stream (the big one)
- ⬜ Implement
Subscribe/CompleteCommandper the design above, including the no-subscriber fallback and bounded buffering. In-process gRPC tests: command arrives on stream; HCACK 4 on the wire; declarative fallback when unsubscribed. - ⬜ Extend
daemon_interop.py: secsgem-py host sendsS2F41 START→ gRPC tool receives it on the stream → tool fires completion event → host seesS6F11. (The full conformant loop against the reference implementation.) - ⬜ Java interop:
secs4jhost variant of the same scenario.
Phase C — the beautiful Python client
- ⬜
clients/python/package (pip install secsgem-client): wraps generated stubs in the agreed API —eq.set(chamber_pressure=2.5),eq.fire("wafer_complete", thickness=1.2),eq.alarm("pressure_high"),@eq.on("START")consuming the stream,eq.health(). Pure Python (no compiled ext). Ship stubs pre-generated. - ⬜ Example: rewrite a minimal
pvd_tool-equivalent in ~40 lines of Python against the daemon; also migrate the C++pvd_tooltoset_handler.
Phase D — GEM300 in-the-loop (process/carrier tools)
- ⬜ Settle job/carrier semantics (who acks S16F5/S3F17, gate vs observe —
see proto comments), then wire
ProcessJob/CarrierActiononto the stream +ReportProcessJob/ReportCarrierinto the PJ/CJ/carrier stores. - ⬜ Recipe download (
ProcessProgramon the stream when S7F3 lands) and EC-change notification (ConstantChangewhen S2F15 lands). - ⬜ Interop scenarios for jobs/carriers vs secsgem-py + secs4j.
Phase E — hardening & operations
- ⬜ gRPC exposure: default to localhost + document UDS; optional TLS creds.
- ⬜
tools/run_interop.sh+ CI lanes: all interop harnesses + TSan daemon lane. - ⬜ Daemon Prometheus metrics + supervised deployment recipe (systemd unit).
- ⬜ Remaining Layer-1 API: traces, limits, substrates/modules, terminal
services, spool depth/flush,
DescribeRPC.
Phase F — fab acceptance (parallel track; the hard gate)
- ⚠️ Standards correctness remains unverified against SEMI texts (behaviour
reconstructed without the standards; interop with secsgem-py/secs4j/Wireshark
mitigates but does not prove). The #1 fab-readiness risk; needs real
standards access and/or a fab's MES qualification run (
docs/MES_INTEROP.md). - ⬜ GEM compliance statement + manual matching the tool's data dictionary.
- ⬜ SECS-I serial driver (asio
serial_portadapter; FSM done) — only if a target tool uses RS-232.