Commit Graph

10 Commits (390e290b570bfa4f8b30f222c5ee549b5d44b3dd)

Author SHA1 Message Date
Sean Doran 390e290b57
fix: prevent LED flicker from dropped knob messages at read-buffer boundary
parse_messages() previously advanced past a 0xFE start byte whenever no
complete frame could be matched, silently consuming partial frames at the
end of each ser.read(64) call as garbage.  When the device sends 20-50
rapid knob messages during a fast turn and one message straddles the
64-byte read boundary, the split message was discarded: the daemon skipped
a knob-position update, the LED color lagged, and when the next complete
message arrived the color jumped — producing a visible flicker.

Fix: break out of the parse loop when a 0xFE is followed by a known type
byte (0x02/0x03/0x06/0x07) but the buffer does not yet hold a complete
frame.  The partial bytes are returned in the remainder and prepended to
the next ser.read() chunk, so no message is ever lost.

Also sync last_led_colors after the initial send_leds() on connect so the
dedup guard is accurate from the first knob event instead of from the
first heartbeat.

Adds 5 tests covering partial heartbeat, button, and knob frames, the
split-read round-trip, and unknown frame type skipping (61 pass total).
2026-02-28 14:45:46 -05:00
Sean Doran 2dbc58f4e1
fix: update last_led_colors on heartbeat to prevent LED flicker
The heartbeat path called send_leds but never updated last_led_colors,
so the dedup guard in handle_knob had no visibility into what was last
actually sent. Heartbeats arriving mid-knob-turn injected extra serial
writes that bypassed the guard and overwhelmed the device's LED renderer,
causing visible flicker.
2026-02-27 21:25:21 -05:00
Sean Doran d69d94eaa2
fix: eliminate LED flicker during knob turns
Two targeted fixes:

1. Deduplicate LED writes in handle_knob: compute new colours and only
   call send_leds() when they differ from last_led_colors.  A single
   physical knob turn produces 20-50 ADC samples in quick succession;
   without this guard each sample triggers a 47-byte write and the
   firmware can't keep up, causing visible flicker.

2. Gate reapply_app_volumes on a 200 ms knob-quiet period.  Calling
   playerctl/pulsectl while the user is actively turning a knob stalls
   the main loop, backing up serial data, missing heartbeats, and
   causing the device LEDs to time out mid-turn.

Both state variables (last_led_colors, last_knob_event) are mutable
lists initialised in main() and threaded through handle_knob, keeping
module-level globals out of the picture.

Adds 6 new unit tests (56 total, all passing).
2026-02-27 19:04:53 -05:00
Sean Doran 1552d73412
Refactor: extract audio.py, add 50 unit tests
- Extract MPRISController and PulseController into src/turnup/audio.py;
  turnupd.py imports from there, dropping ~270 lines of duplicated code
- Add [project.optional-dependencies] dev = [pytest] and pytest testpaths
  to pyproject.toml
- Add tests/test_audio.py: 31 tests covering MPRISController player
  matching, volume clamping, cache TTL, and PulseController MPRIS-first
  routing, PA fallback, and drain_events
- Add tests/test_protocol.py: 19 tests covering parse_messages framing,
  multi-message buffers, garbage skipping, and knob conversion helpers
- All 50 tests pass (pytest 9.0, Python 3.14)
2026-02-27 18:40:13 -05:00
Sean Doran eba8e66879
Add MPRIS2 volume routing and PA event watcher; fix Spotify song transition reset
- Add MPRISController class: wraps playerctl with 3s player-list cache,
  fuzzy app-name matching, and thread-safe get/set_volume via MPRIS2 D-Bus
- Add PulseController event watcher thread: separate pulsectl connection
  listens for sink-input events and queues them so reapply fires within
  ~0.1s for PA-only apps instead of waiting up to 1s
- Route set_app_volume and get_app_volume_norm through MPRIS first,
  falling back to PA stream for non-MPRIS apps (e.g. Brave/Chromium)
- Update reapply_app_volumes to prefer MPRIS path, preventing turnupd
  from fighting Spotify on the PA layer during song transitions
- Wire MPRISController and start_watching() into main(); drain_events()
  triggers immediate reapply on any PA sink-input event
2026-02-27 18:28:56 -05:00
Sean Doran f1fa3080b5
Cap max volume at 100%; bump to v1.2.0 2026-02-27 15:03:50 -05:00
Sean Doran 25da5b46ad
Fix: auto-enable service on install, fix service startup, add config hot-reload; bump to v0.3.3 2026-02-27 13:54:57 -05:00
Sean Doran 60f6e6cb1d
Fix: keep LEDs lit via heartbeat refresh, init from actual volumes; bump to v0.3.1 2026-02-27 12:32:56 -05:00
Sean Doran 0cd1d6a926
Add LED support and remove legacy root-level scripts
- Add per-knob RGB LED configuration, validation, and color interpolation
- Add build_led_packet, send_leds, all_led_colors to turnupd
- Update config with VALID_LED_MODES, _validate_color, _validate_leds
- Update contrib/config.example.json with LED configuration examples
- Remove legacy root-level config.py and turnupd.py (moved to src/)
2026-02-27 11:35:54 -05:00
Sean Doran 002721c63c Initial commit 2026-02-27 10:08:09 -05:00