Updated with knightrider LED

This commit is contained in:
David Rice
2026-06-12 15:33:31 +02:00
parent 386ff588e3
commit ed6e8ec107

View File

@@ -14,6 +14,18 @@ import struct
import wave import wave
import tempfile import tempfile
import io import io
import time
try:
import usb.core
import usb.util
_HAS_USB = True
except ImportError:
_HAS_USB = False
# ── STM32 companion USB IDs — update after running lsusb ─────────────────────
_STM32_VID = None # e.g. 0x0483
_STM32_PID = None # e.g. 0x5740
# ── Force framebuffer QPA before any Qt import ──────────────────────────────── # ── Force framebuffer QPA before any Qt import ────────────────────────────────
if 'DISPLAY' not in os.environ and 'QT_QPA_PLATFORM' not in os.environ: if 'DISPLAY' not in os.environ and 'QT_QPA_PLATFORM' not in os.environ:
@@ -295,6 +307,117 @@ def _vol_btn_style() -> str:
""" """
# ─────────────────────────────────────────────────────────────────────────────
# LED Controller (STM32 companion via USB vendor control transfers)
# ─────────────────────────────────────────────────────────────────────────────
# Arrive brand colours for the LEDs
_LED_PURPLE = (95, 1, 111) # P1 #5F016F
_LED_PINK = (255, 51, 187) # P2 #FF33BB
_BMREQ = 0x41 # Vendor | Interface | Out
_CMD_BRI = 0x00 # SET_BRIGHTNESS
_CMD_COL = 0x01 # SET_COLOR / SET_PATTERN
class LedController:
"""
Controls 2 RGB LED strings on the STM32 companion chip via USB.
Runs a knight-rider thread: pink strobe bounces over a purple base.
"""
STRING_MAIN = 0
STRING_EXT = 1
def __init__(self):
self._dev = None
self._running = False
self._thread = None
if _HAS_USB and _STM32_VID:
try:
self._dev = usb.core.find(idVendor=_STM32_VID, idProduct=_STM32_PID)
if self._dev:
self._dev.set_configuration()
except Exception:
self._dev = None
# ── Public ────────────────────────────────────────────────────────────────
def start(self):
if self._dev is None or self._running:
return
self._running = True
self._thread = threading.Thread(target=self._knight_rider, daemon=True)
self._thread.start()
def stop(self):
self._running = False
if self._dev:
self._set_brightness(self.STRING_MAIN, 0)
# ── USB helpers ───────────────────────────────────────────────────────────
def _windex(self, string):
return (string << 8) | 0x01
def _set_color(self, string, r, g, b, brightness=255):
try:
self._dev.ctrl_transfer(
_BMREQ, _CMD_COL, brightness, self._windex(string), [r, g, b]
)
except Exception:
pass
def _set_brightness(self, string, brightness):
try:
self._dev.ctrl_transfer(
_BMREQ, _CMD_BRI, brightness, self._windex(string), None
)
except Exception:
pass
def _set_pattern(self, string, steps):
"""steps: list of (brightness, duration_ms) — max 5."""
data = []
for bri, dur in steps[:5]:
data += [bri & 0xFF, dur & 0xFF, (dur >> 8) & 0xFF]
try:
self._dev.ctrl_transfer(
_BMREQ, _CMD_COL, 0, self._windex(string), data
)
except Exception:
pass
# ── Knight rider loop ─────────────────────────────────────────────────────
def _knight_rider(self):
STEPS = 20 # steps per half-cycle
STEP_MS = 0.03 # 30 ms per step → ~600 ms fade up, ~600 ms fade down
while self._running:
# Fade purple → pink
for i in range(STEPS):
if not self._running:
return
t = i / STEPS
r = int(_LED_PURPLE[0] + (_LED_PINK[0] - _LED_PURPLE[0]) * t)
g = int(_LED_PURPLE[1] + (_LED_PINK[1] - _LED_PURPLE[1]) * t)
b = int(_LED_PURPLE[2] + (_LED_PINK[2] - _LED_PURPLE[2]) * t)
self._set_color(self.STRING_MAIN, r, g, b)
time.sleep(STEP_MS)
# Fade pink → purple
for i in range(STEPS):
if not self._running:
return
t = i / STEPS
r = int(_LED_PINK[0] + (_LED_PURPLE[0] - _LED_PINK[0]) * t)
g = int(_LED_PINK[1] + (_LED_PURPLE[1] - _LED_PINK[1]) * t)
b = int(_LED_PINK[2] + (_LED_PURPLE[2] - _LED_PINK[2]) * t)
self._set_color(self.STRING_MAIN, r, g, b)
time.sleep(STEP_MS)
def _autocrop(img): def _autocrop(img):
"""Crop a QImage (ARGB32) to the bounding box of non-transparent pixels.""" """Crop a QImage (ARGB32) to the bounding box of non-transparent pixels."""
if not _HAS_NUMPY: if not _HAS_NUMPY:
@@ -361,8 +484,10 @@ class MainWindow(QMainWindow):
super().__init__() super().__init__()
self.audio = AudioEngine() self.audio = AudioEngine()
self.audio.signals.status.connect(self._on_audio_status) self.audio.signals.status.connect(self._on_audio_status)
self.leds = LedController()
self._build_ui() self._build_ui()
self._set_bg(C_BG) self._set_bg(C_BG)
self.leds.start()
# ── UI construction ─────────────────────────────────────────────────────── # ── UI construction ───────────────────────────────────────────────────────
@@ -604,6 +729,7 @@ class MainWindow(QMainWindow):
self._fb_snapshot = data self._fb_snapshot = data
def closeEvent(self, event): def closeEvent(self, event):
self.leds.stop()
self.audio.shutdown() self.audio.shutdown()
super().closeEvent(event) super().closeEvent(event)
# Restore the framebuffer to what it looked like before Qt launched # Restore the framebuffer to what it looked like before Qt launched