refactor(api): complete fastapi migration and related features

Finish native FastAPI controllers, drop vendored microdot, and add
Wi-Fi driver runtime, beat SSE, simulated BPM, sequence playback
improvements, bridge ESP-NOW sources, UI updates, and tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-11 22:55:28 +12:00
parent cb9758b97b
commit ace5770b3a
73 changed files with 4540 additions and 4487 deletions

View File

@@ -13,6 +13,56 @@ if SRC_PATH not in sys.path:
from util import sequence_playback as sp # noqa: E402
def test_effective_switch_wait_ignores_saved_downbeat_when_audio_off(monkeypatch):
class FakeSettings:
def get(self, key, default=None):
if key == "sequence_switch_wait":
return "downbeat"
return default
monkeypatch.setattr("settings.get_settings", lambda: FakeSettings())
monkeypatch.setattr(
"util.audio_detector.shared_beat_detector_timing_sequences", lambda: False
)
assert sp.effective_sequence_switch_wait() == "beat"
def test_simulated_mode_forces_beat_switch_wait(monkeypatch):
class FakeSettings:
def get(self, key, default=None):
if key == "sequence_switch_wait":
return "downbeat"
return default
monkeypatch.setattr("settings.get_settings", lambda: FakeSettings())
monkeypatch.setattr(
"util.audio_detector.shared_beat_detector_timing_sequences", lambda: False
)
assert sp._sequence_switch_wait_from_settings() == "beat"
monkeypatch.setattr(
"util.audio_detector.shared_beat_detector_timing_sequences", lambda: True
)
assert sp._sequence_switch_wait_from_settings() == "downbeat"
def test_beat_switch_when_audio_running_but_sim_clocks(monkeypatch):
"""Mic on without timing sequences: still beat-only (not downbeat)."""
class FakeSettings:
def get(self, key, default=None):
if key == "sequence_switch_wait":
return "downbeat"
return default
monkeypatch.setattr("settings.get_settings", lambda: FakeSettings())
monkeypatch.setattr(
"util.audio_detector.shared_beat_detector_running", lambda: True
)
monkeypatch.setattr(
"util.audio_detector.shared_beat_detector_timing_sequences", lambda: False
)
assert sp.effective_sequence_switch_wait() == "beat"
def test_normalize_wait_for():
assert sp._normalize_wait_for({"wait_for": "beat"}) == "beat"
assert sp._normalize_wait_for({"start_on": "downbeat"}) == "downbeat"
@@ -37,19 +87,49 @@ def test_queue_and_clear_pending():
assert sp.pending_play_status()["pending"] is False
def test_try_consume_pending_beat():
def test_try_consume_pending_beat(monkeypatch):
monkeypatch.setattr(
"util.audio_detector.shared_beat_detector_timing_sequences", lambda: False
)
sp.clear_pending_play()
sp._queue_pending_start("z1", "s1", "p1", None, "beat", bpm=120.0)
async def fake_start(*_a, **_k):
return None
sp._start_immediate = fake_start # type: ignore[method-assign]
monkeypatch.setattr("util.sequence_playback._start_immediate", fake_start)
assert asyncio.run(sp._try_consume_pending_play(is_downbeat=False)) is True
assert sp.pending_play_status()["pending"] is False
def test_try_consume_pending_downbeat_skips_upbeat():
def test_try_consume_pending_beat_accepts_upbeat(monkeypatch):
monkeypatch.setattr(
"util.audio_detector.shared_beat_detector_timing_sequences", lambda: False
)
sp.clear_pending_play()
sp._queue_pending_start("z1", "s1", "p1", None, "beat", bpm=120.0)
sp._mark_simulated_beat_phase()
sp._last_thread_beat_phase = {"bar_beat": 3, "is_downbeat": False}
async def fake_start(*_a, **_k):
return None
monkeypatch.setattr("util.sequence_playback._start_immediate", fake_start)
assert asyncio.run(sp._try_consume_pending_play(is_downbeat=False)) is True
sp.clear_pending_play()
def test_try_consume_pending_downbeat_skips_upbeat(monkeypatch):
monkeypatch.setattr(
"util.audio_detector.shared_beat_detector_timing_sequences", lambda: True
)
class FakeSettings:
def get(self, key, default=None):
if key == "sequence_switch_wait":
return "downbeat"
return default
monkeypatch.setattr("settings.get_settings", lambda: FakeSettings())
sp.clear_pending_play()
sp._queue_pending_start("z1", "s1", "p1", None, "downbeat", bpm=120.0)
assert asyncio.run(sp._try_consume_pending_play(is_downbeat=False)) is False
@@ -63,12 +143,94 @@ def test_try_consume_pending_downbeat_skips_upbeat():
sp.clear_pending_play()
def test_sequence_pass_start_anchors_bar_phase_to_one():
sp.stop()
sp._sim_beat_counter = 7
sp._last_thread_beat_phase = {"bar_beat": 3, "is_downbeat": False}
ctx = {
"lanes": [[{"preset_id": "1", "beats": 6}]],
"lane_states": [{"stepIdx": 0, "beatCount": 0, "done": False}],
"num_lanes": 1,
"loop": False,
"sequence_loop_beat": 0,
"presets_map": {},
}
with sp._beat_run_lock:
sp._beat_run = ctx
assert sp._is_sequence_pass_start(ctx) is True
sp._anchor_bar_phase_for_sequence_start()
phase = sp.simulated_beat_phase_snapshot()
assert phase["bar_beat"] == 1
assert phase["is_downbeat"] is True
assert phase["bar_phase_readout"] == "1/4"
asyncio.run(sp.process_active_beat_advance())
st = sp.playback_status()
assert st["beat_readout"] == "1/6"
assert sp.simulated_beat_phase_snapshot()["bar_beat"] == 1
sp.stop()
def test_sequence_pass_start_not_mid_pass():
ctx = {
"lanes": [[{"preset_id": "1", "beats": 2}, {"preset_id": "2", "beats": 2}]],
"lane_states": [{"stepIdx": 1, "beatCount": 0, "done": False}],
"num_lanes": 1,
"loop": False,
}
assert sp._is_sequence_pass_start(ctx) is False
def test_completed_beat_readout_survives_stop_playback():
sp.stop()
sp.clear_completed_beat_readout()
ctx = {
"lanes": [[{"preset_id": "1", "beats": 6}]],
"lane_states": [{"stepIdx": 0, "beatCount": 6, "done": True}],
"num_lanes": 1,
"loop": False,
"sequence_loop_beat": 6,
"presets_map": {},
}
with sp._beat_run_lock:
sp._beat_run = ctx
sp.remember_completed_beat_readout(sp._beat_readout_for_ctx(ctx))
asyncio.run(sp.stop_playback(clear_devices=False))
assert sp.last_completed_beat_readout() == "6/6"
assert sp.playback_status()["active"] is False
sp.stop()
def test_playback_beat_readout_six_beat_sequence():
"""Beat readout is 1..tot with no duplicate 1 at start or missing final beat."""
sp.stop()
ctx = {
"lanes": [[{"preset_id": "1", "beats": 6}]],
"lane_states": [{"stepIdx": 0, "beatCount": 0, "done": False}],
"num_lanes": 1,
"loop": False,
"sequence_loop_beat": 0,
"presets_map": {},
}
with sp._beat_run_lock:
sp._beat_run = ctx
assert sp.playback_status()["beat_readout"] == ""
for n in range(1, 5):
ctx["lane_states"][0]["beatCount"] = n
assert sp.playback_status()["beat_readout"] == f"{n}/6"
ctx["lane_states"][0]["beatCount"] = 5
assert sp.playback_status()["beat_readout"] == "6/6"
ctx["lane_states"][0]["beatCount"] = 6
ctx["lane_states"][0]["done"] = True
assert sp.playback_status()["beat_readout"] == "6/6"
sp.stop()
def test_downbeat_start_counts_trigger_beat(monkeypatch):
"""The downbeat that starts playback is beat 1 of the step, not beat 0."""
sp.clear_pending_play()
sp.stop()
async def fake_start(_z, _s, _p, _opts):
async def fake_start(_z, _s, _p, _opts, **_kwargs):
sp._beat_run = {
"lanes": [[{"preset_id": "1", "beats": 4}]],
"lane_states": [{"stepIdx": 0, "beatCount": 0, "done": False}],