jmri_scripting.shim

JmriShim — the JMRI-style globals injected into every script.

JMRI scripts (the 369 Jython files we inventoried in Stage 0) call APIs like sensors.provideSensor("LS5") synchronously and get a Sensor back. Our core is async, so the shim bridges via asyncio.run_coroutine_threadsafe() against the event loop the ScriptRunner retains a reference to.

Every shim method runs on the script's worker thread and blocks until the corresponding async coroutine completes on the event-loop thread. Scripts therefore feel like the original JMRI experience: sensor = sensors.provideSensor("LS5"); sensor.setKnownState(ACTIVE).

Top-N coverage targets the most-used paths from Stage 0's contracts/script-api.md ranking.

  1"""``JmriShim`` — the JMRI-style globals injected into every script.
  2
  3JMRI scripts (the 369 Jython files we inventoried in Stage 0) call APIs
  4like ``sensors.provideSensor("LS5")`` synchronously and get a Sensor
  5back. Our core is async, so the shim bridges via
  6:func:`asyncio.run_coroutine_threadsafe` against the event loop the
  7:class:`ScriptRunner` retains a reference to.
  8
  9Every shim method runs *on the script's worker thread* and blocks until
 10the corresponding async coroutine completes on the event-loop thread.
 11Scripts therefore feel like the original JMRI experience: ``sensor =
 12sensors.provideSensor("LS5"); sensor.setKnownState(ACTIVE)``.
 13
 14Top-N coverage targets the most-used paths from Stage 0's
 15``contracts/script-api.md`` ranking.
 16"""
 17from __future__ import annotations
 18
 19import asyncio
 20import logging
 21from collections.abc import Callable, Iterator
 22from typing import TYPE_CHECKING, Any, cast
 23
 24from jmri_core.beans.sensor import Sensor
 25from jmri_core.beans.turnout import Turnout
 26from jmri_core.state import (
 27    LightState,
 28    PowerState,
 29    SensorState,
 30    ThrottleDirection,
 31    TurnoutState,
 32)
 33
 34if TYPE_CHECKING:
 35    from jmri_core.beans.block import Block
 36    from jmri_core.beans.light import Light
 37    from jmri_core.beans.memory import Memory
 38    from jmri_core.beans.reporter import Reporter
 39    from jmri_core.hal.connection import SystemConnection
 40
 41_log = logging.getLogger(__name__)
 42
 43
 44def _block(loop: asyncio.AbstractEventLoop, coro: Any) -> Any:
 45    """Run *coro* on *loop* from a different thread and wait for the result."""
 46    return asyncio.run_coroutine_threadsafe(coro, loop).result()
 47
 48
 49class _ManagerShim[B, W]:
 50    """Sync facade over a :class:`jmri_core.manager.Manager`.
 51
 52    Wraps every bean it hands back with *wrap*, so scripts get the
 53    JMRI-style camelCase methods on the wrapper rather than the raw
 54    async-native bean.
 55    """
 56
 57    def __init__(
 58        self,
 59        loop: asyncio.AbstractEventLoop,
 60        manager: Any,
 61        wrap: Callable[[B], W],
 62    ) -> None:
 63        self._loop = loop
 64        self._mgr = manager
 65        self._wrap = wrap
 66
 67    def provide(self, name: str) -> W:
 68        bean: B = _block(self._loop, self._mgr.provide(name))
 69        return self._wrap(bean)
 70
 71    def get(self, name: str) -> W | None:
 72        bean = self._mgr.get(name)
 73        return None if bean is None else self._wrap(bean)
 74
 75    def get_by_system_name(self, name: str) -> W | None:
 76        bean = self._mgr.get_by_system_name(name)
 77        return None if bean is None else self._wrap(bean)
 78
 79    def get_by_user_name(self, name: str) -> W | None:
 80        bean = self._mgr.get_by_user_name(name)
 81        return None if bean is None else self._wrap(bean)
 82
 83    def get_named_bean_set(self) -> list[W]:
 84        return [self._wrap(b) for b in self._mgr.named_bean_set()]
 85
 86    def __iter__(self) -> Iterator[W]:
 87        return iter(self._wrap(b) for b in self._mgr)
 88
 89    def __len__(self) -> int:
 90        return len(self._mgr)
 91
 92    # JMRI-style camelCase aliases. Each manager only ever uses one
 93    # of provideX/getX so multiple aliases on the same class is fine.
 94    provideSensor = provide
 95    provideTurnout = provide
 96    provideLight = provide
 97    provideMemory = provide
 98    provideReporter = provide
 99    provideBlock = provide
