jmri_scripting.automaton

AbstractAutomaton — the inheritable base every JMRI script extends.

In JMRI's Jython world, the common pattern is::

class MyScript(AbstractAutomaton):
    def init(self): ...
    def handle(self): ...  # returns True to keep running, False to stop

MyScript().start()

We provide the same shape with one tweak: scripts run on a worker thread owned by ScriptRunner rather than spawning their own. start just registers them; the runner pumps handle in a loop until it returns False, the script is stopped externally, or it raises.

 1"""``AbstractAutomaton`` — the inheritable base every JMRI script extends.
 2
 3In JMRI's Jython world, the common pattern is::
 4
 5    class MyScript(AbstractAutomaton):
 6        def init(self): ...
 7        def handle(self): ...  # returns True to keep running, False to stop
 8
 9    MyScript().start()
10
11We provide the same shape with one tweak: scripts run on a worker thread
12owned by :class:`ScriptRunner` rather than spawning their own. ``start``
13just registers them; the runner pumps ``handle`` in a loop until it
14returns False, the script is stopped externally, or it raises.
15"""
16from __future__ import annotations
17
18import logging
19import threading
20from typing import TYPE_CHECKING
21
22if TYPE_CHECKING:
23    from jmri_scripting.shim import JmriShim
24
25
26class AbstractAutomaton:
27    """Base class for scripted automations.
28
29    Subclass and override ``init`` (one-time setup) and ``handle``
30    (called in a loop, returns ``True`` to continue). Scripts can call
31    :meth:`stop` to terminate themselves.
32    """
33
34    # The shim is injected by the runner before init() runs.
35    shim: JmriShim
36
37    def __init__(self, name: str = "") -> None:
38        self.name = name or type(self).__name__
39        self._stop_event = threading.Event()
40        self._log = logging.getLogger(f"jmri_scripting.{self.name}")
41
42    # ----- lifecycle (overridden by subclasses) -----------------------------
43
44    def init(self) -> None:
45        """One-time setup before the handle loop starts."""
46
47    def handle(self) -> bool:
48        """Run one iteration. Return True to keep running, False to stop."""
49        return False
50
51    def cleanup(self) -> None:
52        """Optional teardown after the handle loop exits."""
53
54    # ----- control -------------------------------------------------------
55
56    def stop(self) -> None:
57        """Request the automaton to stop. Effective on the next handle iteration."""
58        self._stop_event.set()
59
60    @property
61    def should_stop(self) -> bool:
62        return self._stop_event.is_set()
63
64    # ----- runner-driven loop ------------------------------------------------
65
66    def run(self) -> None:
67        """Run the full lifecycle: init → handle-loop → cleanup."""
68        try:
69            self.init()
70            while not self._stop_event.is_set():
71                if not self.handle():
72                    break
73        except Exception:
74            self._log.exception("automaton %s crashed", self.name)
75            raise
76        finally:
77            try:
78                self.cleanup()
79            except Exception:
80                self._log.exception("automaton %s cleanup failed", self.name)
class AbstractAutomaton:
27class AbstractAutomaton:
28    """Base class for scripted automations.
29
30    Subclass and override ``init`` (one-time setup) and ``handle``
31    (called in a loop, returns ``True`` to continue). Scripts can call
32    :meth:`stop` to terminate themselves.
33    """
34
35    # The shim is injected by the runner before init() runs.
36    shim: JmriShim
37
38    def __init__(self, name: str = "") -> None:
39        self.name = name or type(self).__name__
40        self._stop_event = threading.Event()
41        self._log = logging.getLogger(f"jmri_scripting.{self.name}")
42
43    # ----- lifecycle (overridden by subclasses) -----------------------------
44
45    def init(self) -> None:
46        """One-time setup before the handle loop starts."""
47
48    def handle(self) -> bool:
49        """Run one iteration. Return True to keep running, False to stop."""
50        return False
51
52    def cleanup(self) -> None:
53        """Optional teardown after the handle loop exits."""
54
55    # ----- control -------------------------------------------------------
56
57    def stop(self) -> None:
58        """Request the automaton to stop. Effective on the next handle iteration."""
59        self._stop_event.set()
60
61    @property
62    def should_stop(self) -> bool:
63        return self._stop_event.is_set()
64
65    # ----- runner-driven loop ------------------------------------------------
66
67    def run(self) -> None:
68        """Run the full lifecycle: init → handle-loop → cleanup."""
69        try:
70            self.init()
71            while not self._stop_event.is_set():
72                if not self.handle():
73                    break
74        except Exception:
75            self._log.exception("automaton %s crashed", self.name)
76            raise
77        finally:
78            try:
79                self.cleanup()
80            except Exception:
81                self._log.exception("automaton %s cleanup failed", self.name)

Base class for scripted automations.

Subclass and override init (one-time setup) and handle (called in a loop, returns True to continue). Scripts can call stop() to terminate themselves.

AbstractAutomaton(name: str = '')
38    def __init__(self, name: str = "") -> None:
39        self.name = name or type(self).__name__
40        self._stop_event = threading.Event()
41        self._log = logging.getLogger(f"jmri_scripting.{self.name}")
name
def init(self) -> None:
45    def init(self) -> None:
46        """One-time setup before the handle loop starts."""

One-time setup before the handle loop starts.

def handle(self) -> bool:
48    def handle(self) -> bool:
49        """Run one iteration. Return True to keep running, False to stop."""
50        return False

Run one iteration. Return True to keep running, False to stop.

def cleanup(self) -> None:
52    def cleanup(self) -> None:
53        """Optional teardown after the handle loop exits."""

Optional teardown after the handle loop exits.

def stop(self) -> None:
57    def stop(self) -> None:
58        """Request the automaton to stop. Effective on the next handle iteration."""
59        self._stop_event.set()

Request the automaton to stop. Effective on the next handle iteration.

should_stop: bool
61    @property
62    def should_stop(self) -> bool:
63        return self._stop_event.is_set()
def run(self) -> None:
67    def run(self) -> None:
68        """Run the full lifecycle: init → handle-loop → cleanup."""
69        try:
70            self.init()
71            while not self._stop_event.is_set():
72                if not self.handle():
73                    break
74        except Exception:
75            self._log.exception("automaton %s crashed", self.name)
76            raise
77        finally:
78            try:
79                self.cleanup()
80            except Exception:
81                self._log.exception("automaton %s cleanup failed", self.name)

Run the full lifecycle: init → handle-loop → cleanup.