feat(client): typo-safe protocol enums + context manager; add wafer_tool example

Interface cleanup so the report_* family matches the typo-safe ethos of
eq.names instead of leaking raw protobuf errors on a misspelled value.

- Milestone / ModuleState / JobState: importable str-enums (member == its
  wire name, so plain strings still work) — autocomplete + a typo-checked
  happy path. The clean rule: equipment-specific *names* live on eq.names;
  fixed protocol *value-sets* are enums.
- _enum_value(): resolves an enum-or-string arg client-side and, on a bad
  value, raises ValueError with a close-match hint *before* the wire. Wired
  into report_job / report_substrate / report_module / request_control_state
  (all previously raised a raw protobuf ValueError).
- Equipment is now a context manager (with Equipment(...) as eq: ...).
- examples/wafer_tool.py: a cluster tool tracking one wafer through one
  module end-to-end (E90 + E157), showing the enums + context manager.
- tests/test_enums.py: asserts the enums stay in lockstep with the proto and
  that the typo path is helpful. Wired into run_interop.sh (pyclient step).
- Interop drives both the enum and string forms on the wire + the ValueError
  typo path. Docs (ch16/ch42) updated; names-vs-enums rule documented.

All Python unit tests + 25 pyclient interop checks pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 22:09:48 +02:00
parent 2218b854ce
commit 8a55137e57
12 changed files with 290 additions and 82 deletions
+17 -15
View File
@@ -207,28 +207,30 @@ If your tool uses the daemon (`secs_gemd`) and the Python client, the
E90 and E157 RPCs are wrapped as two methods:
```python
from secsgem_client import Equipment
from secsgem_client import Equipment, Milestone, ModuleState
eq = Equipment("localhost:50051")
# E90 — substrate journey (daemon drives FSMs, fires CEIDs automatically)
eq.report_substrate("WFR-001", "ARRIVED", carrier_id="FOUP-7", slot=3)
eq.report_substrate("WFR-001", "AT_WORK")
eq.report_substrate("WFR-001", "PROCESSING")
eq.report_substrate("WFR-001", "PROCESSED")
eq.report_substrate("WFR-001", "AT_DESTINATION")
eq.report_substrate("WFR-001", Milestone.ARRIVED, carrier_id="FOUP-7", slot=3)
eq.report_substrate("WFR-001", Milestone.AT_WORK)
eq.report_substrate("WFR-001", Milestone.PROCESSING)
eq.report_substrate("WFR-001", Milestone.PROCESSED)
eq.report_substrate("WFR-001", Milestone.AT_DESTINATION)
# E157 — module state (module is auto-created on first report)
eq.report_module("CHAMBER-A", "GENERAL_EXECUTING")
eq.report_module("CHAMBER-A", "STEP_EXECUTING")
eq.report_module("CHAMBER-A", "STEP_COMPLETED")
eq.report_module("CHAMBER-A", "NOT_EXECUTING")
eq.report_module("CHAMBER-A", ModuleState.GENERAL_EXECUTING)
eq.report_module("CHAMBER-A", ModuleState.STEP_EXECUTING)
eq.report_module("CHAMBER-A", ModuleState.STEP_COMPLETED)
eq.report_module("CHAMBER-A", ModuleState.NOT_EXECUTING)
```
Milestones map to the `SubstrateReport.Milestone` protobuf enum;
module states to `ModuleReport.State`. The daemon's `ReportSubstrate`
handler validates FSM transitions and returns `INVALID_OBJECT` if the
substrate was never `ARRIVED` (which guarantees the daemon owns the
substrate record).
`Milestone` / `ModuleState` are importable enums (each member equals its
plain-string name, so `"ARRIVED"` works just as well). The daemon's
`ReportSubstrate` handler validates FSM transitions: a substrate that never
`ARRIVED` is rejected with `INVALID_OBJECT`, and a **duplicate** `ARRIVED`
with `CANNOT_DO_NOW` (`substrate '...' already exists`) — it never silently
re-creates over a wafer's live state. A complete worked example is
[clients/python/examples/wafer_tool.py](../clients/python/examples/wafer_tool.py).
---