Files
secs-gem/docs/DAEMON_ROADMAP.md
T
raphael 912304966f refactor(gem): decompose default handlers per GEM capability + YAML role bindings
register_default_handlers was a relocated app main(): one 1086-line function,
all-or-nothing. It is now 15 per-capability registration functions along the
lines GEM itself defines (S1F19): identification, equipment constants, clock,
event reports, remote commands, trace/limits, spooling, alarms, exceptions,
material tracking (E90/E116/E157), carriers (E87), recipes, object services
(E39), jobs (E40/E94), terminal services. A sensor-class tool registers three
functions instead of carrying carrier/job handlers it doesn't have;
register_default_handlers composes all 15. Each function derives exactly the
runtime aliases its handlers use (generated programmatically from the moved
bodies with boundary/substitution guards — zero hand-retyping).

Magic constants are gone: the control-state/clock SVIDs (were hardcoded 1/2)
and the CJ Executing/Completed CEIDs (were 400/401) now come from a "roles:"
block in equipment.yaml via EquipmentDescriptor, with historical defaults
when absent, loader parsing, and validation (CEID roles must name declared
events). The coupling is now visible in ONE file instead of silently split
between YAML and C++ — the exact drift class this repo's spec-as-data
philosophy exists to kill.

Tests: capability subsetting, role-driven SVID refresh via S1F3, roles
loader (shipped/custom/absent). Battery: core 473/3087 incl. the 53-handler
conformance sweep, daemon 125/125, live GEM300 demo (client exit 0), daemon
interop 20/20 vs secsgem-py.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 22:44:04 +02:00

14 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 + CompleteCommand HCACK-4 contract implemented + tested in-process AND live vs secsgem-py (full loop: S2F41 → stream → complete → S6F11)
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)

  • GetControlState cross-thread read. Fixed 2026-06-10: the runtime keeps an atomic control-state mirror updated via an add_state_change_handler 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. 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 AND the runtime. It still hard-codes START behaviour in a router handler and hand-wires its own main(). Migrate it to EquipmentRuntime + per-capability registration + commands.set_handler so the flagship example showcases the intended integration shape. (Phase C item 9.)
  • Interop harnesses are manual. tools/run_interop.sh runs 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. Covered locally + in CI with tools/tsan.supp (third-party-only suppressions). Caught + fixed a real test-side contract violation on its first run.
  • ⚠️ 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:

  1. Host sends S2F41 START. The engine's on_command handler (registered by the daemon) runs on the io thread.
  2. If no tool client is subscribed → fall back to the YAML declarative ack. If a tool is subscribed → push the command onto its Subscribe stream and return HCACK=4 (AcceptedWillFinishLater) immediately — never block the io thread or the T3 window on the tool.
  3. 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.
  4. CompleteCommand therefore 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.

Sub-decisions (settled 2026-06-10, implemented + tested):

  • v1 is a firehose: every subscriber receives every host request.
  • NO buffering: with no subscriber a command takes its declarative YAML ack and is not replayed on reconnect — never "will finish later" for work no tool will do. Documented in the proto's Subscribe contract.

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.

  1. 🚧 Multi-observer callbacks (THE structural blocker — hit twice already). HandlerSlot (primary slot keeps legacy set_ semantics; append-only add_ observers survive it) — done for ControlStateMachine + PJ/CJ stores, plus runtime atomic control-state mirror (race retired) and add_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.
  2. 🚧 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_tests in the build job (fails loudly if the daemon silently drops out), and a new python-interop lane (py-host + conformance + daemon harness against localhost, no docker-in-docker). Verify the lanes on the first push.
  3. Fix CompleteCommand proto comment — it described the rejected blocking model; now states the HCACK-4 contract.
  4. Table-driven handler conformance test — one ordered scenario drives 53 of the 56 handlers through router.dispatch (236 assertions). Golden frames: S1F13, S5F1, and a composed S6F11, all hand-computed from E5 rules (external pins, not codec-derived).
  5. Decomposed register_default_handlers into 15 per-capability functions (identification, ECs, clock, event reports, remote commands, trace/limits, spooling, alarms, exceptions, material tracking, carriers, recipes, object services, jobs, terminal) — vendors register only what their equipment is; register_default_handlers = all 15. Magic constants replaced by YAML role bindings (roles: block — control_state_svid, clock_svid, cj_executing_ceid, cj_completed_ceid) parsed into the descriptor with historical defaults, validated (CEID roles must be declared). Tested: subset registration, role-driven SVID refresh, roles loader (present/custom/absent); full battery green (473/3087 core incl. the 53-handler sweep, live GEM300 demo, 20-check daemon interop).
  6. Standardize the mutable-read patternEquipmentRuntime::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. equipment_service.hpp moved to include/secsgem/daemon/ (apps/ include-path hack removed). TSan daemon lane added locally + in CI (tools/tsan.supp suppresses UNinstrumented system libgrpc/libabsl internals only — our frames stay checked). The lane caught a real contract violation on its first run (a test reading the model from the test thread under run_async — fixed to read_sync); now TSan-clean with halt_on_error=1.
  8. Identifier-safe name validation: ConfigValidator warns (not errors) on non-identifier variable/event/alarm/command names — bindings expose names as kwargs/attributes. Format-compliance property test ; unset- Value guard .

Phase A — finish the universal daemon surface (small, unblock vendors)

  1. GetVariablesfrom_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 (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)

  1. Subscribe/CompleteCommand implemented per the HCACK-4 design. Reconnect decision settled and documented in the proto: no buffering — a command with no subscriber takes its declarative YAML ack (the honest pre-daemon behaviour) and is not replayed. Firehose fan-out; per-command forwarding handlers registered from the registry (new names()/spec() accessors); pending-id audit map. In-process tests drive a REAL S2F41 through the default-handler router on the io thread: HCACK 4 with a subscriber (params arrive on the stream), declarative Accept without, CompleteCommand known/unknown ids, fallback restored after unsubscribe.
  2. The full conformant loop runs against secsgem-py live: host S2F41 STARTS2F42 HCACK=4 → tool receives Command(name=START, id) on the stream → CompleteCommand → tool fires the event → host receives S6F11. (interop now 20 checks.)
  3. Java interop: secs4j host variant of the same scenario.

Phase C — the beautiful Python client

  1. 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.
  2. Example: rewrite a minimal pvd_tool-equivalent in ~40 lines of Python against the daemon; also migrate the C++ pvd_tool to set_handler.

Phase D — GEM300 in-the-loop (process/carrier tools)

  1. Settle job/carrier semantics (who acks S16F5/S3F17, gate vs observe — see proto comments), then wire ProcessJob/CarrierAction onto the stream + ReportProcessJob/ReportCarrier into the PJ/CJ/carrier stores.
  2. Recipe download (ProcessProgram on the stream when S7F3 lands) and EC-change notification (ConstantChange when S2F15 lands).
  3. Interop scenarios for jobs/carriers vs secsgem-py + secs4j.

Phase E — hardening & operations

  1. gRPC exposure: default to localhost + document UDS; optional TLS creds.
  2. tools/run_interop.sh + CI lanes: all interop harnesses + TSan daemon lane.
  3. Daemon Prometheus metrics + supervised deployment recipe (systemd unit).
  4. Remaining Layer-1 API: traces, limits, substrates/modules, terminal services, spool depth/flush, Describe RPC.

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_port adapter; FSM done) — only if a target tool uses RS-232.