feat(client): the Python client — a GEM tool in plain Python (Phase C)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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"
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,214 @@
|
||||
"""The Equipment client: a complete GEM tool integration in plain Python.
|
||||
|
||||
The secs_gemd daemon owns everything SEMI — the HSMS link to the host, the
|
||||
GEM state machines, message formats, timers, spooling. This client only ever
|
||||
does two things: tell the equipment about itself (variables, events, alarms)
|
||||
and react to what the host asks (commands). Names are the ones from your
|
||||
equipment.yaml; values are plain Python.
|
||||
|
||||
from secsgem_client import Equipment
|
||||
|
||||
eq = Equipment("localhost:50051")
|
||||
eq.set(ChamberPressure=2.5)
|
||||
eq.fire("ProcessStarted")
|
||||
|
||||
@eq.on("START")
|
||||
def start(cmd):
|
||||
run_recipe(cmd.params.get("PPID"))
|
||||
|
||||
eq.listen() # blocks, dispatching host commands to your handlers
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from dataclasses import dataclass
|
||||
from typing import Callable, Dict, Iterator, Optional
|
||||
|
||||
import grpc
|
||||
|
||||
from ._proto import equipment_pb2 as pb
|
||||
from ._proto import equipment_pb2_grpc as rpc
|
||||
from ._values import from_value, to_value
|
||||
|
||||
|
||||
class SecsGemError(RuntimeError):
|
||||
"""The daemon declined a request (unknown name, bad value, wrong state)."""
|
||||
|
||||
def __init__(self, code: int, message: str):
|
||||
super().__init__(message or pb.Ack.Code.Name(code))
|
||||
self.code = code
|
||||
|
||||
|
||||
def _check(ack: pb.Ack) -> 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))
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -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)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user