From 8686654b15e85ddd41d74c0db660ae10ab672431 Mon Sep 17 00:00:00 2001 From: Raphael Maenle Date: Wed, 10 Jun 2026 22:57:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(client):=20the=20Python=20client=20?= =?UTF-8?q?=E2=80=94=20a=20GEM=20tool=20in=20plain=20Python=20(Phase=20C)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit clients/python: pip-installable "secsgem-client", pure Python (stubs pre-generated from equipment.proto, import made package-relative; no compiled extension, no SEMI knowledge, no C++ toolchain). The API the whole effort aimed at: eq = Equipment("localhost:50051") eq.set(ChamberPressure=2.5); eq["WaferCounter"] = 7 eq.fire("ProcessStarted", ChamberPressure=2.75) eq.alarm("chiller_temp_high"); eq.clear("chiller_temp_high") @eq.on("START") def start(cmd): ... # auto-CompleteCommand after return eq.listen(background=True) eq.control_state; eq.request_control_state("HOST_OFFLINE"); eq.health() Errors raise SecsGemError carrying the daemon's message ("no variable named ..."). bool checked before int in conversion (isinstance(True, int)). examples/mini_tool.py is a complete GEM tool in ~25 lines. PROOF — interop/pyclient_interop.py drives the PUBLISHED package (not raw stubs) against a live secs_gemd with secsgem-py as the fab host: 13 checks all green on first run — set/get round-trips, item syntax, SecsGemError on unknown names, control state, health, fire->S6F11 on the host's wire, alarm/clear->S5F1 with correct set bit, the full command loop (host S2F41 -> HCACK=4 -> @eq.on handler -> completion event back at the host), operator offline. Conversion layer unit-tested standalone; both wired into tools/run_interop.sh as the pyclient step. Co-Authored-By: Claude Fable 5 --- clients/python/README.md | 30 + clients/python/examples/mini_tool.py | 31 + clients/python/pyproject.toml | 14 + clients/python/secsgem_client/__init__.py | 12 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 663 bytes .../__pycache__/_client.cpython-312.pyc | Bin 0 -> 14574 bytes .../__pycache__/_values.cpython-312.pyc | Bin 0 -> 2566 bytes clients/python/secsgem_client/_client.py | 214 +++++++ .../python/secsgem_client/_proto/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 150 bytes .../__pycache__/equipment_pb2.cpython-312.pyc | Bin 0 -> 8041 bytes .../equipment_pb2_grpc.cpython-312.pyc | Bin 0 -> 21993 bytes .../secsgem_client/_proto/equipment_pb2.py | 102 ++++ .../_proto/equipment_pb2_grpc.py | 546 ++++++++++++++++++ clients/python/secsgem_client/_values.py | 46 ++ clients/python/tests/test_values.py | 35 ++ docs/DAEMON_ROADMAP.md | 23 +- interop/pyclient_interop.py | 175 ++++++ tools/run_interop.sh | 14 + 19 files changed, 1235 insertions(+), 7 deletions(-) create mode 100644 clients/python/README.md create mode 100644 clients/python/examples/mini_tool.py create mode 100644 clients/python/pyproject.toml create mode 100644 clients/python/secsgem_client/__init__.py create mode 100644 clients/python/secsgem_client/__pycache__/__init__.cpython-312.pyc create mode 100644 clients/python/secsgem_client/__pycache__/_client.cpython-312.pyc create mode 100644 clients/python/secsgem_client/__pycache__/_values.cpython-312.pyc create mode 100644 clients/python/secsgem_client/_client.py create mode 100644 clients/python/secsgem_client/_proto/__init__.py create mode 100644 clients/python/secsgem_client/_proto/__pycache__/__init__.cpython-312.pyc create mode 100644 clients/python/secsgem_client/_proto/__pycache__/equipment_pb2.cpython-312.pyc create mode 100644 clients/python/secsgem_client/_proto/__pycache__/equipment_pb2_grpc.cpython-312.pyc create mode 100644 clients/python/secsgem_client/_proto/equipment_pb2.py create mode 100644 clients/python/secsgem_client/_proto/equipment_pb2_grpc.py create mode 100644 clients/python/secsgem_client/_values.py create mode 100644 clients/python/tests/test_values.py create mode 100644 interop/pyclient_interop.py diff --git a/clients/python/README.md b/clients/python/README.md new file mode 100644 index 0000000..d36aa2c --- /dev/null +++ b/clients/python/README.md @@ -0,0 +1,30 @@ +# secsgem-client + +A complete GEM tool integration in plain Python. The +[`secs_gemd`](../../docs/DAEMON_ROADMAP.md) daemon owns everything SEMI — +the HSMS link to the host, the GEM state machines, formats, timers, +spooling; this client tells it about your tool and reacts to the host. + +```python +from secsgem_client import Equipment + +eq = Equipment("localhost:50051") + +eq.set(ChamberPressure=2.5) # host sees it on its next poll +eq.fire("ProcessStarted") # S6F11 to the host, report auto-assembled +eq.alarm("chiller_temp_high") # S5F1 (set), eq.clear(...) for clear + +@eq.on("START") # host remote commands -> your function +def start(cmd): + run_recipe(cmd.params.get("PPID")) + eq.fire("ProcessStarted") # the host's real completion signal + +eq.listen() # block and dispatch (background=True for a thread) +``` + +Names are the ones from your `equipment.yaml`; values are plain Python +(`float`, `int`, `bool`, `str`, `bytes`, lists). Errors raise +`SecsGemError` with the daemon's explanation ("no variable named ..."). + +No compiled extension, no SEMI knowledge, no C++ toolchain — `pip install` +and a running daemon is the whole setup. diff --git a/clients/python/examples/mini_tool.py b/clients/python/examples/mini_tool.py new file mode 100644 index 0000000..4f1f7aa --- /dev/null +++ b/clients/python/examples/mini_tool.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +"""A complete GEM tool in ~25 lines. + +Run secs_gemd, then this. The fab host can poll ChamberPressure, receive +ProcessStarted events, get alarms above the threshold, and START the tool — +all SEMI plumbing handled by the daemon. +""" +import random +import time + +from secsgem_client import Equipment + +eq = Equipment("localhost:50051") + + +@eq.on("START") +def start(cmd): + print("host says START", cmd.params) + eq.fire("ProcessStarted") + + +eq.listen(background=True) + +while True: + pressure = round(random.uniform(1.0, 3.0), 3) + eq.set(ChamberPressure=pressure) + if pressure > 2.9: + eq.alarm("chiller_temp_high") + else: + eq.clear("chiller_temp_high") + time.sleep(1) diff --git a/clients/python/pyproject.toml b/clients/python/pyproject.toml new file mode 100644 index 0000000..2a89cfa --- /dev/null +++ b/clients/python/pyproject.toml @@ -0,0 +1,14 @@ +[build-system] +requires = ["setuptools>=61"] +build-backend = "setuptools.build_meta" + +[project] +name = "secsgem-client" +version = "0.1.0" +description = "Drive a secs_gemd SECS/GEM equipment daemon from plain Python" +readme = "README.md" +requires-python = ">=3.9" +dependencies = ["grpcio>=1.50", "protobuf>=4.21"] + +[tool.setuptools.packages.find] +include = ["secsgem_client*"] diff --git a/clients/python/secsgem_client/__init__.py b/clients/python/secsgem_client/__init__.py new file mode 100644 index 0000000..f3b7279 --- /dev/null +++ b/clients/python/secsgem_client/__init__.py @@ -0,0 +1,12 @@ +"""secsgem_client — drive a SECS/GEM equipment daemon from plain Python. + +The secs_gemd daemon speaks SEMI (HSMS, GEM state machines, SECS-II) to the +fab host; this client speaks plain Python to the daemon. You never touch a +stream/function, a format code, or a numeric id — just the names from your +equipment.yaml. +""" + +from ._client import Command, Equipment, Health, SecsGemError + +__all__ = ["Equipment", "Command", "Health", "SecsGemError"] +__version__ = "0.1.0" diff --git a/clients/python/secsgem_client/__pycache__/__init__.cpython-312.pyc b/clients/python/secsgem_client/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b48fc4eedad61f36ceddd8fd2455cdec71c8f6bb GIT binary patch literal 663 zcmY*Wzi-qq7*slDM4(y@@mGO@35iS1xJmBLj12PAej z{uTxX22jQ_v4J`uG4bVoRN&#$XZyYP`8|Ky+lvUU@#pyRYe2~NZnIszO>;ZJ<~`vg zBRt@}GElv&r}|l6g;}Tu*&rYng!exL*^r0Xs2Rk=`WVHW$lZldG%uyl9&WDR0k`r> z0E5N!bdjD-=OA9!vQphOXF?ebOKTKVC6gMSHNG%9iK6F)fKH5}8s7q4CD<#6O7j^! zJYUQg6X*n;XI_9}c_FoMlhve0vsnz@fG0xwJr zT3iW>7j<3$i=4NDsdQQE+)JY;=xb@LVjgnC#RQDS6J0A|We$?B0=TT5@7@8LDdE;` z8&lipE{LRIs!XCE7$Awmc64f#Vw$)2rrW66;kjU?FWT_}Lpc*_YOS$xXhWpfepd!v z8De=eI7}WVhfm|3c0?&RIi=XFU9X`l(jC4-v9d(eE~RMJp)pE7l3Uawf0C3{Rk~)o zv|9a7`u{hllu9it|dcz?6eL#YfFXZ?pI=UWx M+rR&I*NzeJ7d5WPqW}N^ literal 0 HcmV?d00001 diff --git a/clients/python/secsgem_client/__pycache__/_client.cpython-312.pyc b/clients/python/secsgem_client/__pycache__/_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbfeca5417c1fdcdb3858c951156e0b8da34b8d3 GIT binary patch literal 14574 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 None: + if ack.code != pb.Ack.ACCEPT: + raise SecsGemError(ack.code, ack.message) + + +@dataclass +class Command: + """A remote command from the host (e.g. START). The host has already been + told "accepted, will finish later" — report the real outcome by firing an + event (success) or raising an alarm (failure) from your handler.""" + + id: str + name: str + params: Dict[str, object] + _eq: "Equipment" + + def done(self, ok: bool = True) -> None: + """Mark the command complete (for the daemon's audit trail). Called + automatically after your handler returns; call early if you want.""" + self._eq._complete(self.id, ok) + self._done = True + + +@dataclass +class Health: + link: str # "DISCONNECTED" | "CONNECTED" | "SELECTED" + control_state: str # "HOST_OFFLINE" | "ONLINE_REMOTE" | ... + spool_depth: int + + +class Equipment: + """A connection to the secs_gemd daemon — your equipment, by name.""" + + def __init__(self, address: str = "localhost:50051", + connect_timeout: float = 10.0): + self._channel = grpc.insecure_channel(address) + grpc.channel_ready_future(self._channel).result(timeout=connect_timeout) + self._stub = rpc.EquipmentStub(self._channel) + self._handlers: Dict[str, Callable[[Command], object]] = {} + self._listen_thread: Optional[threading.Thread] = None + self._stop = threading.Event() + + # ---- report state to the host ------------------------------------------- + + def set(self, values: Optional[Dict[str, object]] = None, **kwargs) -> None: + """Update status/data variables by name: eq.set(ChamberPressure=2.5). + The host sees the new values immediately when it polls.""" + merged = dict(values or {}) + merged.update(kwargs) + req = pb.VariableUpdate() + for name, v in merged.items(): + req.values[name].CopyFrom(to_value(v)) + _check(self._stub.SetVariables(req)) + + def get(self, *names: str) -> Dict[str, object]: + """Read variables back from the equipment. No names = all of them.""" + try: + snap = self._stub.GetVariables(pb.VariableQuery(names=list(names))) + except grpc.RpcError as e: + raise SecsGemError(pb.Ack.PARAMETER_INVALID, e.details()) from None + return {k: from_value(v) for k, v in snap.values.items()} + + def __setitem__(self, name: str, value) -> None: + self.set({name: value}) + + def __getitem__(self, name: str): + return self.get(name)[name] + + def fire(self, event: str, **data) -> None: + """Fire a collection event by name; the daemon assembles the configured + report and sends it to the host. kwargs set variable values for this + event first: eq.fire("WaferComplete", Thickness=1.2).""" + req = pb.Event(name=event) + for name, v in data.items(): + req.data[name].CopyFrom(to_value(v)) + _check(self._stub.FireEvent(req)) + + def alarm(self, name: str) -> None: + """Raise an alarm by its config name (or stringified ALID).""" + _check(self._stub.SetAlarm(pb.Alarm(name=name))) + + def clear(self, name: str) -> None: + """Clear a previously raised alarm.""" + _check(self._stub.ClearAlarm(pb.Alarm(name=name))) + + # ---- control state & health --------------------------------------------- + + @property + def control_state(self) -> str: + """The GEM control state, e.g. "ONLINE_REMOTE".""" + cs = self._stub.GetControlState(pb.Empty()) + return pb.ControlState.State.Name(cs.state) + + def request_control_state(self, desired: str) -> None: + """Operator-panel transition, e.g. eq.request_control_state("HOST_OFFLINE") + before maintenance. Raises if the E30 table says no from here.""" + req = pb.ControlStateRequest( + desired=pb.ControlState.State.Value(desired)) + _check(self._stub.RequestControlState(req)) + + def health(self) -> Health: + """One health snapshot (link / control state / spool depth).""" + for h in self._stub.WatchHealth(pb.Empty()): + return Health(pb.Health.LinkState.Name(h.link), + pb.ControlState.State.Name(h.control_state), + h.spool_depth) + raise SecsGemError(pb.Ack.CANNOT_DO_NOW, "health stream ended") + + def watch_health(self) -> Iterator[Health]: + """Yields a Health snapshot on every link/state change.""" + for h in self._stub.WatchHealth(pb.Empty()): + yield Health(pb.Health.LinkState.Name(h.link), + pb.ControlState.State.Name(h.control_state), + h.spool_depth) + + # ---- react to the host ---------------------------------------------------- + + def on(self, command: str): + """Decorator: run this function when the host sends `command`. + Use "*" to catch commands with no specific handler.""" + + def register(fn: Callable[[Command], object]): + self._handlers[command] = fn + return fn + + return register + + def listen(self, background: bool = False) -> None: + """Consume host requests and dispatch them to @eq.on handlers. + Blocks; pass background=True to run on a daemon thread instead.""" + if background: + self._listen_thread = threading.Thread(target=self._listen_loop, + daemon=True) + self._listen_thread.start() + return + self._listen_loop() + + def close(self) -> None: + self._stop.set() + self._channel.close() + + # ---- internals ------------------------------------------------------------- + + def _listen_loop(self) -> None: + try: + for hr in self._stub.Subscribe(pb.SubscribeRequest(client="secsgem_client")): + if self._stop.is_set(): + return + if not hr.HasField("command"): + continue # future HostRequest variants (jobs, carriers, ...) + cmd = hr.command + handler = self._handlers.get(cmd.name) or self._handlers.get("*") + command = Command(cmd.id, cmd.name, + {k: from_value(v) for k, v in cmd.params.items()}, + self) + ok = True + try: + if handler is not None: + handler(command) + except Exception: + ok = False + raise + finally: + if not getattr(command, "_done", False): + self._complete(cmd.id, ok) + except grpc.RpcError: + if not self._stop.is_set(): + raise + + def _complete(self, command_id: str, ok: bool) -> None: + ack = pb.Ack(code=pb.Ack.ACCEPT if ok else pb.Ack.REJECTED) + self._stub.CompleteCommand(pb.CommandResult(id=command_id, ack=ack)) diff --git a/clients/python/secsgem_client/_proto/__init__.py b/clients/python/secsgem_client/_proto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/clients/python/secsgem_client/_proto/__pycache__/__init__.cpython-312.pyc b/clients/python/secsgem_client/_proto/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..599a98a56bbd1d8bd29e39bfd768a33dbc1e1d09 GIT binary patch literal 150 zcmX@j%ge<81n19bW`XF(AOanHW&w&!XQ*V*Wb|9fP{ah}eFmxdWvZW8P@tcjlbM=V zQmkK4S(1^Tr(c|!T%4Yo8xIlIk1r_7FUi-BkI&4@EQycTE2zB1VUwGmQks)$SHuc5 Tg%OC0L5z>gjEsy$%s>_Z?64$_ literal 0 HcmV?d00001 diff --git a/clients/python/secsgem_client/_proto/__pycache__/equipment_pb2.cpython-312.pyc b/clients/python/secsgem_client/_proto/__pycache__/equipment_pb2.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..752f611794d654d878fffcd6610a1436ac55253b GIT binary patch literal 8041 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 literal 0 HcmV?d00001 diff --git a/clients/python/secsgem_client/_proto/equipment_pb2.py b/clients/python/secsgem_client/_proto/equipment_pb2.py new file mode 100644 index 0000000..13bb62a --- /dev/null +++ b/clients/python/secsgem_client/_proto/equipment_pb2.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: equipment.proto +# Protobuf Python Version: 4.25.0 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0f\x65quipment.proto\x12\nsecsgem.v1\"\x89\x01\n\x05Value\x12\x0e\n\x04text\x18\x01 \x01(\tH\x00\x12\x11\n\x07integer\x18\x02 \x01(\x12H\x00\x12\x0e\n\x04real\x18\x03 \x01(\x01H\x00\x12\x11\n\x07\x62oolean\x18\x04 \x01(\x08H\x00\x12\x10\n\x06\x62inary\x18\x05 \x01(\x0cH\x00\x12 \n\x04list\x18\x06 \x01(\x0b\x32\x10.secsgem.v1.ListH\x00\x42\x06\n\x04kind\"(\n\x04List\x12 \n\x05items\x18\x01 \x03(\x0b\x32\x11.secsgem.v1.Value\"\x07\n\x05\x45mpty\"\x8a\x01\n\x0eVariableUpdate\x12\x36\n\x06values\x18\x01 \x03(\x0b\x32&.secsgem.v1.VariableUpdate.ValuesEntry\x1a@\n\x0bValuesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.secsgem.v1.Value:\x02\x38\x01\"\x1e\n\rVariableQuery\x12\r\n\x05names\x18\x01 \x03(\t\"\x8e\x01\n\x10VariableSnapshot\x12\x38\n\x06values\x18\x01 \x03(\x0b\x32(.secsgem.v1.VariableSnapshot.ValuesEntry\x1a@\n\x0bValuesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.secsgem.v1.Value:\x02\x38\x01\"\x80\x01\n\x05\x45vent\x12\x0c\n\x04name\x18\x01 \x01(\t\x12)\n\x04\x64\x61ta\x18\x02 \x03(\x0b\x32\x1b.secsgem.v1.Event.DataEntry\x1a>\n\tDataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.secsgem.v1.Value:\x02\x38\x01\"\x15\n\x05\x41larm\x12\x0c\n\x04name\x18\x01 \x01(\t\"\"\n\x10SubscribeRequest\x12\x0e\n\x06\x63lient\x18\x01 \x01(\t\"\xa8\x01\n\x0c\x43ontrolState\x12-\n\x05state\x18\x01 \x01(\x0e\x32\x1e.secsgem.v1.ControlState.State\"i\n\x05State\x12\x15\n\x11\x45QUIPMENT_OFFLINE\x10\x00\x12\x12\n\x0e\x41TTEMPT_ONLINE\x10\x01\x12\x10\n\x0cHOST_OFFLINE\x10\x02\x12\x10\n\x0cONLINE_LOCAL\x10\x03\x12\x11\n\rONLINE_REMOTE\x10\x04\"F\n\x13\x43ontrolStateRequest\x12/\n\x07\x64\x65sired\x18\x01 \x01(\x0e\x32\x1e.secsgem.v1.ControlState.State\"\xbd\x02\n\x0bHostRequest\x12&\n\x07\x63ommand\x18\x01 \x01(\x0b\x32\x13.secsgem.v1.CommandH\x00\x12\x37\n\rcontrol_state\x18\x02 \x01(\x0b\x32\x1e.secsgem.v1.ControlStateChangeH\x00\x12.\n\x08\x63onstant\x18\x03 \x01(\x0b\x32\x1a.secsgem.v1.ConstantChangeH\x00\x12\x35\n\x0fprocess_program\x18\x04 \x01(\x0b\x32\x1a.secsgem.v1.ProcessProgramH\x00\x12,\n\x07\x63\x61rrier\x18\x05 \x01(\x0b\x32\x19.secsgem.v1.CarrierActionH\x00\x12-\n\x0bprocess_job\x18\x06 \x01(\x0b\x32\x16.secsgem.v1.ProcessJobH\x00\x42\t\n\x07request\"\x96\x01\n\x07\x43ommand\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12/\n\x06params\x18\x03 \x03(\x0b\x32\x1f.secsgem.v1.Command.ParamsEntry\x1a@\n\x0bParamsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.secsgem.v1.Value:\x02\x38\x01\"C\n\x12\x43ontrolStateChange\x12-\n\x05state\x18\x01 \x01(\x0e\x32\x1e.secsgem.v1.ControlState.State\"@\n\x0e\x43onstantChange\x12\x0c\n\x04name\x18\x01 \x01(\t\x12 \n\x05value\x18\x02 \x01(\x0b\x32\x11.secsgem.v1.Value\",\n\x0eProcessProgram\x12\x0c\n\x04ppid\x18\x01 \x01(\t\x12\x0c\n\x04\x62ody\x18\x02 \x01(\x0c\"\x95\x01\n\rCarrierAction\x12\x12\n\ncarrier_id\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\r\x12\x30\n\x06\x61\x63tion\x18\x03 \x01(\x0e\x32 .secsgem.v1.CarrierAction.Action\"0\n\x06\x41\x63tion\x12\r\n\tVERIFY_ID\x10\x00\x12\x0b\n\x07PROCEED\x10\x01\x12\n\n\x06\x43\x41NCEL\x10\x02\"\xae\x01\n\nProcessJob\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12\x0e\n\x06recipe\x18\x02 \x01(\t\x12-\n\x06\x61\x63tion\x18\x03 \x01(\x0e\x32\x1d.secsgem.v1.ProcessJob.Action\x12\x10\n\x08\x63\x61rriers\x18\x04 \x03(\t\"?\n\x06\x41\x63tion\x12\t\n\x05START\x10\x00\x12\x08\n\x04STOP\x10\x01\x12\t\n\x05PAUSE\x10\x02\x12\n\n\x06RESUME\x10\x03\x12\t\n\x05\x41\x42ORT\x10\x04\"9\n\rCommandResult\x12\n\n\x02id\x18\x01 \x01(\t\x12\x1c\n\x03\x61\x63k\x18\x02 \x01(\x0b\x32\x0f.secsgem.v1.Ack\"\x97\x01\n\x0fProcessJobState\x12\x0e\n\x06job_id\x18\x01 \x01(\t\x12\x30\n\x05state\x18\x02 \x01(\x0e\x32!.secsgem.v1.ProcessJobState.State\"B\n\x05State\x12\x0e\n\nSETTING_UP\x10\x00\x12\x0e\n\nPROCESSING\x10\x01\x12\x0c\n\x08\x43OMPLETE\x10\x02\x12\x0b\n\x07\x41\x42ORTED\x10\x03\"\xa1\x01\n\x0c\x43\x61rrierState\x12\x12\n\ncarrier_id\x18\x01 \x01(\t\x12\x0c\n\x04port\x18\x02 \x01(\r\x12-\n\x05state\x18\x03 \x01(\x0e\x32\x1e.secsgem.v1.CarrierState.State\x12\r\n\x05slots\x18\x04 \x03(\x08\"1\n\x05State\x12\x0b\n\x07WAITING\x10\x00\x12\r\n\tIN_ACCESS\x10\x01\x12\x0c\n\x08\x43OMPLETE\x10\x02\"\xbc\x01\n\x06Health\x12*\n\x04link\x18\x01 \x01(\x0e\x32\x1c.secsgem.v1.Health.LinkState\x12\x13\n\x0bspool_depth\x18\x02 \x01(\r\x12\x35\n\rcontrol_state\x18\x03 \x01(\x0e\x32\x1e.secsgem.v1.ControlState.State\":\n\tLinkState\x12\x10\n\x0c\x44ISCONNECTED\x10\x00\x12\r\n\tCONNECTED\x10\x01\x12\x0c\n\x08SELECTED\x10\x02\"\xd0\x01\n\x03\x41\x63k\x12\"\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x14.secsgem.v1.Ack.Code\x12\x0f\n\x07message\x18\x02 \x01(\t\"\x93\x01\n\x04\x43ode\x12\n\n\x06\x41\x43\x43\x45PT\x10\x00\x12\x13\n\x0fINVALID_COMMAND\x10\x01\x12\x11\n\rCANNOT_DO_NOW\x10\x02\x12\x15\n\x11PARAMETER_INVALID\x10\x03\x12\x1e\n\x1a\x41\x43\x43\x45PTED_WILL_FINISH_LATER\x10\x04\x12\x0c\n\x08REJECTED\x10\x05\x12\x12\n\x0eINVALID_OBJECT\x10\x06\x32\xe8\x05\n\tEquipment\x12;\n\x0cSetVariables\x12\x1a.secsgem.v1.VariableUpdate\x1a\x0f.secsgem.v1.Ack\x12G\n\x0cGetVariables\x12\x19.secsgem.v1.VariableQuery\x1a\x1c.secsgem.v1.VariableSnapshot\x12/\n\tFireEvent\x12\x11.secsgem.v1.Event\x1a\x0f.secsgem.v1.Ack\x12.\n\x08SetAlarm\x12\x11.secsgem.v1.Alarm\x1a\x0f.secsgem.v1.Ack\x12\x30\n\nClearAlarm\x12\x11.secsgem.v1.Alarm\x1a\x0f.secsgem.v1.Ack\x12>\n\x0fGetControlState\x12\x11.secsgem.v1.Empty\x1a\x18.secsgem.v1.ControlState\x12G\n\x13RequestControlState\x12\x1f.secsgem.v1.ControlStateRequest\x1a\x0f.secsgem.v1.Ack\x12\x44\n\tSubscribe\x12\x1c.secsgem.v1.SubscribeRequest\x1a\x17.secsgem.v1.HostRequest0\x01\x12=\n\x0f\x43ompleteCommand\x12\x19.secsgem.v1.CommandResult\x1a\x0f.secsgem.v1.Ack\x12@\n\x10ReportProcessJob\x12\x1b.secsgem.v1.ProcessJobState\x1a\x0f.secsgem.v1.Ack\x12:\n\rReportCarrier\x12\x18.secsgem.v1.CarrierState\x1a\x0f.secsgem.v1.Ack\x12\x36\n\x0bWatchHealth\x12\x11.secsgem.v1.Empty\x1a\x12.secsgem.v1.Health0\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'equipment_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + DESCRIPTOR._options = None + _globals['_VARIABLEUPDATE_VALUESENTRY']._options = None + _globals['_VARIABLEUPDATE_VALUESENTRY']._serialized_options = b'8\001' + _globals['_VARIABLESNAPSHOT_VALUESENTRY']._options = None + _globals['_VARIABLESNAPSHOT_VALUESENTRY']._serialized_options = b'8\001' + _globals['_EVENT_DATAENTRY']._options = None + _globals['_EVENT_DATAENTRY']._serialized_options = b'8\001' + _globals['_COMMAND_PARAMSENTRY']._options = None + _globals['_COMMAND_PARAMSENTRY']._serialized_options = b'8\001' + _globals['_VALUE']._serialized_start=32 + _globals['_VALUE']._serialized_end=169 + _globals['_LIST']._serialized_start=171 + _globals['_LIST']._serialized_end=211 + _globals['_EMPTY']._serialized_start=213 + _globals['_EMPTY']._serialized_end=220 + _globals['_VARIABLEUPDATE']._serialized_start=223 + _globals['_VARIABLEUPDATE']._serialized_end=361 + _globals['_VARIABLEUPDATE_VALUESENTRY']._serialized_start=297 + _globals['_VARIABLEUPDATE_VALUESENTRY']._serialized_end=361 + _globals['_VARIABLEQUERY']._serialized_start=363 + _globals['_VARIABLEQUERY']._serialized_end=393 + _globals['_VARIABLESNAPSHOT']._serialized_start=396 + _globals['_VARIABLESNAPSHOT']._serialized_end=538 + _globals['_VARIABLESNAPSHOT_VALUESENTRY']._serialized_start=297 + _globals['_VARIABLESNAPSHOT_VALUESENTRY']._serialized_end=361 + _globals['_EVENT']._serialized_start=541 + _globals['_EVENT']._serialized_end=669 + _globals['_EVENT_DATAENTRY']._serialized_start=607 + _globals['_EVENT_DATAENTRY']._serialized_end=669 + _globals['_ALARM']._serialized_start=671 + _globals['_ALARM']._serialized_end=692 + _globals['_SUBSCRIBEREQUEST']._serialized_start=694 + _globals['_SUBSCRIBEREQUEST']._serialized_end=728 + _globals['_CONTROLSTATE']._serialized_start=731 + _globals['_CONTROLSTATE']._serialized_end=899 + _globals['_CONTROLSTATE_STATE']._serialized_start=794 + _globals['_CONTROLSTATE_STATE']._serialized_end=899 + _globals['_CONTROLSTATEREQUEST']._serialized_start=901 + _globals['_CONTROLSTATEREQUEST']._serialized_end=971 + _globals['_HOSTREQUEST']._serialized_start=974 + _globals['_HOSTREQUEST']._serialized_end=1291 + _globals['_COMMAND']._serialized_start=1294 + _globals['_COMMAND']._serialized_end=1444 + _globals['_COMMAND_PARAMSENTRY']._serialized_start=1380 + _globals['_COMMAND_PARAMSENTRY']._serialized_end=1444 + _globals['_CONTROLSTATECHANGE']._serialized_start=1446 + _globals['_CONTROLSTATECHANGE']._serialized_end=1513 + _globals['_CONSTANTCHANGE']._serialized_start=1515 + _globals['_CONSTANTCHANGE']._serialized_end=1579 + _globals['_PROCESSPROGRAM']._serialized_start=1581 + _globals['_PROCESSPROGRAM']._serialized_end=1625 + _globals['_CARRIERACTION']._serialized_start=1628 + _globals['_CARRIERACTION']._serialized_end=1777 + _globals['_CARRIERACTION_ACTION']._serialized_start=1729 + _globals['_CARRIERACTION_ACTION']._serialized_end=1777 + _globals['_PROCESSJOB']._serialized_start=1780 + _globals['_PROCESSJOB']._serialized_end=1954 + _globals['_PROCESSJOB_ACTION']._serialized_start=1891 + _globals['_PROCESSJOB_ACTION']._serialized_end=1954 + _globals['_COMMANDRESULT']._serialized_start=1956 + _globals['_COMMANDRESULT']._serialized_end=2013 + _globals['_PROCESSJOBSTATE']._serialized_start=2016 + _globals['_PROCESSJOBSTATE']._serialized_end=2167 + _globals['_PROCESSJOBSTATE_STATE']._serialized_start=2101 + _globals['_PROCESSJOBSTATE_STATE']._serialized_end=2167 + _globals['_CARRIERSTATE']._serialized_start=2170 + _globals['_CARRIERSTATE']._serialized_end=2331 + _globals['_CARRIERSTATE_STATE']._serialized_start=2282 + _globals['_CARRIERSTATE_STATE']._serialized_end=2331 + _globals['_HEALTH']._serialized_start=2334 + _globals['_HEALTH']._serialized_end=2522 + _globals['_HEALTH_LINKSTATE']._serialized_start=2464 + _globals['_HEALTH_LINKSTATE']._serialized_end=2522 + _globals['_ACK']._serialized_start=2525 + _globals['_ACK']._serialized_end=2733 + _globals['_ACK_CODE']._serialized_start=2586 + _globals['_ACK_CODE']._serialized_end=2733 + _globals['_EQUIPMENT']._serialized_start=2736 + _globals['_EQUIPMENT']._serialized_end=3480 +# @@protoc_insertion_point(module_scope) diff --git a/clients/python/secsgem_client/_proto/equipment_pb2_grpc.py b/clients/python/secsgem_client/_proto/equipment_pb2_grpc.py new file mode 100644 index 0000000..2291227 --- /dev/null +++ b/clients/python/secsgem_client/_proto/equipment_pb2_grpc.py @@ -0,0 +1,546 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +from . import equipment_pb2 as equipment__pb2 + + +class EquipmentStub(object): + """============================================================================= + SECS/GEM Equipment API + + The daemon *is* one piece of SEMI equipment: it owns the HSMS link to the + host (MES) and speaks GEM on your behalf. Your tool software connects as a + client and only ever does two things: + + • tell the equipment about itself — set variables, fire events, raise alarms + • react to what the host asks for — receive commands/jobs, answer them + + Everything SECS lives inside the daemon: message framing, report definitions, + the GEM state machines, timers, spooling. You need no SEMI knowledge to use + this API. Items are addressed by the human names from your equipment config + (e.g. "chamber_pressure"), never by numeric SVID / CEID / ALID. + + CAPABILITY TIERS — wire up only what your equipment is: + • Universal — variables, events, alarms, control state, commands. Every tool. + • Carriers — E87 carrier/load-port flows. Only carrier-based equipment. + • Recipes — S7 process-program transfer. Only recipe-driven equipment. + • Jobs — E40 process jobs. Only job-based process/front-end equipment. + If a tier doesn't apply, you simply never receive its HostRequest variants and + never call its report RPCs. + ============================================================================= + + ---- Universal: report state to the host -------------------------------- + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.SetVariables = channel.unary_unary( + '/secsgem.v1.Equipment/SetVariables', + request_serializer=equipment__pb2.VariableUpdate.SerializeToString, + response_deserializer=equipment__pb2.Ack.FromString, + ) + self.GetVariables = channel.unary_unary( + '/secsgem.v1.Equipment/GetVariables', + request_serializer=equipment__pb2.VariableQuery.SerializeToString, + response_deserializer=equipment__pb2.VariableSnapshot.FromString, + ) + self.FireEvent = channel.unary_unary( + '/secsgem.v1.Equipment/FireEvent', + request_serializer=equipment__pb2.Event.SerializeToString, + response_deserializer=equipment__pb2.Ack.FromString, + ) + self.SetAlarm = channel.unary_unary( + '/secsgem.v1.Equipment/SetAlarm', + request_serializer=equipment__pb2.Alarm.SerializeToString, + response_deserializer=equipment__pb2.Ack.FromString, + ) + self.ClearAlarm = channel.unary_unary( + '/secsgem.v1.Equipment/ClearAlarm', + request_serializer=equipment__pb2.Alarm.SerializeToString, + response_deserializer=equipment__pb2.Ack.FromString, + ) + self.GetControlState = channel.unary_unary( + '/secsgem.v1.Equipment/GetControlState', + request_serializer=equipment__pb2.Empty.SerializeToString, + response_deserializer=equipment__pb2.ControlState.FromString, + ) + self.RequestControlState = channel.unary_unary( + '/secsgem.v1.Equipment/RequestControlState', + request_serializer=equipment__pb2.ControlStateRequest.SerializeToString, + response_deserializer=equipment__pb2.Ack.FromString, + ) + self.Subscribe = channel.unary_stream( + '/secsgem.v1.Equipment/Subscribe', + request_serializer=equipment__pb2.SubscribeRequest.SerializeToString, + response_deserializer=equipment__pb2.HostRequest.FromString, + ) + self.CompleteCommand = channel.unary_unary( + '/secsgem.v1.Equipment/CompleteCommand', + request_serializer=equipment__pb2.CommandResult.SerializeToString, + response_deserializer=equipment__pb2.Ack.FromString, + ) + self.ReportProcessJob = channel.unary_unary( + '/secsgem.v1.Equipment/ReportProcessJob', + request_serializer=equipment__pb2.ProcessJobState.SerializeToString, + response_deserializer=equipment__pb2.Ack.FromString, + ) + self.ReportCarrier = channel.unary_unary( + '/secsgem.v1.Equipment/ReportCarrier', + request_serializer=equipment__pb2.CarrierState.SerializeToString, + response_deserializer=equipment__pb2.Ack.FromString, + ) + self.WatchHealth = channel.unary_stream( + '/secsgem.v1.Equipment/WatchHealth', + request_serializer=equipment__pb2.Empty.SerializeToString, + response_deserializer=equipment__pb2.Health.FromString, + ) + + +class EquipmentServicer(object): + """============================================================================= + SECS/GEM Equipment API + + The daemon *is* one piece of SEMI equipment: it owns the HSMS link to the + host (MES) and speaks GEM on your behalf. Your tool software connects as a + client and only ever does two things: + + • tell the equipment about itself — set variables, fire events, raise alarms + • react to what the host asks for — receive commands/jobs, answer them + + Everything SECS lives inside the daemon: message framing, report definitions, + the GEM state machines, timers, spooling. You need no SEMI knowledge to use + this API. Items are addressed by the human names from your equipment config + (e.g. "chamber_pressure"), never by numeric SVID / CEID / ALID. + + CAPABILITY TIERS — wire up only what your equipment is: + • Universal — variables, events, alarms, control state, commands. Every tool. + • Carriers — E87 carrier/load-port flows. Only carrier-based equipment. + • Recipes — S7 process-program transfer. Only recipe-driven equipment. + • Jobs — E40 process jobs. Only job-based process/front-end equipment. + If a tier doesn't apply, you simply never receive its HostRequest variants and + never call its report RPCs. + ============================================================================= + + ---- Universal: report state to the host -------------------------------- + """ + + def SetVariables(self, request, context): + """Update one or more status/data variables by name. The daemon remembers the + values, so the host always sees the latest when it polls (S1F3). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetVariables(self, request, context): + """Read back what the daemon currently holds (useful on tool restart/reconnect). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def FireEvent(self, request, context): + """Fire a collection event by name. The daemon assembles the configured report + and sends S6F11. Values in `data` override current values for this one event. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def SetAlarm(self, request, context): + """Raise (S5F1 set) or clear an alarm by name. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ClearAlarm(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def GetControlState(self, request, context): + """---- Universal: control state ------------------------------------------- + + Current GEM control state (ONLINE/LOCAL/REMOTE/OFFLINE). + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def RequestControlState(self, request, context): + """Request a transition — e.g. an operator panel taking the tool OFFLINE for + maintenance, or back ONLINE. The daemon applies E30 rules and may decline. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Subscribe(self, request, context): + """---- Universal: react to the host --------------------------------------- + + Subscribe to everything the host asks of this equipment. The daemon streams + HostRequest messages for as long as the call stays open. + + Delivery contract (v1): + - firehose: every subscriber receives every host request; + - NO buffering: a command arriving while no client is subscribed is + answered with its declarative ack from the equipment config (the + pre-daemon behaviour) and is NOT replayed on reconnect — the daemon + never tells the host "will finish later" for work no tool will do; + - when a Command does arrive here, the host has ALREADY been answered + with S2F42 HCACK=4; report the real outcome via FireEvent/SetAlarm. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def CompleteCommand(self, request, context): + """Report the outcome of a Command delivered on the stream, quoting its `id`. + NOTE the contract (SEMI-conformant, non-blocking): the daemon has ALREADY + answered the host with S2F42 HCACK=4 ("accepted, will finish later") when + it pushed the command onto the stream — the host's transaction is closed. + CompleteCommand therefore correlates/audits the command lifecycle; the + host learns the real outcome via the events/alarms you fire (FireEvent / + SetAlarm), exactly as E30 intends. A synchronous gating mode (tool decides + the HCACK before S2F42 goes out) is a possible v2 extension. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ReportProcessJob(self, request, context): + """---- Jobs / Carriers: report progress of work the host asked for -------- + Keyed by the durable id (job_id / carrier_id), not a per-message id — these + are long-lived objects you report against as the physical work proceeds. + + E40 — job-based tools + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def ReportCarrier(self, request, context): + """E87 — carrier-based tools + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def WatchHealth(self, request, context): + """---- Diagnostics -------------------------------------------------------- + + Live daemon/link status: distinguishes "host went offline" from "cable + unplugged" from "spool filling up". Streams a snapshot on every change. + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_EquipmentServicer_to_server(servicer, server): + rpc_method_handlers = { + 'SetVariables': grpc.unary_unary_rpc_method_handler( + servicer.SetVariables, + request_deserializer=equipment__pb2.VariableUpdate.FromString, + response_serializer=equipment__pb2.Ack.SerializeToString, + ), + 'GetVariables': grpc.unary_unary_rpc_method_handler( + servicer.GetVariables, + request_deserializer=equipment__pb2.VariableQuery.FromString, + response_serializer=equipment__pb2.VariableSnapshot.SerializeToString, + ), + 'FireEvent': grpc.unary_unary_rpc_method_handler( + servicer.FireEvent, + request_deserializer=equipment__pb2.Event.FromString, + response_serializer=equipment__pb2.Ack.SerializeToString, + ), + 'SetAlarm': grpc.unary_unary_rpc_method_handler( + servicer.SetAlarm, + request_deserializer=equipment__pb2.Alarm.FromString, + response_serializer=equipment__pb2.Ack.SerializeToString, + ), + 'ClearAlarm': grpc.unary_unary_rpc_method_handler( + servicer.ClearAlarm, + request_deserializer=equipment__pb2.Alarm.FromString, + response_serializer=equipment__pb2.Ack.SerializeToString, + ), + 'GetControlState': grpc.unary_unary_rpc_method_handler( + servicer.GetControlState, + request_deserializer=equipment__pb2.Empty.FromString, + response_serializer=equipment__pb2.ControlState.SerializeToString, + ), + 'RequestControlState': grpc.unary_unary_rpc_method_handler( + servicer.RequestControlState, + request_deserializer=equipment__pb2.ControlStateRequest.FromString, + response_serializer=equipment__pb2.Ack.SerializeToString, + ), + 'Subscribe': grpc.unary_stream_rpc_method_handler( + servicer.Subscribe, + request_deserializer=equipment__pb2.SubscribeRequest.FromString, + response_serializer=equipment__pb2.HostRequest.SerializeToString, + ), + 'CompleteCommand': grpc.unary_unary_rpc_method_handler( + servicer.CompleteCommand, + request_deserializer=equipment__pb2.CommandResult.FromString, + response_serializer=equipment__pb2.Ack.SerializeToString, + ), + 'ReportProcessJob': grpc.unary_unary_rpc_method_handler( + servicer.ReportProcessJob, + request_deserializer=equipment__pb2.ProcessJobState.FromString, + response_serializer=equipment__pb2.Ack.SerializeToString, + ), + 'ReportCarrier': grpc.unary_unary_rpc_method_handler( + servicer.ReportCarrier, + request_deserializer=equipment__pb2.CarrierState.FromString, + response_serializer=equipment__pb2.Ack.SerializeToString, + ), + 'WatchHealth': grpc.unary_stream_rpc_method_handler( + servicer.WatchHealth, + request_deserializer=equipment__pb2.Empty.FromString, + response_serializer=equipment__pb2.Health.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'secsgem.v1.Equipment', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class Equipment(object): + """============================================================================= + SECS/GEM Equipment API + + The daemon *is* one piece of SEMI equipment: it owns the HSMS link to the + host (MES) and speaks GEM on your behalf. Your tool software connects as a + client and only ever does two things: + + • tell the equipment about itself — set variables, fire events, raise alarms + • react to what the host asks for — receive commands/jobs, answer them + + Everything SECS lives inside the daemon: message framing, report definitions, + the GEM state machines, timers, spooling. You need no SEMI knowledge to use + this API. Items are addressed by the human names from your equipment config + (e.g. "chamber_pressure"), never by numeric SVID / CEID / ALID. + + CAPABILITY TIERS — wire up only what your equipment is: + • Universal — variables, events, alarms, control state, commands. Every tool. + • Carriers — E87 carrier/load-port flows. Only carrier-based equipment. + • Recipes — S7 process-program transfer. Only recipe-driven equipment. + • Jobs — E40 process jobs. Only job-based process/front-end equipment. + If a tier doesn't apply, you simply never receive its HostRequest variants and + never call its report RPCs. + ============================================================================= + + ---- Universal: report state to the host -------------------------------- + """ + + @staticmethod + def SetVariables(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/SetVariables', + equipment__pb2.VariableUpdate.SerializeToString, + equipment__pb2.Ack.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetVariables(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/GetVariables', + equipment__pb2.VariableQuery.SerializeToString, + equipment__pb2.VariableSnapshot.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def FireEvent(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/FireEvent', + equipment__pb2.Event.SerializeToString, + equipment__pb2.Ack.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def SetAlarm(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/SetAlarm', + equipment__pb2.Alarm.SerializeToString, + equipment__pb2.Ack.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ClearAlarm(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/ClearAlarm', + equipment__pb2.Alarm.SerializeToString, + equipment__pb2.Ack.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def GetControlState(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/GetControlState', + equipment__pb2.Empty.SerializeToString, + equipment__pb2.ControlState.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def RequestControlState(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/RequestControlState', + equipment__pb2.ControlStateRequest.SerializeToString, + equipment__pb2.Ack.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def Subscribe(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/secsgem.v1.Equipment/Subscribe', + equipment__pb2.SubscribeRequest.SerializeToString, + equipment__pb2.HostRequest.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def CompleteCommand(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/CompleteCommand', + equipment__pb2.CommandResult.SerializeToString, + equipment__pb2.Ack.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ReportProcessJob(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/ReportProcessJob', + equipment__pb2.ProcessJobState.SerializeToString, + equipment__pb2.Ack.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def ReportCarrier(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/secsgem.v1.Equipment/ReportCarrier', + equipment__pb2.CarrierState.SerializeToString, + equipment__pb2.Ack.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def WatchHealth(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_stream(request, target, '/secsgem.v1.Equipment/WatchHealth', + equipment__pb2.Empty.SerializeToString, + equipment__pb2.Health.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/clients/python/secsgem_client/_values.py b/clients/python/secsgem_client/_values.py new file mode 100644 index 0000000..904e089 --- /dev/null +++ b/clients/python/secsgem_client/_values.py @@ -0,0 +1,46 @@ +"""Plain Python values <-> the wire's Value message. + +The daemon owns all SECS-II knowledge (it converts to each variable's +declared wire format); this layer only maps Python types onto the Value +oneof. Order matters in to_value: bool is checked before int because +isinstance(True, int) is True in Python. +""" + +from __future__ import annotations + +from ._proto import equipment_pb2 as pb + + +def to_value(v) -> pb.Value: + if isinstance(v, pb.Value): + return v + if isinstance(v, bool): + return pb.Value(boolean=v) + if isinstance(v, int): + return pb.Value(integer=v) + if isinstance(v, float): + return pb.Value(real=v) + if isinstance(v, str): + return pb.Value(text=v) + if isinstance(v, (bytes, bytearray)): + return pb.Value(binary=bytes(v)) + if isinstance(v, (list, tuple)): + return pb.Value(list=pb.List(items=[to_value(e) for e in v])) + raise TypeError(f"cannot convert {type(v).__name__} to a SECS value") + + +def from_value(v: pb.Value): + kind = v.WhichOneof("kind") + if kind == "text": + return v.text + if kind == "integer": + return v.integer + if kind == "real": + return v.real + if kind == "boolean": + return v.boolean + if kind == "binary": + return v.binary + if kind == "list": + return [from_value(e) for e in v.list.items] + return None # unset diff --git a/clients/python/tests/test_values.py b/clients/python/tests/test_values.py new file mode 100644 index 0000000..def8d2d --- /dev/null +++ b/clients/python/tests/test_values.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python3 +"""Conversion round-trips for the Value layer. Plain asserts — run directly.""" +import sys + +from secsgem_client._proto import equipment_pb2 as pb +from secsgem_client._values import from_value, to_value + + +def roundtrip(v): + return from_value(to_value(v)) + + +def main() -> int: + assert roundtrip(2.5) == 2.5 + assert roundtrip(7) == 7 + assert roundtrip(-3) == -3 + assert roundtrip(True) is True # bool BEFORE int: must stay boolean + assert roundtrip(False) is False + assert to_value(True).WhichOneof("kind") == "boolean" + assert to_value(1).WhichOneof("kind") == "integer" + assert roundtrip("wafer-17") == "wafer-17" + assert roundtrip(b"\x01\x02") == b"\x01\x02" + assert roundtrip([1, 2.5, "x", [True]]) == [1, 2.5, "x", [True]] + assert from_value(pb.Value()) is None # unset oneof + try: + to_value(object()) + raise SystemExit("expected TypeError for unconvertible type") + except TypeError: + pass + print("values: all conversion checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/DAEMON_ROADMAP.md b/docs/DAEMON_ROADMAP.md index a47d137..324e13d 100644 --- a/docs/DAEMON_ROADMAP.md +++ b/docs/DAEMON_ROADMAP.md @@ -30,7 +30,7 @@ host and stays conformant while the tool software restarts/upgrades/crashes. | 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 | +| Python client package (the "beautiful API") | ✅ | `clients/python` (`secsgem-client`); 13-check interop green via the published API | ## Known issues (found in the 2026-06-10 audit; honest list) @@ -173,12 +173,21 @@ debts tax every later phase, and the most valuable tests aren't automated. 7. ⬜ Java interop: `secs4j` host variant of the same scenario. ### Phase C — the beautiful Python client -8. ⬜ `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. -9. ⬜ Example: rewrite a minimal `pvd_tool`-equivalent in ~40 lines of Python - against the daemon; also migrate the C++ `pvd_tool` to `set_handler`. +8. ✅ `clients/python/` — pip-installable `secsgem-client`, pure Python, + stubs pre-generated (relative-import fixed). The full agreed API: + `eq.set(ChamberPressure=2.5)` kwargs + `eq["..."]` item syntax, `eq.get`, + `eq.fire(event, **data)`, `eq.alarm`/`eq.clear`, `eq.control_state`, + `eq.request_control_state`, `eq.health()`/`watch_health()`, and + `@eq.on("START")` + `eq.listen(background=...)` with auto-CompleteCommand. + Errors raise `SecsGemError` carrying the daemon's explanation. + PROOF: `interop/pyclient_interop.py` drives the PUBLISHED package against + a live daemon with secsgem-py as the host — 13 checks all green (S6F11/ + S5F1 set+clear on the wire, HCACK-4 command loop through the decorator, + operator offline). Conversion layer unit-tested (bool-before-int etc). + Wired into tools/run_interop.sh as the `pyclient` step. +9. 🚧 `clients/python/examples/mini_tool.py` — a complete GEM tool in ~25 + lines ✅. Migrating the C++ `pvd_tool` to EquipmentRuntime + capability + registration + `set_handler` ⬜. ### Phase D — GEM300 in-the-loop (process/carrier tools) 10. ⬜ Settle job/carrier semantics (who acks S16F5/S3F17, gate vs observe — diff --git a/interop/pyclient_interop.py b/interop/pyclient_interop.py new file mode 100644 index 0000000..f5c4294 --- /dev/null +++ b/interop/pyclient_interop.py @@ -0,0 +1,175 @@ +"""End-to-end validation of the secsgem_client Python package. + +The PUBLISHED client API (not raw stubs) plays the tool against a live +secs_gemd, while secsgem-py — the reference GEM implementation — plays the +fab host over HSMS. Every beautiful-API call is asserted against what the +host actually receives on the wire: + + eq.set / eq["..."] -> S1F3-visible values, GetVariables round-trip + eq.fire -> host receives S6F11 with the configured report + eq.alarm / eq.clear -> host receives S5F1 set/clear + @eq.on + eq.listen -> host's S2F41 gets HCACK=4, handler runs, + completion event reaches the host + eq.control_state / request_control_state / eq.health + +Exits 0 on success. +""" + +from __future__ import annotations + +import argparse +import logging +import sys +import threading +import time + +sys.path.insert(0, "/app/clients/python") + +import secsgem.common +import secsgem.gem +import secsgem.hsms +import secsgem.secs + +from secsgem_client import Equipment, SecsGemError + +LOG = logging.getLogger("pyclient-interop") +F = secsgem.secs.functions + + +def run(grpc_addr: str, hsms_host: str, hsms_port: int) -> int: + failures: list[str] = [] + + def check(label: str, ok: bool, detail: str = "") -> None: + LOG.info("[%s] %s%s", "OK " if ok else "FAIL", label, + f" — {detail}" if detail else "") + if not ok: + failures.append(label) + + # ---- the tool, via the published client API ---- + eq = Equipment(grpc_addr) + + started = threading.Event() + + @eq.on("START") + def _start(cmd): # noqa: ANN001 + started.set() + eq.fire("ProcessStarted") # the host's real completion signal + + eq.listen(background=True) + + # ---- the host, via secsgem-py ---- + settings = secsgem.hsms.HsmsSettings( + address=hsms_host, port=hsms_port, session_id=0, + connect_mode=secsgem.hsms.HsmsConnectMode.ACTIVE, + device_type=secsgem.common.DeviceType.HOST) + host = secsgem.gem.GemHostHandler(settings) + + ceid300 = threading.Event() + s5f1 = {} + s5f1_seen = threading.Event() + + def on_s6f11(_h, message): + body = host.settings.streams_functions.decode(message).get() + if isinstance(body, dict) and body.get("CEID") == 300: + ceid300.set() + host.send_response(F.SecsS06F12(0), message.header.system) + + def on_s5f1(_h, message): + body = host.settings.streams_functions.decode(message).get() + if isinstance(body, dict): + s5f1.update(body) + s5f1_seen.set() + host.send_response(F.SecsS05F02(0), message.header.system) + + host.register_stream_function(6, 11, on_s6f11) + host.register_stream_function(5, 1, on_s5f1) + host.enable() + try: + if not host.waitfor_communicating(timeout=15): + check("HSMS establish-communications", False) + return 1 + check("HSMS establish-communications", True) + host.send_and_waitfor_response(F.SecsS01F17()) + time.sleep(0.3) + + # ---- variables: kwargs, item syntax, read-back, errors ---- + eq.set(ChamberPressure=2.5, WaferCounter=7) + check("eq.set(kwargs) + eq.get round-trip", + eq.get("ChamberPressure", "WaferCounter") == + {"ChamberPressure": 2.5, "WaferCounter": 7}) + eq["ChamberPressure"] = 1.25 + check('eq["..."] item syntax', eq["ChamberPressure"] == 1.25) + try: + eq.set(NoSuchVariable=1) + check("unknown variable raises SecsGemError", False) + except SecsGemError as e: + check("unknown variable raises SecsGemError", "NoSuchVariable" in str(e)) + + # ---- control state / health ---- + check("eq.control_state", eq.control_state == "ONLINE_REMOTE", + eq.control_state) + h = eq.health() + check("eq.health()", h.link == "SELECTED" and + h.control_state == "ONLINE_REMOTE", str(h)) + + # ---- events: configure a report host-side, fire client-side ---- + host.send_and_waitfor_response( + F.SecsS02F33({"DATAID": 1, "DATA": [{"RPTID": 1, "VID": [101]}]})) + host.send_and_waitfor_response( + F.SecsS02F35({"DATAID": 1, "DATA": [{"CEID": 300, "RPTID": [1]}]})) + host.send_and_waitfor_response(F.SecsS02F37({"CEED": True, "CEID": [300]})) + eq.fire("ProcessStarted", ChamberPressure=2.75) + check("eq.fire -> host receives S6F11", ceid300.wait(timeout=10)) + + # ---- alarms ---- + host.send_and_waitfor_response(F.SecsS05F03({"ALED": 0x80, "ALID": 1})) + eq.alarm("chiller_temp_high") + got = s5f1_seen.wait(timeout=10) + check("eq.alarm -> host receives S5F1 (set)", + got and s5f1.get("ALID") == 1 and (int(s5f1.get("ALCD") or 0) & 0x80)) + s5f1_seen.clear() + eq.clear("chiller_temp_high") + got = s5f1_seen.wait(timeout=10) + check("eq.clear -> host receives S5F1 (clear)", + got and not (int(s5f1.get("ALCD") or 0) & 0x80)) + + # ---- the command loop through @eq.on + eq.listen ---- + ceid300.clear() + rsp = host.send_and_waitfor_response( + F.SecsS02F41({"RCMD": "START", "PARAMS": []})) + body = host.settings.streams_functions.decode(rsp).get() + check("host S2F41 -> HCACK=4", + isinstance(body, dict) and int(body.get("HCACK") or -1) == 4) + check("@eq.on('START') handler ran", started.wait(timeout=10)) + check("handler's eq.fire reached the host (completion signal)", + ceid300.wait(timeout=10)) + + # ---- operator offline via the client ---- + eq.request_control_state("HOST_OFFLINE") + check("request_control_state(HOST_OFFLINE)", + eq.control_state == "HOST_OFFLINE", eq.control_state) + finally: + host.disable() + eq.close() + + if failures: + LOG.error("FAILURES (%d): %s", len(failures), failures) + return 1 + LOG.info("all secsgem_client interop checks passed") + return 0 + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--grpc", default="gemd:50051") + ap.add_argument("--hsms-host", default="gemd") + ap.add_argument("--hsms-port", type=int, default=5000) + args = ap.parse_args() + logging.basicConfig(level=logging.INFO, format="%(message)s") + logging.getLogger("communication").setLevel(logging.WARNING) + logging.getLogger("hsms_connection").setLevel(logging.WARNING) + return run(args.grpc, args.hsms_host, args.hsms_port) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/run_interop.sh b/tools/run_interop.sh index bdae89b..f6c7164 100755 --- a/tools/run_interop.sh +++ b/tools/run_interop.sh @@ -89,6 +89,20 @@ compose run --rm --no-deps interop \ record daemon $? compose stop gemd >/dev/null 2>&1 +# ---- Python client package vs secs_gemd -------------------------------------- +note "pyclient: secsgem_client package + secsgem-py host vs secs_gemd" +( + set -e + compose run --rm --no-deps -e PYTHONPATH=/app/clients/python interop \ + python3 /app/clients/python/tests/test_values.py + compose up -d --no-deps gemd + sleep 2 + compose run --rm --no-deps -e PYTHONPATH=/app/clients/python interop \ + python3 pyclient_interop.py --grpc gemd:50051 --hsms-host gemd +) +record pyclient $? +compose stop gemd >/dev/null 2>&1 + # ---- spool persistence across restart --------------------------------------- note "spool: persistence across server restart" (