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 }
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.
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
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.