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:
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3
|
||||
"""A cluster tool that tracks material — E90 wafers + E157 modules.
|
||||
|
||||
Where mini_tool.py is the bare quickstart, this shows the *material-tracking*
|
||||
side: when the host issues START, we run one wafer through one process module
|
||||
and report every milestone. The daemon turns each report into the standard
|
||||
GEM 300 collection events the host has subscribed to — we never touch a CEID.
|
||||
|
||||
build/secs_gemd --port 5000 --config-dir data # then run this
|
||||
|
||||
The host sees, per wafer: SubstrateArrived, module GeneralExecuting, the wafer
|
||||
Acquired, the step running, processing start/stop, step complete, the wafer
|
||||
delivered, and the module back to idle — the complete in-flight trace.
|
||||
"""
|
||||
import time
|
||||
|
||||
from secsgem_client import Equipment, Milestone, ModuleState
|
||||
|
||||
MODULE = "CHAMBER-A"
|
||||
|
||||
|
||||
def run_wafer(eq: Equipment, wafer: str, carrier: str, slot: int) -> None:
|
||||
"""One wafer's journey through one module. Milestone / ModuleState are
|
||||
importable enums — autocomplete-friendly and typo-checked — but plain
|
||||
strings ("ARRIVED", "STEP_EXECUTING") work identically if you prefer."""
|
||||
eq.report_substrate(wafer, Milestone.ARRIVED, carrier_id=carrier, slot=slot)
|
||||
eq.report_module(MODULE, ModuleState.GENERAL_EXECUTING) # module spins up
|
||||
|
||||
eq.report_substrate(wafer, Milestone.AT_WORK) # robot loads wafer
|
||||
eq.report_module(MODULE, ModuleState.STEP_EXECUTING) # recipe step begins
|
||||
eq.report_substrate(wafer, Milestone.PROCESSING)
|
||||
eq.fire(eq.names.event.ProcessStarted)
|
||||
|
||||
time.sleep(1) # ...the actual process...
|
||||
|
||||
eq.report_substrate(wafer, Milestone.PROCESSED)
|
||||
eq.report_module(MODULE, ModuleState.STEP_COMPLETED)
|
||||
eq.report_substrate(wafer, Milestone.AT_DESTINATION) # robot unloads wafer
|
||||
eq.report_module(MODULE, ModuleState.NOT_EXECUTING) # module idle again
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Context manager: the gRPC channel is closed for you on exit.
|
||||
with Equipment("localhost:50051") as eq:
|
||||
|
||||
@eq.command # bound by name, validated against the daemon
|
||||
def START(cmd): # the host's S2F41 START lands here
|
||||
carrier = cmd.params.get("CARRIER", "FOUP-1")
|
||||
for slot in range(1, 4): # three wafers out of the FOUP
|
||||
run_wafer(eq, f"{carrier}-W{slot}", carrier, slot)
|
||||
|
||||
eq.listen() # blocks, dispatching host commands
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user