feat(client)+feat(daemon): eq.names, @eq.command, E90/E157 RPCs
Python client:
- eq.names.event.* / .alarm.* / .command.* / .var.* / .constant.* —
autocomplete-able, typo-safe name lookup backed by the Describe RPC
(lazy, cached; AttributeError on bad name with close-match hints)
- @eq.command decorator — binds a handler by function name, validated
against the equipment's real command set at decoration time
- eq.report_substrate() — E90 wafer milestone reporting
- eq.report_module() — E157 module state reporting (auto-create)
Daemon (C++ service):
- ReportSubstrate RPC — drives E90 location + processing FSMs
- ReportModule RPC — drives E157 module FSM (auto-create on first report)
- ack_from_outcome() helper — consistent Ack mapping for read_sync results
Proto: SubstrateReport, ModuleReport, EquipmentDescription,
SpoolFlushRequest, TerminalMessage; Describe, FlushSpool,
SendTerminalMessage RPCs
Tests: C++ FSM test (journey + ghost rejection + E157 illegal jump);
interop coverage for names API and E90/E157 round-trip
Docs: ch42 RPC table + Python example updated; ch16 daemon-path section added
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,37 @@ wafer was at every moment.
|
||||
|
||||
---
|
||||
|
||||
## Daemon path (Python client)
|
||||
|
||||
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
|
||||
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")
|
||||
|
||||
# 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")
|
||||
```
|
||||
|
||||
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).
|
||||
|
||||
---
|
||||
|
||||
## Where to go next
|
||||
|
||||
You now know how every component of in-flight material is
|
||||
|
||||
@@ -31,7 +31,7 @@ drops; the daemon model covers the gap if *your software* drops.
|
||||
tool software (any language) secs_gemd fab host / MES
|
||||
┌──────────────────────────┐ gRPC ┌──────────────────────────┐ HSMS ┌────────┐
|
||||
│ set / fire / alarm │◄─────►│ EquipmentRuntime │◄────►│ MES │
|
||||
│ @on("START") handlers │ :50051│ + register_default_* │SECS-II└────────┘
|
||||
│ @command / @on handlers │ :50051│ + register_default_* │SECS-II└────────┘
|
||||
│ (restartable, crashable) │ │ + spool, timers, FSMs │
|
||||
└──────────────────────────┘ └──────────────────────────┘
|
||||
```
|
||||
@@ -63,9 +63,15 @@ stays F4 on the wire no matter what you send).
|
||||
| `FireEvent` | trigger a collection event; daemon assembles the configured report → S6F11 |
|
||||
| `SetAlarm` / `ClearAlarm` | S5F1 set/clear, by alarm `name:` (or stringified ALID) |
|
||||
| `GetControlState` / `RequestControlState` | read the E30 control state / operator transitions |
|
||||
| `ReportCarrier` | E87 carrier state transitions (WAITING / IN_ACCESS / COMPLETE) |
|
||||
| `ReportSubstrate` | E90 wafer tracking (ARRIVED / AT_WORK / PROCESSING / PROCESSED / AT_DESTINATION) |
|
||||
| `ReportModule` | E157 module tracking (NOT_EXECUTING / GENERAL_EXECUTING / STEP_EXECUTING / STEP_COMPLETED) |
|
||||
| `WatchHealth` | server stream: link state, control state, spool depth |
|
||||
| `Subscribe` | server stream: everything the host asks of the tool |
|
||||
| `CompleteCommand` | close a streamed command's audit entry |
|
||||
| `Describe` | all names this equipment exposes (variables, events, alarms, commands, constants) |
|
||||
| `FlushSpool` | drain or discard spooled messages |
|
||||
| `SendTerminalMessage` | S10F1 operator message to the host |
|
||||
|
||||
### The HCACK-4 command contract
|
||||
|
||||
@@ -102,28 +108,68 @@ eq.set(ChamberPressure=2.5) # host sees it on its next S1F3
|
||||
eq["WaferCounter"] = 7 # item syntax, same thing
|
||||
print(eq.get("ChamberPressure")) # read back through the daemon
|
||||
|
||||
eq.fire("ProcessStarted", ChamberPressure=2.75) # values, then S6F11
|
||||
eq.alarm("chiller_temp_high") # S5F1 set
|
||||
eq.clear("chiller_temp_high") # S5F1 clear
|
||||
# eq.names — autocomplete-able, typo-safe name lookup (fetched from Describe)
|
||||
eq.fire(eq.names.event.ProcessStarted) # typo → AttributeError at the line it happened
|
||||
eq.alarm(eq.names.alarm.chiller_temp_high)
|
||||
eq.clear(eq.names.alarm.chiller_temp_high)
|
||||
# Plain strings still work; names are a convenience, not a requirement.
|
||||
eq.fire("ProcessStarted", ChamberPressure=2.75)
|
||||
|
||||
@eq.on("START") # host S2F41 -> your function
|
||||
def start(cmd):
|
||||
@eq.command # function name IS the command name;
|
||||
def START(cmd): # validated against Describe at decoration time
|
||||
run_recipe(cmd.params.get("PPID"))
|
||||
eq.fire("ProcessStarted") # the host's completion signal
|
||||
eq.fire(eq.names.event.ProcessStarted)
|
||||
|
||||
# @eq.on("NAME") still works — use it when the name can't be a Python identifier
|
||||
# or when you prefer explicit strings.
|
||||
|
||||
eq.listen(background=True) # consume the Subscribe stream
|
||||
|
||||
eq.control_state # "ONLINE_REMOTE"
|
||||
eq.request_control_state("HOST_OFFLINE") # operator panel -> maintenance
|
||||
eq.health() # link / control state / spool depth
|
||||
|
||||
# E90 / E157 material tracking
|
||||
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_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")
|
||||
```
|
||||
|
||||
Anything the daemon declines raises `SecsGemError` with its explanation
|
||||
(`no variable named 'ChamberPresure'`). A complete runnable tool is
|
||||
[clients/python/examples/mini_tool.py](../clients/python/examples/mini_tool.py)
|
||||
(~25 lines). The package is validated end-to-end by
|
||||
`interop/pyclient_interop.py`: 13 checks driving the published API while
|
||||
secsgem-py judges the wire.
|
||||
`interop/pyclient_interop.py` driving the published API while secsgem-py
|
||||
judges the wire.
|
||||
|
||||
### `eq.names` — name autocomplete and typo safety
|
||||
|
||||
`eq.names` fetches `Describe` from the daemon once (lazy, cached), then
|
||||
exposes five sub-namespaces:
|
||||
|
||||
```python
|
||||
eq.names.event.ProcessStarted # → "ProcessStarted"
|
||||
eq.names.alarm.chiller_temp_high # → "chiller_temp_high"
|
||||
eq.names.command.START # → "START"
|
||||
eq.names.var.ChamberPressure # → "ChamberPressure"
|
||||
eq.names.constant.MaxPressure # → "MaxPressure"
|
||||
|
||||
# Typo → AttributeError with suggestions:
|
||||
# AttributeError: no event named 'ProcessStated'. Did you mean ProcessStarted?
|
||||
|
||||
# Membership test — useful in @eq.on guards:
|
||||
"START" in eq.names.command # → True
|
||||
```
|
||||
|
||||
`dir(eq.names.event)` lists all event names — REPL and IDE autocomplete
|
||||
work out of the box.
|
||||
|
||||
Other languages: generate stubs from the proto (`protoc` supports 11+
|
||||
languages) and wrap them the same way — the Python client is ~200 lines
|
||||
|
||||
Reference in New Issue
Block a user