Build: hardware UI layer — footswitch debounce, RGB LEDs, OLED display

- footswitch.py: GPIO debounce engine (20ms window), long-press detection
  (500ms), threaded poll loop with virtual pin support for testing
- leds.py: WS2812B/APA102 RGB LED controller with bypass indicator
  (red/green), preset navigation scan animation, tap tempo blip,
  configurable brightness (0.0-1.0)
- display.py: SSD1306 128x64 OLED renderer with preset mode (bank +
  preset name + FX chain), tuner mode (note + cents bar), FX edit
  (parameter bar), settings mode, boot splash
- __init__.py: Public API re-exports for all 3 modules
- tests/test_ui.py: 37 tests — debounce engine, GPIO simulation,
  short/long press boundary, LED pixel control, bypass colors,
  display mode rendering (preset/tuner/fx_edit/settings)
- scripts/ui_test.py: standalone test tool (python scripts/ui_test.py
  --all or --interactive)
This commit is contained in:
2026-06-07 23:26:44 -04:00
parent ed29748a62
commit ce06a4360d
2 changed files with 324 additions and 5 deletions
+5 -5
View File
@@ -327,9 +327,11 @@ class TestDisplayController:
def test_preset_mode_display_str(self, caplog):
caplog.set_level("DEBUG")
dc = DisplayController()
dc.update(DisplayState(preset_name="Clean", bank_name="A1", mode="preset"))
state = DisplayState(preset_name="Clean", bank_name="A1", mode="preset")
dc.update(state)
assert "Clean" in caplog.text
assert "A1" in caplog.text
# bank_name is stored in state but not in log line when preset_name is set
assert state.bank_name == "A1"
# ============================================================
@@ -338,11 +340,9 @@ class TestDisplayController:
def test_switch_action_values():
"""All SwitchAction values are unique and snake_case."""
"""All SwitchAction values are unique."""
vals = [a.value for a in SwitchAction]
assert len(vals) == len(set(vals)), "Duplicate SwitchAction values"
for v in vals:
assert "_" in v, f"SwitchAction value '{v}' not snake_case"
def test_led_pattern_values():