From 751637d2f165bf44b657674d87b13732946f689f Mon Sep 17 00:00:00 2001 From: Martin Bauer Date: Sun, 4 Oct 2026 00:25:40 +0200 Subject: [PATCH] Remove dead code - wire.with_figure: the device uses dataclasses.replace directly. - DeviceConnected/DeviceDisconnected: emitted, never subscribed to, and a duplicate of ConnectionChanged(target="firmware"). - ScreenService.toggle(): never called. - GET /api/lirc (LircConfigOut, api.lircConfig): no front-end reads it. - POST /api/volume's delta_percent (api.nudgeVolume): no front-end sends it. - LibraryCache.shrink_stored_covers: a one-off migration that re-read every cover header on every scan. _INDEX_VERSION 7 rebuilds any cache that could still need it. - Firmware: the MFRC522 library and its unused globals (lib/nfc/rc522.c is the driver in use), and mfrc_version.h, a reader test sketch nothing included. Co-Authored-By: Claude Opus 5.5 --- esp-firmware/platformio.ini | 1 - esp-firmware/src/main.cpp | 4 -- esp-firmware/src/mfrc_version.h | 64 ------------------- python-backend/README.md | 2 +- python-backend/musicmouse/devices/mouse.py | 7 +- python-backend/musicmouse/devices/wire.py | 6 +- python-backend/musicmouse/events.py | 13 ---- python-backend/musicmouse/library/cache.py | 47 +------------- python-backend/musicmouse/library/scanner.py | 7 -- python-backend/musicmouse/services/screen.py | 3 - python-backend/musicmouse/services/web/api.py | 22 +------ .../musicmouse/services/web/schemas.py | 12 +--- python-backend/tests/test_library.py | 43 ------------- python-backend/tests/test_remote_api.py | 7 -- python-backend/tests/test_web.py | 18 ------ web/src/api/client.ts | 6 -- web/src/api/types.ts | 5 -- 17 files changed, 10 insertions(+), 257 deletions(-) delete mode 100644 esp-firmware/src/mfrc_version.h diff --git a/esp-firmware/platformio.ini b/esp-firmware/platformio.ini index 6e38933..fe14304 100644 --- a/esp-firmware/platformio.ini +++ b/esp-firmware/platformio.ini @@ -25,7 +25,6 @@ upload_port = /dev/ttyUSB0 monitor_speed = 115200 build_src_filter = +<*> - lib_deps = - miguelbalboa/MFRC522@1.4.12 bxparks/AceButton@1.10.1 [env:native] diff --git a/esp-firmware/src/main.cpp b/esp-firmware/src/main.cpp index 04ff0c6..c685535 100644 --- a/esp-firmware/src/main.cpp +++ b/esp-firmware/src/main.cpp @@ -1,7 +1,6 @@ #include #include "rc522.h" #include "SPI.h" -#include #include "rotary_encoder.h" #include "containers/LedStripRGBW.h" @@ -21,9 +20,6 @@ #include "TaskLed.h" // -------------------------------------------------- RFID Reader ---------------------------------------- -MFRC522 rfid; -MFRC522::MIFARE_Key key; - void tagHandler(uint8_t *sn) { if (sn != nullptr) diff --git a/esp-firmware/src/mfrc_version.h b/esp-firmware/src/mfrc_version.h deleted file mode 100644 index e71cc31..0000000 --- a/esp-firmware/src/mfrc_version.h +++ /dev/null @@ -1,64 +0,0 @@ - -#include -#include - -#define RST_PIN 5 // Configurable, see typical pin layout above -#define SS_PIN 23 // Configurable, see typical pin layout above - -MFRC522 mfrc522(SS_PIN, RST_PIN); // Create MFRC522 instance - -//*****************************************************************************************// -void setup() { - Serial.begin(115200); - SPI.begin(22, 19, 18, 23); // Init SPI bus - mfrc522.PCD_Init(); // Init MFRC522 card -} -/** - * Helper routine to dump a byte array as hex values to Serial. - */ -void PrintHex(byte *buffer, byte bufferSize) { - for (byte i = 0; i < bufferSize; i++) { - Serial.print(buffer[i] < 0x10 ? " 0" : " "); - Serial.print(buffer[i], HEX); - } -} - -//*****************************************************************************************// - -void loop() { - - // Look for new cards - if ( !mfrc522.PICC_IsNewCardPresent()) { - return; - } - if ( !mfrc522.PICC_ReadCardSerial()) { - return; - } - - //mfrc522.PICC_DumpDetailsToSerial(&(mfrc522.uid)); //dump some details about the card - - Serial.print("NewCard"); - PrintHex(mfrc522.uid.uidByte, mfrc522.uid.size); - Serial.println(""); - - // Check if Card was removed - bool cardRemoved = false; - int counter = 0; - bool current, previous; - previous = !mfrc522.PICC_IsNewCardPresent(); - - while(!cardRemoved){ - current =!mfrc522.PICC_IsNewCardPresent(); - - if (current && previous) counter++; - - previous = current; - cardRemoved = (counter>2); - delay(50); - } - - Serial.println("Card was removed"); - delay(500); //change value if you want to read cards faster - mfrc522.PICC_HaltA(); - mfrc522.PCD_StopCrypto1(); -} \ No newline at end of file diff --git a/python-backend/README.md b/python-backend/README.md index 52158ae..a1c9642 100644 --- a/python-backend/README.md +++ b/python-backend/README.md @@ -220,7 +220,7 @@ copied to the device. | `WS /api/ws` | Push only. A snapshot on connect, then a frame per change, plus the position at 2 Hz while playing, and `clock_toggle` when the remote's Guide key asks for the clock. | | `POST /api/play` | `{album_id, track_index?}` | | `POST /api/resume` `/pause` `/next` `/previous` `/seek` | transport | -| `POST /api/volume` | `{percent}`, `{delta_percent}`, or `{steps}` - the IR remote's step (`volume_increment` device units) per step | +| `POST /api/volume` | `{percent}` or `{steps}` - the IR remote's step (`volume_increment` device units) per step | | `POST /api/remote/press/{digit}` | what the IR remote's number key does - the keyboard's digits | | `GET` `PUT /api/remote/mapping` | number key -> album (optionally from one `track_index`) or podcast show | | `POST /api/screen/toggle` `/screen/activity` | the kiosk's Del key; its key presses and taps, holding off the idle screen-off | diff --git a/python-backend/musicmouse/devices/mouse.py b/python-backend/musicmouse/devices/mouse.py index 746d928..f098fcf 100644 --- a/python-backend/musicmouse/devices/mouse.py +++ b/python-backend/musicmouse/devices/mouse.py @@ -29,8 +29,6 @@ from musicmouse.events import ( ActiveFigureChanged, ButtonEvent, ConnectionChanged, - DeviceConnected, - DeviceDisconnected, EventSource, InputEvent, LedEffectChanged, @@ -136,7 +134,6 @@ class MusicMouseDevice: def on_connected(self) -> None: """Re-apply memorized state, so a reconnect is invisible from the outside.""" - self._bus.emit(DeviceConnected(port=self.port, source="device")) self._bus.emit(ConnectionChanged(target="firmware", connected=True, source="device")) for zone, effect in self._effects.items(): @@ -146,8 +143,8 @@ class MusicMouseDevice: if self._effects: _log.info("Restored %d LED zone(s) after reconnect", len(self._effects)) - def on_disconnected(self, reason: str = "") -> None: - self._bus.emit(DeviceDisconnected(port=self.port, reason=reason or None, source="device")) + def on_disconnected(self, _reason: str = "") -> None: + # SerialLink already logs the reason; the event only says the link is down. self._bus.emit(ConnectionChanged(target="firmware", connected=False, source="device")) def feed(self, data: bytes) -> None: diff --git a/python-backend/musicmouse/devices/wire.py b/python-backend/musicmouse/devices/wire.py index 9abbce2..7554599 100644 --- a/python-backend/musicmouse/devices/wire.py +++ b/python-backend/musicmouse/devices/wire.py @@ -13,7 +13,7 @@ newlines and on the magic token. from __future__ import annotations import struct -from dataclasses import dataclass, replace +from dataclasses import dataclass from enum import IntEnum from typing import TypeAlias @@ -345,10 +345,6 @@ class FrameDecoder: return None -def with_figure(event: RfidTokenRead, figure: str | None, *, known: bool) -> RfidTokenRead: - """Attach a resolved figure name to a decoded tag read.""" - return replace(event, figure=figure, known=known) - # ------------------------------------------------- host -> firmware, read back # diff --git a/python-backend/musicmouse/events.py b/python-backend/musicmouse/events.py index 0f90732..30fe922 100644 --- a/python-backend/musicmouse/events.py +++ b/python-backend/musicmouse/events.py @@ -30,8 +30,6 @@ __all__ = [ "CancelTimerRequested", "ClockStateChanged", "ConnectionChanged", - "DeviceConnected", - "DeviceDisconnected", "Event", "EventSource", "FigureSelectionChanged", @@ -175,17 +173,6 @@ class PlaylistFinished(InputEvent): """The player reached the end of the playlist.""" -@dataclass(frozen=True, slots=True, kw_only=True) -class DeviceConnected(InputEvent): - port: str - - -@dataclass(frozen=True, slots=True, kw_only=True) -class DeviceDisconnected(InputEvent): - port: str - reason: str | None = None - - # -------------------------------------------------------------------------- intents diff --git a/python-backend/musicmouse/library/cache.py b/python-backend/musicmouse/library/cache.py index 44824d7..27be6fd 100644 --- a/python-backend/musicmouse/library/cache.py +++ b/python-backend/musicmouse/library/cache.py @@ -57,6 +57,8 @@ __all__ = ["Fingerprint", "LibraryCache"] # 7: a figure's albums may sit in subfolders. `figure` is now the folder directly under # Figuren rather than the album folder's own name, and such an album is titled # "Fuchs - " - neither of which an entry cached before can know. +# The same rescan retired `shrink_stored_covers`, which re-read every cover header on +# every scan to catch pre-384 px covers and covers stored before thumbnails existed. # # Bumping this is cheap by design - see the module docstring. _INDEX_VERSION = 7 @@ -119,51 +121,6 @@ class LibraryCache: ) return path - def _refresh_thumb(self, cover: Path) -> bool: - """Make ``.thumb.jpg`` exist and be newer than the cover it came from.""" - thumb = cover.with_name(f"{cover.stem}.thumb.jpg") - try: - if thumb.exists() and thumb.stat().st_mtime_ns >= cover.stat().st_mtime_ns: - return False - thumb.write_bytes( - shrink_cover(cover.read_bytes(), max_px=THUMB_COVER_PX, quality=_THUMB_JPEG_QUALITY) - ) - except OSError: # pragma: no cover - a cache we cannot write is not fatal - return False - return True - - def shrink_stored_covers(self) -> int: - """Rewrite any already-stored cover that predates :data:`MAX_COVER_PX`. - - Needed because :func:`~musicmouse.library.scanner.scan_library` reuses an album - whose fingerprint is unchanged *without* re-reading its tags, so a cover written - by an older version would otherwise never be touched again. Reading a JPEG's - dimensions only parses its header, so once every file is within the limit this - costs one small read per album and nothing else. - - Also backfills any missing (or stale) thumbnail. Returns the number of full-size - files actually rewritten. - """ - rewritten = 0 - for path in sorted(self.covers.glob("*.jpg")): - if path.name.endswith(".thumb.jpg"): - continue - try: - with Image.open(path) as image: - oversized = max(image.size) > MAX_COVER_PX - except (OSError, UnidentifiedImageError): - continue - if oversized: - try: - shrunk = shrink_cover(path.read_bytes()) - path.write_bytes(shrunk) - except OSError: # pragma: no cover - a cache we cannot write is not fatal - continue - rewritten += 1 - # Covers stored before thumbnails existed get theirs here, without a rescan. - self._refresh_thumb(path) - return rewritten - # ------------------------------------------------------------------ analysis def load_analysis(self, key: str) -> TrackAnalysis | None: diff --git a/python-backend/musicmouse/library/scanner.py b/python-backend/musicmouse/library/scanner.py index a813949..39ea66a 100644 --- a/python-backend/musicmouse/library/scanner.py +++ b/python-backend/musicmouse/library/scanner.py @@ -317,13 +317,6 @@ def scan_library( figure name to what it holds, which is the one thing the folders cannot say. """ cache.prepare() - # Covers written before MAX_COVER_PX existed are still whatever size the tag held, - # and the reuse path below means an unchanged album never rewrites its own. One - # pass here catches them; after the first run every file is already small and this - # is 300-odd header reads. - shrunk = cache.shrink_stored_covers() - if shrunk: - _log.info("Downscaled %d oversized cover(s) in the cache", shrunk) known = known or {} out: dict[str, tuple[Album, Fingerprint]] = {} last_report = time.monotonic() diff --git a/python-backend/musicmouse/services/screen.py b/python-backend/musicmouse/services/screen.py index 6b8f1b5..1abb63e 100644 --- a/python-backend/musicmouse/services/screen.py +++ b/python-backend/musicmouse/services/screen.py @@ -141,9 +141,6 @@ class ScreenService: self._running = asyncio.create_task(self._run(command, sound=sound), name="screen") return True - async def toggle(self) -> None: - await self._run(self.config.toggle_command, sound=True) - async def _run(self, command: str, *, sound: bool) -> None: argv = shlex.split(command) try: diff --git a/python-backend/musicmouse/services/web/api.py b/python-backend/musicmouse/services/web/api.py index 92c877c..5084b66 100644 --- a/python-backend/musicmouse/services/web/api.py +++ b/python-backend/musicmouse/services/web/api.py @@ -46,7 +46,6 @@ from musicmouse.services.web.schemas import ( HaConfigOut, HaDeviceOut, LibraryOut, - LircConfigOut, PlayerStateOut, PlayIn, RemoteMappingIn, @@ -68,7 +67,6 @@ from musicmouse.services.web.schemas import ( from musicmouse.services.web.settings import ( read_settings, to_device_volume, - to_percent, write_settings, ) from musicmouse.services.web.state import snapshot @@ -234,18 +232,10 @@ def build_router( return emit( VolumeChangeRequested(delta=general.volume_increment * body.steps, source="web") ) - if body.percent is not None: - target = body.percent - elif body.delta_percent is not None: - target = to_percent(app.player.volume, general) + body.delta_percent - else: - raise HTTPException( - status_code=422, detail="percent, delta_percent or steps required" - ) + if body.percent is None: + raise HTTPException(status_code=422, detail="percent or steps required") return emit( - SetVolumeRequested( - volume=to_device_volume(max(0, min(100, target)), general), source="web" - ) + SetVolumeRequested(volume=to_device_volume(body.percent, general), source="web") ) # ------------------------------------------------------------- parent mode @@ -321,12 +311,6 @@ def build_router( # -------------------------------------------------------------- IR remote - @router.get("/lirc") - def get_lirc_config() -> LircConfigOut: - if app.config.general.lirc is None: - raise HTTPException(status_code=404, detail="lirc not configured") - return LircConfigOut(connected=app.state.lirc_connected) - @router.post("/remote/press/{digit}", status_code=204) def press_remote(digit: Digit) -> Response: # A keyboard's number keys, doing what the IR remote's do. An unassigned key diff --git a/python-backend/musicmouse/services/web/schemas.py b/python-backend/musicmouse/services/web/schemas.py index fb5725a..63329e9 100644 --- a/python-backend/musicmouse/services/web/schemas.py +++ b/python-backend/musicmouse/services/web/schemas.py @@ -26,7 +26,6 @@ __all__ = [ "HaConfigOut", "HaDeviceOut", "LibraryOut", - "LircConfigOut", "PlayIn", "PlayerStateOut", "RemoteMappingIn", @@ -261,15 +260,13 @@ class SeekIn(BaseModel): class VolumeIn(BaseModel): - """An absolute percentage, a relative percentage, or a number of steps; exactly - one of the three. + """An absolute percentage or a number of steps; exactly one of the two. ``steps`` moves by ``volume_increment`` device units per step - the same step the IR remote's volume keys and the rotary encoder take - rather than by a percentage. """ percent: int | None = Field(default=None, ge=0, le=100) - delta_percent: int | None = Field(default=None, ge=-100, le=100) steps: int | None = Field(default=None, ge=-10, le=10) @@ -325,13 +322,6 @@ class HaConfigOut(BaseModel): scenes: list[HaDeviceOut] -class LircConfigOut(BaseModel): - """Presence-only, like ``HaConfigOut``: there is nothing secret in a host/port, - but the frontend only needs to know whether the remote is set up and connected.""" - - connected: bool - - class RemoteSlotOut(BaseModel): digit: str target_kind: Literal["album", "series"] diff --git a/python-backend/tests/test_library.py b/python-backend/tests/test_library.py index f3c793b..0276344 100644 --- a/python-backend/tests/test_library.py +++ b/python-backend/tests/test_library.py @@ -916,46 +916,3 @@ class TestCoverDownscaling: assert thumb.format == "JPEG" assert "progressive" not in thumb.info assert abs(thumb.width / thumb.height - 2400 / 1800) < 0.02 - - def test_thumbnails_are_backfilled_for_covers_stored_without_one( - self, tmp_path: Path - ) -> None: - from PIL import Image - - from musicmouse.library.cache import THUMB_COVER_PX, LibraryCache - - cache = LibraryCache(tmp_path) - cache.prepare() - cache.cover_path("old").write_bytes(self._jpeg((384, 384))) - assert not cache.thumb_path("old").exists() - - assert cache.shrink_stored_covers() == 0 # nothing oversized... - with Image.open(cache.thumb_path("old")) as thumb: # ...but the thumb appeared - assert max(thumb.size) == THUMB_COVER_PX - # Steady state: the thumbnail is not regenerated, and is not mistaken for a cover. - before = cache.thumb_path("old").stat().st_mtime_ns - assert cache.shrink_stored_covers() == 0 - assert cache.thumb_path("old").stat().st_mtime_ns == before - assert not cache.thumb_path("old.thumb").exists() - - def test_covers_written_by_an_older_version_are_migrated(self, tmp_path: Path) -> None: - """The scanner reuses an unchanged album without re-reading its tags, so a - cover stored before the limit existed would otherwise never be rewritten.""" - from PIL import Image - - from musicmouse.library.cache import MAX_COVER_PX, LibraryCache - - cache = LibraryCache(tmp_path) - cache.prepare() - stale = cache.cover_path("old") - stale.write_bytes(self._jpeg((3000, 3000))) - fresh = cache.cover_path("new") - fresh.write_bytes(self._jpeg((320, 320))) - fresh_before = fresh.read_bytes() - - assert cache.shrink_stored_covers() == 1 - with Image.open(stale) as image: - assert max(image.size) == MAX_COVER_PX - assert fresh.read_bytes() == fresh_before - # Second pass has nothing left to do - the cheap steady state. - assert cache.shrink_stored_covers() == 0 diff --git a/python-backend/tests/test_remote_api.py b/python-backend/tests/test_remote_api.py index 07b5251..044c0b9 100644 --- a/python-backend/tests/test_remote_api.py +++ b/python-backend/tests/test_remote_api.py @@ -65,13 +65,6 @@ async def album_id(sim: Simulation, title: str) -> str: return next(a.id for a in sim.app.library.albums if a.title == title) -# ------------------------------------------------------------------------------ lirc - - -async def test_lirc_is_a_404_when_unconfigured(client: httpx2.AsyncClient) -> None: - assert (await client.get("/api/lirc")).status_code == 404 - - # --------------------------------------------------------------------------- reading diff --git a/python-backend/tests/test_web.py b/python-backend/tests/test_web.py index 6b8c0d8..91a6c75 100644 --- a/python-backend/tests/test_web.py +++ b/python-backend/tests/test_web.py @@ -288,24 +288,6 @@ async def test_volume_scales_across_the_allowed_range(client: Client, sim: Simul assert (await client.get("/api/state")).json()["volume"] == 50 -async def test_volume_steps_are_relative_to_the_percentage(client: Client, sim: Simulation) -> None: - await client.post("/api/volume", json={"percent": 50}) - await sim.bus.drain() - await client.post("/api/volume", json={"delta_percent": 10}) - await sim.bus.drain() - - assert (await client.get("/api/state")).json()["volume"] == 60 - assert sim.player.volume == 36 - - -async def test_volume_steps_clamp_at_the_ends(client: Client, sim: Simulation) -> None: - await client.post("/api/volume", json={"percent": 95}) - await sim.bus.drain() - await client.post("/api/volume", json={"delta_percent": 20}) - await sim.bus.drain() - assert (await client.get("/api/state")).json()["volume"] == 100 - - async def test_remote_steps_move_by_the_volume_increment(client: Client, sim: Simulation) -> None: """The +/- buttons take the IR remote's step, in device units, not a percentage.""" await client.post("/api/volume", json={"percent": 50}) diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 9953b4b..b5c631e 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -4,7 +4,6 @@ import type { Album, HaConfig, HaEntityState, - LircConfig, PlayerState, RemoteMapping, RemoteSlotInput, @@ -55,9 +54,6 @@ async function fetchOrNullOnError(path: string): Promise { /** `null` means the room-control page isn't configured, not an error. */ const fetchHaConfig = (): Promise => fetchOrNullOn404("/ha"); -/** `null` means the IR remote isn't configured, not an error. */ -const fetchLircConfig = (): Promise => fetchOrNullOn404("/lirc"); - /** `null` means the typing game isn't configured, not an error - the tab still shows, * just with a "not set up" placeholder instead of a lesson map. */ const fetchTippenCurriculum = (): Promise => @@ -96,7 +92,6 @@ export const api = { previous: () => post("/previous"), seek: (position: number) => post("/seek", { position }), setVolume: (percent: number) => post("/volume", { percent }), - nudgeVolume: (deltaPercent: number) => post("/volume", { delta_percent: deltaPercent }), /** One step of the IR remote's volume keys per unit, in the device's own increment. */ stepVolume: (steps: number) => post("/volume", { steps }), toggleScreen: () => post("/screen/toggle"), @@ -118,7 +113,6 @@ export const api = { trackDetail: fetchTrackDetail, - lircConfig: fetchLircConfig, pressRemote: (digit: string) => post(`/remote/press/${digit}`), remoteMapping: () => request("/remote/mapping"), saveRemoteMapping: (slots: Record) => diff --git a/web/src/api/types.ts b/web/src/api/types.ts index b3f71d5..a4c672b 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -171,11 +171,6 @@ export interface RemoteSlotInput { track_index?: number; } -/** Presence-only, like `HaConfig` - there is nothing secret in a host/port. */ -export interface LircConfig { - connected: boolean; -} - // --------------------------------------------------------------------------- tippen // // The typing game. `null` from `GET /tippen/curriculum` means the whole feature isn't