Files
musicmouse/python-backend/musicmouse/simulator/repl.py
Martin Bauer d44c24ec97 Full rearchitecture using Claude
- event bus systen
- all components are independent
- preparation for web frontend
2026-08-26 13:22:28 +02:00

97 lines
3.1 KiB
Python

"""The interactive simulator prompt.
Runs the whole app - bus, device, reactions, MQTT if configured - against fake
hardware, and lets you poke it by hand while watching the events go past.
"""
from __future__ import annotations
import asyncio
import contextlib
from musicmouse.events import (
ActiveFigureChanged,
Event,
InputEvent,
LedEffectChanged,
PlaybackChanged,
StateEvent,
TrackChanged,
VolumeChanged,
)
from musicmouse.simulator.driver import ExpectationError, ScriptError
from musicmouse.simulator.harness import Simulation
__all__ = ["run_repl"]
PROMPT = "musicmouse> "
HELP = """\
place <figure> put a figure on the reader remove
press <left|right|rotary> [action] turn <+n|-n>
touch <left_ear|right_ear|left_foot|right_foot> release <same>
play pause next prev volume <0-100>
disconnect / reconnect simulate the USB cable
wait <1s|500ms|2> let time pass
status one-line summary leds
expect <key> <value> assert (playing, figure, playlist, track, title,
volume, brightness, ring, mouse, shelf)
help this text quit
"""
def _describe(event: Event) -> str | None:
"""A compact one-liner, or None for events too noisy to show."""
match event:
case LedEffectChanged(zone=zone, effect=effect):
return f" led {zone:>5}: {effect}"
case PlaybackChanged(playing=playing, figure=figure):
state = "playing" if playing else "paused"
return f" play {state}{f' [{figure}]' if figure else ''}"
case TrackChanged(index=index, track=track):
return f" play track {index}" + (f": {track.title}" if track else "")
case VolumeChanged(volume=volume):
return f" vol {volume}"
case ActiveFigureChanged(figure=figure):
return f" rfid {figure or '(removed)'}"
case InputEvent():
return f" in {event}"
case StateEvent():
return f" state {event}"
case _:
return None
async def run_repl(sim: Simulation) -> None:
print("MusicMouse simulator - no hardware, no audio. 'help' for commands.\n")
sim.bus.subscribe_all(_print_event)
print(sim.driver.status())
while True:
try:
line = await asyncio.to_thread(input, PROMPT)
except (EOFError, KeyboardInterrupt):
print()
return
command = line.strip().lower()
if command in {"quit", "exit", "q"}:
return
if command in {"help", "?"}:
print(HELP, end="")
continue
try:
if (output := await sim.driver.execute(line)) is not None:
print(output)
except (ScriptError, ExpectationError) as exc:
print(f"! {exc}")
except Exception as exc: # the prompt must survive anything
print(f"! {type(exc).__name__}: {exc}")
def _print_event(event: Event) -> None:
if (text := _describe(event)) is not None:
with contextlib.suppress(OSError):
print(text)