100    provideRoute = provide
101    getSensor = get
102    getTurnout = get
103    getLight = get
104    getMemory = get
105    getReporter = get
106    getBlock = get
107    getSignalHead = get
108    getSignalMast = get
109    getByUserName = get_by_user_name
110    getBySystemName = get_by_system_name
111    getNamedBeanSet = get_named_bean_set
112
113
114# --- bean shims --------------------------------------------------------------
115
116
117class _SensorShim:
118    """Sync wrapper over a real Sensor."""
119
120    __slots__ = ("_bean", "_loop")
121
122    def __init__(self, loop: asyncio.AbstractEventLoop, bean: Sensor) -> None:
123        self._loop = loop
124        self._bean = bean
125
126    @property
127    def system_name(self) -> str:
128        return self._bean.system_name
129
130    @property
131    def user_name(self) -> str | None:
132        return self._bean.user_name
133
134    @property
135    def known_state(self) -> SensorState:
136        return self._bean.known_state
137
138    @property
139    def isActive(self) -> bool:  # noqa: N802 — JMRI casing
140        return self._bean.is_active
141
142    def setKnownState(self, state: SensorState) -> None:  # noqa: N802
143        _block(self._loop, self._bean._set_known_state(state))
144
145    def getKnownState(self) -> SensorState:  # noqa: N802
146        return self._bean.known_state
147
148
149class _TurnoutShim:
150    """Sync wrapper over a Turnout."""
151
152    __slots__ = ("_bean", "_loop")
153
154    def __init__(self, loop: asyncio.AbstractEventLoop, bean: Turnout) -> None:
155        self._loop = loop
156        self._bean = bean
157
158    @property
159    def system_name(self) -> str:
160        return self._bean.system_name
161
162    @property
163    def user_name(self) -> str | None:
164        return self._bean.user_name
165
166    @property
167    def commanded_state(self) -> TurnoutState:
168        return self._bean.commanded_state
169
170    @property
171    def known_state(self) -> TurnoutState:
172        return self._bean.known_state
173
174    def setCommandedState(self, state: TurnoutState) -> None:  # noqa: N802
175        _block(self._loop, self._bean.set_commanded_state(state))
176
177    def getCommandedState(self) -> TurnoutState:  # noqa: N802
178        return self._bean.commanded_state
179
180    def getKnownState(self) -> TurnoutState:  # noqa: N802
181        return self._bean.known_state
182
183
184class _LightShim:
185    __slots__ = ("_bean", "_loop")
186
187    def __init__(self, loop: asyncio.AbstractEventLoop, bean: Light) -> None:
188        self._loop = loop
189        self._bean = bean
190
191    @property
192    def system_name(self) -> str:
193        return self._bean.system_name
194
195    @property
196    def state(self) -> LightState:
197        return self._bean.state
198
199    def setState(self, state: LightState) -> None:  # noqa: N802
200        _block(self._loop, self._bean.set_state(state))
201
202    def getState(self) -> LightState:  # noqa: N802
203        return self._bean.state
204
205
206class _MemoryShim:
207    __slots__ = ("_bean", "_loop")
208
209    def __init__(self, loop: asyncio.AbstractEventLoop, bean: Memory) -> None:
210        self._loop = loop
211        self._bean = bean
212
213    @property
214    def system_name(self) -> str:
215        return self._bean.system_name
216
217    @property
218    def value(self) -> Any:
219        return self._bean.value
220
221    def setValue(self, value: Any) -> None:  # noqa: N802
222        _block(self._loop, self._bean.set_value(value))
223
224    def getValue(self) -> Any:  # noqa: N802
225        return self._bean.value
226
227
228class _ReporterShim:
229    __slots__ = ("_bean", "_loop")
230
231    def __init__(self, loop: asyncio.AbstractEventLoop, bean: Reporter) -> None:
232        self._loop = loop
233        self._bean = bean
234
235    @property
236    def system_name(self) -> str:
237        return self._bean.system_name
238
239    @property
240    def current_report(self) -> Any:
241        return self._bean.current_report
242
243    def getCurrentReport(self) -> Any:  # noqa: N802
244        return self._bean.current_report
245
246    def getLastReport(self) -> Any:  # noqa: N802
247        return self._bean.last_report
248
249
250class _BlockShim:
251    __slots__ = ("_bean", "_loop")
252
253    def __init__(self, loop: asyncio.AbstractEventLoop, bean: Block) -> None:
254        self._loop = loop
255        self._bean = bean
256
257    @property
258    def system_name(self) -> str:
259        return self._bean.system_name
260
261    @property
262    def state(self) -> Any:
263        return self._bean.state
264
265    @property
266    def value(self) -> Any:
267        return self._bean.value
268
269    def setValue(self, value: Any) -> None:  # noqa: N802
270        _block(self._loop, self._bean.set_value(value))
271
272
273class _PowerShim:
274    """Sync wrapper over the connection's PowerManager.
275
276    JMRI Java's Jython bridge exposes ``powermanager.getPower()`` which
277    returns an integer state code — usable but you have to remember to
278    compare against ``powermanager.ON``. The Python rewrite adds
279    friendlier accessors as first-class properties (:attr:`state`,
280    :attr:`isOn`, :attr:`isOff`, :attr:`isIdle`, :attr:`isUnknown`) so
281    scripts can write ``if powermanager.isOn: …`` directly. ``getPower``
282    is retained verbatim for JMRI-Jython script compatibility.
283
284    Two independent power questions matter to scripts:
285
286    * **Track power** — is the DCC signal live on the rails? Controlled
287      by :meth:`setPower` / :meth:`getPower` and the ``isOn`` /
288      ``isOff`` / ``isIdle`` properties. This is what the CS sends to
289      the layout and what most scripts check ("don't try to move locos
290      when the track power is off").
291    * **Command-station power** — is the CS itself talking to us at
292      all? Exposed as :attr:`isCommandStationOn` /
293      :meth:`getCommandStationPower`. Always ``True`` when the
294      connection is open under the default driver semantics; some
295      drivers with heartbeat detection can flip it False mid-session
296      when the CS goes down.
297    """
298
299    __slots__ = ("_conn", "_loop", "_pm")
300
301    ON = PowerState.ON
302    OFF = PowerState.OFF
303    IDLE = PowerState.IDLE
304    UNKNOWN = PowerState.UNKNOWN
305
306    def __init__(
307        self, loop: asyncio.AbstractEventLoop, pm: Any, connection: Any = None,
308    ) -> None:
309        self._loop = loop
310        self._pm = pm
311        self._conn = connection
312
313    def setPower(self, state: PowerState | int) -> None:  # noqa: N802
314        if isinstance(state, int) and not isinstance(state, PowerState):
315            state = PowerState(state)
316        _block(self._loop, self._pm.set_power(state))
317
318    def getPower(self) -> PowerState:  # noqa: N802
319        return cast("PowerState", self._pm.state)
320
321    @property
322    def state(self) -> PowerState:
323        """Current track power state as a :class:`PowerState` enum.
324
325        Pythonic alternative to :meth:`getPower` — the enum's
326        ``.name`` (``"ON"`` / ``"OFF"`` / ``"IDLE"`` / ``"UNKNOWN"``)
327        formats nicely in logs and ``str()`` calls.
328        """
329        return cast("PowerState", self._pm.state)
330
331    @property
332    def isOn(self) -> bool:  # noqa: N802
333        """``True`` iff track power is currently ON.
334
335        The most common script query, elevated to a one-name check.
336        """
337        return self._pm.state == PowerState.ON
338
339    @property
340    def isOff(self) -> bool:  # noqa: N802
341        """``True`` iff track power is currently OFF."""
342        return self._pm.state == PowerState.OFF
343
344    @property
345    def isIdle(self) -> bool:  # noqa: N802
346        """``True`` iff track power is IDLE (some CS report this after e-stop)."""
347        return self._pm.state == PowerState.IDLE
348
349    @property
350    def isUnknown(self) -> bool:  # noqa: N802
351        """``True`` when we haven't yet observed a power state.
352
353        Fresh boot before the driver has heard from the CS, or a driver
354        that doesn't push status changes and hasn't been polled yet.
355        """
356        return self._pm.state == PowerState.UNKNOWN
357
358    # ----- command-station power -----------------------------------------
359    #
360    # Track power (above) is DCC-on-rails. CS power is "is the command
361    # station itself talking to us at all?" — a strict bool since the
362    # only visible states are On (open connection) and Off (no CS).
363
364    @property
365    def isCommandStationOn(self) -> bool:  # noqa: N802
366        """``True`` when the command station itself is powered / reachable.
367
368        Base drivers tie this to the connection being open; drivers with
369        heartbeat detection may flip this False without closing the
370        connection. Scripts wanting "should I even try to talk to the
371        CS right now?" should check this before ``isOn``.
372        """
373        if self._conn is None:
374            return False
375        return bool(getattr(self._conn, "is_command_station_on", False))
376
377    def getCommandStationPower(self) -> bool:  # noqa: N802
378        """JMRI-Java-shaped accessor returning ``True`` iff the CS is
379        powered. Mirrors :meth:`getPower` for track power."""
380        return self.isCommandStationOn
381
382
383class _ConsistShim:
384    """Sync wrapper over the connection's ConsistManager.
385
386    JMRI Java's ``ConsistManager`` API used by Jython scripts::
387
388        cm = InstanceManager.getDefault(ConsistManager)
389        c = cm.getConsist(dcc_address)
390        c.add(loco_address, direction_normal=True)
391        c.remove(loco_address)
392        cm.delConsist(dcc_address)
393
394    We match those names — ``getConsist`` auto-creates a new consist
395    if one doesn't exist yet at that address (matches JMRI Java's
396    behavior; ``newConsist`` is an alias for the same). Speed /
397    function / direction fan-out at the consist level is handled by
398    the underlying async manager; the shim delegates through the
399    manager rather than the individual Consist bean so per-driver
400    dispatch (LocoNet advanced, NCE memory-backed, DCC-EX) still
401    fires.
402    """
403
404    __slots__ = ("_loop", "_mgr")
405
406    def __init__(self, loop: asyncio.AbstractEventLoop, mgr: Any) -> None:
407        self._loop = loop
408        self._mgr = mgr
409
410    def getConsist(self, address: int) -> "_ConsistProxy":  # noqa: N802
411        """Return the consist at *address*, creating it if absent."""
412        existing = self._mgr.get(address)
413        if existing is None:
414            _block(self._loop, self._mgr.create(address))
415        return _ConsistProxy(self._loop, self._mgr, address)
416
417    def newConsist(self, address: int) -> "_ConsistProxy":  # noqa: N802
418        """Alias for :meth:`getConsist` — JMRI Java scripts use both."""
419        return self.getConsist(address)
420
421    def delConsist(self, address: int) -> None:  # noqa: N802
422        """Delete the consist at *address*."""
423        _block(self._loop, self._mgr.delete(address))
424
425    def getConsistList(self) -> list[int]:  # noqa: N802
426        """Return the DCC addresses of every registered consist."""
427        return [c.address for c in self._mgr]
428
429
430class _ConsistProxy:
431    """Per-consist facade returned from :meth:`_ConsistShim.getConsist`.
432
433    Kept thin: every mutation goes through the manager so hardware
434    dispatch happens uniformly. Read state via :attr:`members` /
435    :attr:`getConsistAddress`.
436    """
437
438    __slots__ = ("_loop", "_mgr", "_address")
439
440    def __init__(
441        self, loop: asyncio.AbstractEventLoop, mgr: Any, address: int,
442    ) -> None:
443        self._loop = loop
444        self._mgr = mgr
445        self._address = address
446
447    def getConsistAddress(self) -> int:  # noqa: N802
448        return self._address
449
450    def add(self, loco: int, direction_normal: bool = True) -> None:
451        """Add *loco* to the consist. ``direction_normal=False`` for a
452        loco pointing backwards relative to the consist lead."""
453        _block(self._loop, self._mgr.add_member(
454            self._address, loco, direction_normal=direction_normal,
455        ))
456
457    def remove(self, loco: int) -> None:
458        _block(self._loop, self._mgr.remove_member(self._address, loco))
459
460    @property
461    def members(self) -> list[int]:
462        consist = self._mgr.get(self._address)
463        if consist is None:
464            return []
465        # The Consist model stores members under ``locos`` (per JMRI
466        # Java's field name); expose the more script-friendly
467        # ``members`` for a caller iterating over addresses.
468        return [m.address for m in consist.locos]
469
470
471class _ProgrammerShim:
472    """Sync wrapper over the connection's ProgrammerManager.
473
474    JMRI Java's Jython usage::
475
476        pm = InstanceManager.getDefault(ProgrammerManager)
477        prog = pm.getGlobalProgrammer()          # service mode
478        prog = pm.getAddressedProgrammer(True, addr)  # ops mode
479
480    Then on the returned programmer::
481
482        value = prog.readCV(29)         # sync in JMRI Java
483        prog.writeCV(29, value)
484
485    CV reads block for 1–3s on service mode. Ops-mode reads require
486    RailCom + a driver that supports it (currently DCC-EX; NCE / LocoNet
487    don't).
488    """
489
490    __slots__ = ("_loop", "_mgr")
491
492    def __init__(self, loop: asyncio.AbstractEventLoop, mgr: Any) -> None:
493        self._loop = loop
494        self._mgr = mgr
495
496    def getGlobalProgrammer(self) -> "_ProgrammerProxy | None":  # noqa: N802
497        prog = self._mgr.global_programmer()
498        return _ProgrammerProxy(self._loop, prog) if prog is not None else None
499
500    def getAddressedProgrammer(  # noqa: N802
501        self, long_address: bool, address: int,
502    ) -> "_ProgrammerProxy | None":
503        prog = self._mgr.ops_programmer(address, long_address=long_address)
504        return _ProgrammerProxy(self._loop, prog) if prog is not None else None
505
506
507class _ProgrammerProxy:
508    """Sync facade over a single :class:`~jmri_core.beans.programmer.Programmer`.
509
510    Exposes ``readCV(cv)`` and ``writeCV(cv, value)`` — the two calls
511    every JMRI Jython programming script uses. Errors from the
512    async layer (timeouts, driver-specific failures) propagate as
513    the same exception type the async call raised.
514    """
515
516    __slots__ = ("_loop", "_prog")
517
518    def __init__(
519        self, loop: asyncio.AbstractEventLoop, prog: Any,
520    ) -> None:
521        self._loop = loop
522        self._prog = prog
523
524    def readCV(self, cv: int) -> int:  # noqa: N802
525        return _block(self._loop, self._prog.read_cv(cv))
526
527    def writeCV(self, cv: int, value: int) -> None:  # noqa: N802
528        _block(self._loop, self._prog.write_cv(cv, value))
529
530
531# ---- top-level shim --------------------------------------------------------
532
533
534class JmriShim:
535    """Bundle of script globals — the namespace every script sees.
536
537    A script's globals dict is populated from :meth:`globals`. JMRI-style
538    bare names (``sensors``, ``turnouts``, ``powermanager``, ``ACTIVE``,
539    ``CLOSED``, ``ON``, …) work without imports, matching the existing
540    Jython convention.
541    """
542
543    def __init__(
544        self, loop: asyncio.AbstractEventLoop, connection: SystemConnection
545    ) -> None:
546        self._loop = loop
547        self.connection = connection
548
549        self.sensors: _ManagerShim[Any, _SensorShim] = _ManagerShim(
550            loop, connection.sensors, lambda b: _SensorShim(loop, b),
551        )
552        self.turnouts: _ManagerShim[Any, _TurnoutShim] = _ManagerShim(
553            loop, connection.turnouts, lambda b: _TurnoutShim(loop, b),
554        )
555        self.lights: _ManagerShim[Any, _LightShim] = _ManagerShim(
556            loop, connection.lights, lambda b: _LightShim(loop, b),
557        )
558        self.memories: _ManagerShim[Any, _MemoryShim] = _ManagerShim(
559            loop, connection.memories, lambda b: _MemoryShim(loop, b),
560        )
561        self.reporters: _ManagerShim[Any, _ReporterShim] = _ManagerShim(
562            loop, connection.reporters, lambda b: _ReporterShim(loop, b),
563        )
564        self.blocks: _ManagerShim[Any, _BlockShim] = _ManagerShim(
565            loop, connection.blocks, lambda b: _BlockShim(loop, b),
566        )
567        # Routes/signals are less commonly mutated from scripts; expose
568        # them as raw managers so users can fall through to async if needed.
569        self.routes: _ManagerShim[Any, Any] = _ManagerShim(
570            loop, connection.routes, lambda b: b,
571        )
572        self.signals: _ManagerShim[Any, Any] = _ManagerShim(
573            loop, connection.signal_heads, lambda b: b,
574        )
575        self.masts: _ManagerShim[Any, Any] = _ManagerShim(
576            loop, connection.signal_masts, lambda b: b,
577        )
578
579        self.powermanager = _PowerShim(loop, connection.power, connection)
580        # Consists + programmers — both were on the deferred list
581        # for the Jython shim through beta 3. Wire them here so
582        # ``InstanceManager.getDefault(ConsistManager)`` and the
583        # ``ProgrammerManager`` equivalent resolve for scripts
584        # migrating from JMRI Java.
585        self.consists = _ConsistShim(loop, connection.consists)
586        self.programmers = _ProgrammerShim(loop, connection.programmers)
587
588        # State constants — JMRI scripts use bare ``ACTIVE``, ``CLOSED``, etc.
589        self.ACTIVE = SensorState.ACTIVE
590        self.INACTIVE = SensorState.INACTIVE
591        self.CLOSED = TurnoutState.CLOSED
592        self.THROWN = TurnoutState.THROWN
593        self.ON = LightState.ON
594        self.OFF = LightState.OFF
595        self.FORWARD = ThrottleDirection.FORWARD
596        self.REVERSE = ThrottleDirection.REVERSE
597
598    def globals(self) -> dict[str, Any]:
599        """Return the globals dict to inject into a script's ``exec`` namespace."""
600        return {
601            "sensors": self.sensors,
602            "turnouts": self.turnouts,
603            "lights": self.lights,
604            "memories": self.memories,
605            "reporters": self.reporters,
606            "blocks": self.blocks,
607            "routes": self.routes,
608            "signals": self.signals,
609            "masts": self.masts,
610            "powermanager": self.powermanager,
611            "consists": self.consists,
612            "programmers": self.programmers,
613            "ACTIVE": self.ACTIVE,
614            "INACTIVE": self.INACTIVE,
615            "CLOSED": self.CLOSED,
616            "THROWN": self.THROWN,
617            "ON": self.ON,
618            "OFF": self.OFF,
619            "FORWARD": self.FORWARD,
620            "REVERSE": self.REVERSE,
621            "SensorState": SensorState,
622            "TurnoutState": TurnoutState,
623            "LightState": LightState,
624            "PowerState": PowerState,
625            "shim": self,
626        }
class JmriShim:
535class JmriShim:
536    """Bundle of script globals — the namespace every script sees.
537
538    A script's globals dict is populated from :meth:`globals`. JMRI-style
539    bare names (``sensors``, ``turnouts``, ``powermanager``, ``ACTIVE``,
540    ``CLOSED``, ``ON``, …) work without imports, matching the existing
541    Jython convention.
542    """
543
544    def __init__(
545        self, loop: asyncio.AbstractEventLoop, connection: SystemConnection
546    ) -> None:
547        self._loop = loop
548        self.connection = connection
549
550        self.sensors: _ManagerShim[Any, _SensorShim] = _ManagerShim(
551            loop, connection.sensors, lambda b: _SensorShim(loop, b),
552        )
553        self.turnouts: _ManagerShim[Any, _TurnoutShim] = _ManagerShim(
554            loop, connection.turnouts, lambda b: _TurnoutShim(loop, b),
555        )
556        self.lights: _ManagerShim[Any, _LightShim] = _ManagerShim(
557            loop, connection.lights, lambda b: _LightShim(loop, b),
558        )
559        self.memories: _ManagerShim[Any, _MemoryShim] = _ManagerShim(
560            loop, connection.memories, lambda b: _MemoryShim(loop, b),
561        )
562        self.reporters: _ManagerShim[Any, _ReporterShim] = _ManagerShim(
563            loop, connection.reporters, lambda b: _ReporterShim(loop, b),
564        )
565        self.blocks: _ManagerShim[Any, _BlockShim] = _ManagerShim(
566            loop, connection.blocks, lambda b: _BlockShim(loop, b),
567        )
568        # Routes/signals are less commonly mutated from scripts; expose
569        # them as raw managers so users can fall through to async if needed.
570        self.routes: _ManagerShim[Any, Any] = _ManagerShim(
571            loop, connection.routes, lambda b: b,
572        )
573        self.signals: _ManagerShim[Any, Any] = _ManagerShim(
574            loop, connection.signal_heads, lambda b: b,
575        )
576        self.masts: _ManagerShim[Any, Any] = _ManagerShim(
577            loop, connection.signal_masts, lambda b: b,
578        )
579
580        self.powermanager = _PowerShim(loop, connection.power, connection)
581        # Consists + programmers — both were on the deferred list
582        # for the Jython shim through beta 3. Wire them here so
583        # ``InstanceManager.getDefault(ConsistManager)`` and the
584        # ``ProgrammerManager`` equivalent resolve for scripts
585        # migrating from JMRI Java.
586        self.consists = _ConsistShim(loop, connection.consists)
587        self.programmers = _ProgrammerShim(loop, connection.programmers)
588
589        # State constants — JMRI scripts use bare ``ACTIVE``, ``CLOSED``, etc.
590        self.ACTIVE = SensorState.ACTIVE
591        self.INACTIVE = SensorState.INACTIVE
592        self.CLOSED = TurnoutState.CLOSED
593        self.THROWN = TurnoutState.THROWN
594        self.ON = LightState.ON
595        self.OFF = LightState.OFF
596        self.FORWARD = ThrottleDirection.FORWARD
597        self.REVERSE = ThrottleDirection.REVERSE
598
599    def globals(self) -> dict[str, Any]:
600        """Return the globals dict to inject into a script's ``exec`` namespace."""
601        return {
602            "sensors": self.sensors,
603            "turnouts": self.turnouts,
604            "lights": self.lights,
605            "memories": self.memories,
606            "reporters": self.reporters,
607            "blocks": self.blocks,
608            "routes": self.routes,
609            "signals": self.signals,
610            "masts": self.masts,
611            "powermanager": self.powermanager,
612            "consists": self.consists,
613            "programmers": self.programmers,
614            "ACTIVE": self.ACTIVE,
615            "INACTIVE": self.INACTIVE,
616            "CLOSED": self.CLOSED,
617            "THROWN": self.THROWN,
618            "ON": self.ON,
619            "OFF": self.OFF,
620            "FORWARD": self.FORWARD,
621            "REVERSE": self.REVERSE,
622            "SensorState": SensorState,
623            "TurnoutState": TurnoutState,
624            "LightState": LightState,
625            "PowerState": PowerState,
626            "shim": self,
627        }

