diff --git a/clients/python/examples/wafer_tool.py b/clients/python/examples/wafer_tool.py new file mode 100644 index 0000000..8949b40 --- /dev/null +++ b/clients/python/examples/wafer_tool.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 +"""A cluster tool that tracks material — E90 wafers + E157 modules. + +Where mini_tool.py is the bare quickstart, this shows the *material-tracking* +side: when the host issues START, we run one wafer through one process module +and report every milestone. The daemon turns each report into the standard +GEM 300 collection events the host has subscribed to — we never touch a CEID. + + build/secs_gemd --port 5000 --config-dir data # then run this + +The host sees, per wafer: SubstrateArrived, module GeneralExecuting, the wafer +Acquired, the step running, processing start/stop, step complete, the wafer +delivered, and the module back to idle — the complete in-flight trace. +""" +import time + +from secsgem_client import Equipment, Milestone, ModuleState + +MODULE = "CHAMBER-A" + + +def run_wafer(eq: Equipment, wafer: str, carrier: str, slot: int) -> None: + """One wafer's journey through one module. Milestone / ModuleState are + importable enums — autocomplete-friendly and typo-checked — but plain + strings ("ARRIVED", "STEP_EXECUTING") work identically if you prefer.""" + eq.report_substrate(wafer, Milestone.ARRIVED, carrier_id=carrier, slot=slot) + eq.report_module(MODULE, ModuleState.GENERAL_EXECUTING) # module spins up + + eq.report_substrate(wafer, Milestone.AT_WORK) # robot loads wafer + eq.report_module(MODULE, ModuleState.STEP_EXECUTING) # recipe step begins + eq.report_substrate(wafer, Milestone.PROCESSING) + eq.fire(eq.names.event.ProcessStarted) + + time.sleep(1) # ...the actual process... + + eq.report_substrate(wafer, Milestone.PROCESSED) + eq.report_module(MODULE, ModuleState.STEP_COMPLETED) + eq.report_substrate(wafer, Milestone.AT_DESTINATION) # robot unloads wafer + eq.report_module(MODULE, ModuleState.NOT_EXECUTING) # module idle again + + +def main() -> None: + # Context manager: the gRPC channel is closed for you on exit. + with Equipment("localhost:50051") as eq: + + @eq.command # bound by name, validated against the daemon + def START(cmd): # the host's S2F41 START lands here + carrier = cmd.params.get("CARRIER", "FOUP-1") + for slot in range(1, 4): # three wafers out of the FOUP + run_wafer(eq, f"{carrier}-W{slot}", carrier, slot) + + eq.listen() # blocks, dispatching host commands + + +if __name__ == "__main__": + main() diff --git a/clients/python/secsgem_client/__init__.py b/clients/python/secsgem_client/__init__.py index 276914a..b28eeb4 100644 --- a/clients/python/secsgem_client/__init__.py +++ b/clients/python/secsgem_client/__init__.py @@ -7,6 +7,10 @@ equipment.yaml. """ from ._client import Command, Equipment, Health, ProcessJob, SecsGemError +from ._enums import JobState, Milestone, ModuleState -__all__ = ["Equipment", "Command", "Health", "ProcessJob", "SecsGemError"] +__all__ = [ + "Equipment", "Command", "Health", "ProcessJob", "SecsGemError", + "Milestone", "ModuleState", "JobState", +] __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 index fc850a2..2520fe3 100644 Binary files a/clients/python/secsgem_client/__pycache__/__init__.cpython-312.pyc and b/clients/python/secsgem_client/__pycache__/__init__.cpython-312.pyc differ diff --git a/clients/python/secsgem_client/__pycache__/_client.cpython-312.pyc b/clients/python/secsgem_client/__pycache__/_client.cpython-312.pyc index b06a42c..ca91d16 100644 Binary files a/clients/python/secsgem_client/__pycache__/_client.cpython-312.pyc and b/clients/python/secsgem_client/__pycache__/_client.cpython-312.pyc differ diff --git a/clients/python/secsgem_client/__pycache__/_enums.cpython-312.pyc b/clients/python/secsgem_client/__pycache__/_enums.cpython-312.pyc new file mode 100644 index 0000000..45ce85f Binary files /dev/null and b/clients/python/secsgem_client/__pycache__/_enums.cpython-312.pyc differ diff --git a/clients/python/secsgem_client/_client.py b/clients/python/secsgem_client/_client.py index eae842e..53cb8a6 100644 --- a/clients/python/secsgem_client/_client.py +++ b/clients/python/secsgem_client/_client.py @@ -23,13 +23,15 @@ equipment.yaml; values are plain Python. from __future__ import annotations import difflib +import enum import threading from dataclasses import dataclass from types import SimpleNamespace -from typing import Callable, Dict, Iterator, Optional +from typing import Callable, Dict, Iterator, Optional, Union import grpc +from ._enums import JobState, Milestone, ModuleState from ._proto import equipment_pb2 as pb from ._proto import equipment_pb2_grpc as rpc from ._values import from_value, to_value @@ -48,6 +50,21 @@ def _check(ack: pb.Ack) -> None: raise SecsGemError(ack.code, ack.message) +def _enum_value(proto_enum, value, kind: str) -> int: + """Resolve a protocol-enum argument (an enum member like Milestone.ARRIVED + or a plain string like "ARRIVED") to its protobuf int, typo-safely — the + same close-match courtesy as eq.names, instead of a raw protobuf error.""" + name = value.value if isinstance(value, enum.Enum) else str(value) + try: + return proto_enum.Value(name) + except ValueError: + valid = list(proto_enum.keys()) + close = difflib.get_close_matches(name, valid, n=3) + hint = (f" Did you mean {', '.join(close)}?" if close + else f" Valid: {', '.join(valid)}.") + raise ValueError(f"unknown {kind} '{name}'.{hint}") from None + + @dataclass class Command: """A remote command from the host (e.g. START). The host has already been @@ -196,7 +213,7 @@ class Equipment: """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)) + desired=_enum_value(pb.ControlState.State, desired, "control state")) _check(self._stub.RequestControlState(req)) def health(self) -> Health: @@ -259,32 +276,36 @@ class Equipment: self._constant_handler = fn return fn - def report_job(self, job_id: str, state: str) -> None: - """Report physical job progress: "SETTING_UP" | "PROCESSING" | - "COMPLETE" | "ABORTED". The daemon drives the E40 state machine and - notifies the host (S16F9).""" + def report_job(self, job_id: str, state: Union[JobState, str]) -> None: + """Report physical job progress (E40). state: a JobState or its name — + SETTING_UP | PROCESSING | COMPLETE | ABORTED. The daemon drives the E40 + state machine and notifies the host (S16F9).""" req = pb.ProcessJobState( - job_id=job_id, state=pb.ProcessJobState.State.Value(state)) + job_id=job_id, + state=_enum_value(pb.ProcessJobState.State, state, "job state")) _check(self._stub.ReportProcessJob(req)) - def report_substrate(self, substrate_id: str, milestone: str, + def report_substrate(self, substrate_id: str, + milestone: Union[Milestone, str], carrier_id: str = "", slot: int = 0) -> None: - """E90 wafer tracking. milestone: ARRIVED | AT_WORK | PROCESSING | - PROCESSED | AT_DESTINATION. The daemon drives the E90 FSMs and emits - the standard CEIDs to the host.""" + """E90 wafer tracking. milestone: a Milestone or its name — ARRIVED | + AT_WORK | PROCESSING | PROCESSED | AT_DESTINATION. ARRIVED records the + origin (carrier_id, slot) and creates the wafer; the rest drive its + E90 FSMs, and the daemon emits the standard CEIDs to the host.""" req = pb.SubstrateReport( substrate_id=substrate_id, - milestone=pb.SubstrateReport.Milestone.Value(milestone), + milestone=_enum_value(pb.SubstrateReport.Milestone, milestone, "milestone"), carrier_id=carrier_id, slot=slot) _check(self._stub.ReportSubstrate(req)) - def report_module(self, module_id: str, state: str) -> None: - """E157 module tracking. state: NOT_EXECUTING | GENERAL_EXECUTING | - STEP_EXECUTING | STEP_COMPLETED. Auto-creates the module on first - report.""" + def report_module(self, module_id: str, + state: Union[ModuleState, str]) -> None: + """E157 module tracking. state: a ModuleState or its name — + NOT_EXECUTING | GENERAL_EXECUTING | STEP_EXECUTING | STEP_COMPLETED. + Auto-creates the module on first report; NOT_EXECUTING resets it.""" req = pb.ModuleReport( module_id=module_id, - state=pb.ModuleReport.State.Value(state)) + state=_enum_value(pb.ModuleReport.State, state, "module state")) _check(self._stub.ReportModule(req)) def listen(self, background: bool = False) -> None: @@ -301,6 +322,13 @@ class Equipment: self._stop.set() self._channel.close() + def __enter__(self) -> "Equipment": + return self + + def __exit__(self, *exc) -> bool: + self.close() + return False + # ---- internals ------------------------------------------------------------- def _listen_loop(self) -> None: diff --git a/clients/python/secsgem_client/_enums.py b/clients/python/secsgem_client/_enums.py new file mode 100644 index 0000000..073e7f9 --- /dev/null +++ b/clients/python/secsgem_client/_enums.py @@ -0,0 +1,48 @@ +"""The fixed protocol enums you pass to the report_* / control methods. + +Unlike the equipment's *names* (events, alarms, commands — those vary per +tool and live on ``eq.names``), these value sets are fixed by the SEMI +standards, so they ship as importable enums: + + from secsgem_client import Equipment, Milestone, ModuleState + + eq.report_substrate("WFR-1", Milestone.ARRIVED) # autocomplete + typo-safe + eq.report_substrate("WFR-1", "ARRIVED") # plain string also works + +Each member *is* its wire name (``Milestone.ARRIVED == "ARRIVED"``), so the +two forms are interchangeable; the enums simply give you IDE autocomplete and +a typo-checked happy path. A wrong string raises ``ValueError`` with a +close-match suggestion (see ``_client._enum_value``). +""" + +from __future__ import annotations + +import enum + + +class Milestone(str, enum.Enum): + """E90 substrate (wafer) lifecycle milestones — for ``eq.report_substrate``.""" + + ARRIVED = "ARRIVED" # created at its source carrier slot + AT_WORK = "AT_WORK" # picked up for processing + PROCESSING = "PROCESSING" # recipe step started + PROCESSED = "PROCESSED" # recipe step finished + AT_DESTINATION = "AT_DESTINATION" # returned / deposited + + +class ModuleState(str, enum.Enum): + """E157 module execution states — for ``eq.report_module``.""" + + NOT_EXECUTING = "NOT_EXECUTING" # idle (also resets the module) + GENERAL_EXECUTING = "GENERAL_EXECUTING" # setup / pre- / post-process + STEP_EXECUTING = "STEP_EXECUTING" # actively running a recipe step + STEP_COMPLETED = "STEP_COMPLETED" + + +class JobState(str, enum.Enum): + """E40 process-job progress — for ``eq.report_job``.""" + + SETTING_UP = "SETTING_UP" + PROCESSING = "PROCESSING" + COMPLETE = "COMPLETE" + ABORTED = "ABORTED" diff --git a/clients/python/tests/test_enums.py b/clients/python/tests/test_enums.py new file mode 100644 index 0000000..978ddb1 --- /dev/null +++ b/clients/python/tests/test_enums.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""The exported protocol enums must stay in lockstep with the proto, and the +typo-safe resolver must reject bad values with a helpful message. Plain +asserts — run directly.""" +import sys + +from secsgem_client import JobState, Milestone, ModuleState +from secsgem_client._client import _enum_value +from secsgem_client._proto import equipment_pb2 as pb + + +def members(e): + return {m.value for m in e} + + +def main() -> int: + # Each enum mirrors its proto source exactly — no drift, no missing member. + assert members(Milestone) == set(pb.SubstrateReport.Milestone.keys()) + assert members(ModuleState) == set(pb.ModuleReport.State.keys()) + assert members(JobState) == set(pb.ProcessJobState.State.keys()) + + # A member IS its wire name, so enum and plain string are interchangeable. + assert Milestone.ARRIVED == "ARRIVED" + assert _enum_value(pb.SubstrateReport.Milestone, Milestone.ARRIVED, "milestone") \ + == _enum_value(pb.SubstrateReport.Milestone, "ARRIVED", "milestone") + + # A typo raises ValueError (client-side, not a daemon round-trip) and the + # message offers the close match instead of leaking a raw protobuf error. + try: + _enum_value(pb.SubstrateReport.Milestone, "AT_WROK", "milestone") + raise SystemExit("expected ValueError for a misspelled milestone") + except ValueError as e: + assert "AT_WORK" in str(e), str(e) + + print("enums: proto-sync + typo-safety checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/docs/16_e90_e157_substrates_modules.md b/docs/16_e90_e157_substrates_modules.md index f541c3a..d51efbb 100644 --- a/docs/16_e90_e157_substrates_modules.md +++ b/docs/16_e90_e157_substrates_modules.md @@ -207,28 +207,30 @@ If your tool uses the daemon (`secs_gemd`) and the Python client, the E90 and E157 RPCs are wrapped as two methods: ```python -from secsgem_client import Equipment +from secsgem_client import Equipment, Milestone, ModuleState eq = Equipment("localhost:50051") # E90 — substrate journey (daemon drives FSMs, fires CEIDs automatically) -eq.report_substrate("WFR-001", "ARRIVED", carrier_id="FOUP-7", slot=3) -eq.report_substrate("WFR-001", "AT_WORK") -eq.report_substrate("WFR-001", "PROCESSING") -eq.report_substrate("WFR-001", "PROCESSED") -eq.report_substrate("WFR-001", "AT_DESTINATION") +eq.report_substrate("WFR-001", Milestone.ARRIVED, carrier_id="FOUP-7", slot=3) +eq.report_substrate("WFR-001", Milestone.AT_WORK) +eq.report_substrate("WFR-001", Milestone.PROCESSING) +eq.report_substrate("WFR-001", Milestone.PROCESSED) +eq.report_substrate("WFR-001", Milestone.AT_DESTINATION) # E157 — module state (module is auto-created on first report) -eq.report_module("CHAMBER-A", "GENERAL_EXECUTING") -eq.report_module("CHAMBER-A", "STEP_EXECUTING") -eq.report_module("CHAMBER-A", "STEP_COMPLETED") -eq.report_module("CHAMBER-A", "NOT_EXECUTING") +eq.report_module("CHAMBER-A", ModuleState.GENERAL_EXECUTING) +eq.report_module("CHAMBER-A", ModuleState.STEP_EXECUTING) +eq.report_module("CHAMBER-A", ModuleState.STEP_COMPLETED) +eq.report_module("CHAMBER-A", ModuleState.NOT_EXECUTING) ``` -Milestones map to the `SubstrateReport.Milestone` protobuf enum; -module states to `ModuleReport.State`. The daemon's `ReportSubstrate` -handler validates FSM transitions and returns `INVALID_OBJECT` if the -substrate was never `ARRIVED` (which guarantees the daemon owns the -substrate record). +`Milestone` / `ModuleState` are importable enums (each member equals its +plain-string name, so `"ARRIVED"` works just as well). The daemon's +`ReportSubstrate` handler validates FSM transitions: a substrate that never +`ARRIVED` is rejected with `INVALID_OBJECT`, and a **duplicate** `ARRIVED` +with `CANNOT_DO_NOW` (`substrate '...' already exists`) — it never silently +re-creates over a wafer's live state. A complete worked example is +[clients/python/examples/wafer_tool.py](../clients/python/examples/wafer_tool.py). --- diff --git a/docs/42_vendor_daemon_and_clients.md b/docs/42_vendor_daemon_and_clients.md index 4a9a0e8..7a4cc62 100644 --- a/docs/42_vendor_daemon_and_clients.md +++ b/docs/42_vendor_daemon_and_clients.md @@ -100,52 +100,57 @@ protocol was designed for it. extension) and the entire integration is: ```python -from secsgem_client import Equipment +from secsgem_client import Equipment, Milestone, ModuleState -eq = Equipment("localhost:50051") +with Equipment("localhost:50051") as eq: # context manager closes the channel -eq.set(ChamberPressure=2.5) # host sees it on its next S1F3 -eq["WaferCounter"] = 7 # item syntax, same thing -print(eq.get("ChamberPressure")) # read back through the daemon + eq.set(ChamberPressure=2.5) # host sees it on its next S1F3 + eq["WaferCounter"] = 7 # item syntax, same thing + print(eq.get("ChamberPressure")) # read back through the daemon -# eq.names — autocomplete-able, typo-safe name lookup (fetched from Describe) -eq.fire(eq.names.event.ProcessStarted) # typo → AttributeError at the line it happened -eq.alarm(eq.names.alarm.chiller_temp_high) -eq.clear(eq.names.alarm.chiller_temp_high) -# Plain strings still work; names are a convenience, not a requirement. -eq.fire("ProcessStarted", ChamberPressure=2.75) + # eq.names — autocomplete-able, typo-safe name lookup (fetched from Describe) + eq.fire(eq.names.event.ProcessStarted) # typo → AttributeError at the line it happened + eq.alarm(eq.names.alarm.chiller_temp_high) + eq.clear(eq.names.alarm.chiller_temp_high) + # Plain strings still work; names are a convenience, not a requirement. + eq.fire("ProcessStarted", ChamberPressure=2.75) -@eq.command # function name IS the command name; -def START(cmd): # validated against Describe at decoration time - run_recipe(cmd.params.get("PPID")) - eq.fire(eq.names.event.ProcessStarted) + @eq.command # function name IS the command name; + def START(cmd): # validated against Describe at decoration time + run_recipe(cmd.params.get("PPID")) + eq.fire(eq.names.event.ProcessStarted) -# @eq.on("NAME") still works — use it when the name can't be a Python identifier -# or when you prefer explicit strings. + # @eq.on("NAME") still works — use it when the name can't be a Python identifier + # or when you prefer explicit strings. -eq.listen(background=True) # consume the Subscribe stream + eq.listen(background=True) # consume the Subscribe stream -eq.control_state # "ONLINE_REMOTE" -eq.request_control_state("HOST_OFFLINE") # operator panel -> maintenance -eq.health() # link / control state / spool depth + eq.control_state # "ONLINE_REMOTE" + eq.request_control_state("HOST_OFFLINE") # operator panel -> maintenance + eq.health() # link / control state / spool depth -# E90 / E157 material tracking -eq.report_substrate("WFR-001", "ARRIVED", carrier_id="FOUP-7", slot=3) -eq.report_substrate("WFR-001", "AT_WORK") -eq.report_substrate("WFR-001", "PROCESSING") -eq.report_substrate("WFR-001", "PROCESSED") -eq.report_substrate("WFR-001", "AT_DESTINATION") + # E90 / E157 material tracking. Milestone / ModuleState are importable + # enums (autocomplete + typo-checked); the equivalent plain strings work too. + eq.report_substrate("WFR-001", Milestone.ARRIVED, carrier_id="FOUP-7", slot=3) + eq.report_substrate("WFR-001", Milestone.AT_WORK) + eq.report_substrate("WFR-001", Milestone.PROCESSING) + eq.report_substrate("WFR-001", Milestone.PROCESSED) + eq.report_substrate("WFR-001", Milestone.AT_DESTINATION) -eq.report_module("CHAMBER-A", "GENERAL_EXECUTING") -eq.report_module("CHAMBER-A", "STEP_EXECUTING") -eq.report_module("CHAMBER-A", "STEP_COMPLETED") -eq.report_module("CHAMBER-A", "NOT_EXECUTING") + eq.report_module("CHAMBER-A", ModuleState.GENERAL_EXECUTING) + eq.report_module("CHAMBER-A", ModuleState.STEP_EXECUTING) + eq.report_module("CHAMBER-A", ModuleState.STEP_COMPLETED) + eq.report_module("CHAMBER-A", ModuleState.NOT_EXECUTING) ``` -Anything the daemon declines raises `SecsGemError` with its explanation -(`no variable named 'ChamberPresure'`). A complete runnable tool is -[clients/python/examples/mini_tool.py](../clients/python/examples/mini_tool.py) -(~25 lines). The package is validated end-to-end by +Two error channels, by design: a **bad value you control** (a misspelled +milestone, an unknown control state) raises a plain `ValueError`/`NameError` +*before* any round-trip, with a close-match hint; anything the **daemon** +declines (unknown variable name, illegal FSM transition) raises `SecsGemError` +with its explanation (`no variable named 'ChamberPresure'`). Runnable tools: +[mini_tool.py](../clients/python/examples/mini_tool.py) (~25-line quickstart) +and [wafer_tool.py](../clients/python/examples/wafer_tool.py) (E90/E157 +material tracking). The package is validated end-to-end by `interop/pyclient_interop.py` driving the published API while secsgem-py judges the wire. @@ -171,6 +176,23 @@ eq.names.constant.MaxPressure # → "MaxPressure" `dir(eq.names.event)` lists all event names — REPL and IDE autocomplete work out of the box. +### Names vs. enums — one rule + +There are two kinds of identifier in the API, split on whether *your tool* or +*the SEMI standard* owns the value: + +- **Equipment-specific names** — events, alarms, commands, variables, + constants — come from *your* `equipment.yaml`, so they live on the instance + as `eq.names.*` (fetched from the live daemon). +- **Fixed protocol value-sets** — `Milestone`, `ModuleState`, `JobState` — + are defined by the standards, so they're importable enums + (`from secsgem_client import Milestone`). `Milestone.ARRIVED == "ARRIVED"`, + so an enum member and its plain string are interchangeable; the enum just + buys you autocomplete and a typo-checked happy path. + +Either way a wrong value fails fast and helpfully — a `ValueError`/`NameError` +with a close-match suggestion, raised client-side before the wire. + Other languages: generate stubs from the proto (`protoc` supports 11+ languages) and wrap them the same way — the Python client is ~200 lines and is the reference for what a thin wrapper should feel like. diff --git a/interop/pyclient_interop.py b/interop/pyclient_interop.py index 8bf8922..9b541ab 100644 --- a/interop/pyclient_interop.py +++ b/interop/pyclient_interop.py @@ -33,7 +33,7 @@ import secsgem.gem import secsgem.hsms import secsgem.secs -from secsgem_client import Equipment, SecsGemError +from secsgem_client import Equipment, Milestone, ModuleState, SecsGemError LOG = logging.getLogger("pyclient-interop") F = secsgem.secs.functions @@ -175,27 +175,33 @@ def run(grpc_addr: str, hsms_host: str, hsms_port: int) -> int: except NameError: check("@eq.command on unknown name raises NameError", True) - # ---- E90 substrate tracking: full journey, then the error path ---- - eq.report_substrate("WFR-PY-1", "ARRIVED", carrier_id="FOUP-PY", slot=4) - eq.report_substrate("WFR-PY-1", "AT_WORK") - eq.report_substrate("WFR-PY-1", "PROCESSING") - eq.report_substrate("WFR-PY-1", "PROCESSED") - eq.report_substrate("WFR-PY-1", "AT_DESTINATION") - check("report_substrate full journey accepted (no raise)", True) + # ---- E90 substrate tracking: enum + string forms, then error paths ---- + eq.report_substrate("WFR-PY-1", Milestone.ARRIVED, + carrier_id="FOUP-PY", slot=4) # enum form + eq.report_substrate("WFR-PY-1", "AT_WORK") # plain string, same thing + eq.report_substrate("WFR-PY-1", Milestone.PROCESSING) + eq.report_substrate("WFR-PY-1", Milestone.PROCESSED) + eq.report_substrate("WFR-PY-1", Milestone.AT_DESTINATION) + check("report_substrate full journey (enum + string) accepted", True) try: - eq.report_substrate("WFR-GHOST", "AT_WORK") # never ARRIVED + eq.report_substrate("WFR-PY-1", "AT_WROK") # typo: client-side reject + check("misspelled milestone raises ValueError", False) + except ValueError as e: + check("misspelled milestone raises ValueError", "AT_WORK" in str(e)) + try: + eq.report_substrate("WFR-GHOST", Milestone.AT_WORK) # never ARRIVED check("report_substrate on unknown wafer raises", False) except SecsGemError as e: check("report_substrate on unknown wafer raises", "WFR-GHOST" in str(e)) # ---- E157 module tracking: walk and reset ---- - eq.report_module("MOD-PY-1", "GENERAL_EXECUTING") - eq.report_module("MOD-PY-1", "STEP_EXECUTING") - eq.report_module("MOD-PY-1", "STEP_COMPLETED") - eq.report_module("MOD-PY-1", "NOT_EXECUTING") + eq.report_module("MOD-PY-1", ModuleState.GENERAL_EXECUTING) + eq.report_module("MOD-PY-1", ModuleState.STEP_EXECUTING) + eq.report_module("MOD-PY-1", ModuleState.STEP_COMPLETED) + eq.report_module("MOD-PY-1", ModuleState.NOT_EXECUTING) check("report_module walk + reset accepted (no raise)", True) try: - eq.report_module("MOD-PY-2", "STEP_EXECUTING") # illegal from idle + eq.report_module("MOD-PY-2", ModuleState.STEP_EXECUTING) # illegal from idle check("report_module illegal jump raises", False) except SecsGemError: check("report_module illegal jump raises", True) diff --git a/tools/run_interop.sh b/tools/run_interop.sh index d27c744..347c23f 100755 --- a/tools/run_interop.sh +++ b/tools/run_interop.sh @@ -98,6 +98,8 @@ 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 run --rm --no-deps -e PYTHONPATH=/app/clients/python interop \ + python3 /app/clients/python/tests/test_enums.py compose up -d --no-deps gemd sleep 2 compose run --rm --no-deps -e PYTHONPATH=/app/clients/python interop \