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,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