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:
2026-06-26 21:43:07 +02:00
parent 9876dd9b5a
commit a2ebbf7c65
14 changed files with 602 additions and 27 deletions
+55 -9
View File
@@ -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