Bundle of script globals — the namespace every script sees.

A script's globals dict is populated from globals(). JMRI-style bare names (sensors, turnouts, powermanager, ACTIVE, CLOSED, ON, …) work without imports, matching the existing Jython convention.

JmriShim( loop: asyncio.events.AbstractEventLoop, connection: jmri_core.hal.SystemConnection)
544    def __init__(
545        self, loop: asyncio.AbstractEventLoop, connection: SystemConnection
546    ) -> None:
547        self._loop = loop
548        self.connection = connection
549
550        self.sensors: _ManagerShim[Any, _SensorShim] = _ManagerShim(
551            loop, connection.sensors, lambda b: _SensorShim(loop, b),
552        )
553        self.turnouts: _ManagerShim[Any, _TurnoutShim] = _ManagerShim(
554            loop, connection.turnouts, lambda b: _TurnoutShim(loop, b),
555        )
556        self.lights: _ManagerShim[Any, _LightShim] = _ManagerShim(
557            loop, connection.lights, lambda b: _LightShim(loop, b),
558        )
559        self.memories: _ManagerShim[Any, _MemoryShim] = _ManagerShim(
560            loop, connection.memories, lambda b: _MemoryShim(loop, b),
561        )
562        self.reporters: _ManagerShim[Any, _ReporterShim] = _ManagerShim(
563            loop, connection.reporters, lambda b: _ReporterShim(loop, b),
564        )
565        self.blocks: _ManagerShim[Any, _BlockShim] = _ManagerShim(
566            loop, connection.blocks, lambda b: _BlockShim(loop, b),
567        )
568        # Routes/signals are less commonly mutated from scripts; expose
569        # them as raw managers so users can fall through to async if needed.
570        self.routes: _ManagerShim[Any, Any] = _ManagerShim(
571            loop, connection.routes, lambda b: b,
572        )
573        self.signals: _ManagerShim[Any, Any] = _ManagerShim(
574            loop, connection.signal_heads, lambda b: b,
575        )
576        self.masts: _ManagerShim[Any, Any] = _ManagerShim(
577            loop, connection.signal_masts, lambda b: b,
578        )
579
580        self.powermanager = _PowerShim(loop, connection.power, connection)
581        # Consists + programmers — both were on the deferred list
582        # for the Jython shim through beta 3. Wire them here so
583        # ``InstanceManager.getDefault(ConsistManager)`` and the
584        # ``ProgrammerManager`` equivalent resolve for scripts
585        # migrating from JMRI Java.
586        self.consists = _ConsistShim(loop, connection.consists)
587        self.programmers = _ProgrammerShim(loop, connection.programmers)
588
589        # State constants — JMRI scripts use bare ``ACTIVE``, ``CLOSED``, etc.
590        self.ACTIVE = SensorState.ACTIVE
591        self.INACTIVE = SensorState.INACTIVE
592        self.CLOSED = TurnoutState.CLOSED
593        self.THROWN = TurnoutState.THROWN
594        self.ON = LightState.ON
595        self.OFF = LightState.OFF
596        self.FORWARD = ThrottleDirection.FORWARD
597        self.REVERSE = ThrottleDirection.REVERSE
connection
sensors: jmri_scripting.shim._ManagerShim[typing.Any, jmri_scripting.shim._SensorShim]
turnouts: jmri_scripting.shim._ManagerShim[typing.Any, jmri_scripting.shim._TurnoutShim]
lights: jmri_scripting.shim._ManagerShim[typing.Any, jmri_scripting.shim._LightShim]
memories: jmri_scripting.shim._ManagerShim[typing.Any, jmri_scripting.shim._MemoryShim]
reporters: jmri_scripting.shim._ManagerShim[typing.Any, jmri_scripting.shim._ReporterShim]
blocks: jmri_scripting.shim._ManagerShim[typing.Any, jmri_scripting.shim._BlockShim]
routes: jmri_scripting.shim._ManagerShim[typing.Any, typing.Any]
signals: jmri_scripting.shim._ManagerShim[typing.Any, typing.Any]
masts: jmri_scripting.shim._ManagerShim[typing.Any, typing.Any]
powermanager
consists
programmers
ACTIVE
INACTIVE
CLOSED
THROWN
ON
OFF
FORWARD
REVERSE
def globals(self) -> dict[str, typing.Any]:
599    def globals(self) -> dict[str, Any]:
600        """Return the globals dict to inject into a script's ``exec`` namespace."""
601        return {
602            "sensors": self.sensors,
603            "turnouts": self.turnouts,
604            "lights": self.lights,
605            "memories": self.memories,
606            "reporters": self.reporters,
607            "blocks": self.blocks,
608            "routes": self.routes,
609            "signals": self.signals,
610            "masts": self.masts,
611            "powermanager": self.powermanager,
612            "consists": self.consists,
613            "programmers": self.programmers,
614            "ACTIVE": self.ACTIVE,
615            "INACTIVE": self.INACTIVE,
616            "CLOSED": self.CLOSED,
617            "THROWN": self.THROWN,
618            "ON": self.ON,
619            "OFF": self.OFF,
620            "FORWARD": self.FORWARD,
621            "REVERSE": self.REVERSE,
622            "SensorState": SensorState,
623            "TurnoutState": TurnoutState,
624            "LightState": LightState,
625            "PowerState": PowerState,
626            "shim": self,
627        }

Return the globals dict to inject into a script's exec namespace.