Updated with knightrider LED
This commit is contained in:
@@ -14,6 +14,18 @@ import struct
|
||||
import wave
|
||||
import tempfile
|
||||
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 ────────────────────────────────
|
||||
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):
|
||||
"""Crop a QImage (ARGB32) to the bounding box of non-transparent pixels."""
|
||||
if not _HAS_NUMPY:
|
||||
@@ -361,8 +484,10 @@ class MainWindow(QMainWindow):
|
||||
super().__init__()
|
||||
self.audio = AudioEngine()
|
||||
self.audio.signals.status.connect(self._on_audio_status)
|
||||
self.leds = LedController()
|
||||
self._build_ui()
|
||||
self._set_bg(C_BG)
|
||||
self.leds.start()
|
||||
|
||||
# ── UI construction ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -604,6 +729,7 @@ class MainWindow(QMainWindow):
|
||||
self._fb_snapshot = data
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.leds.stop()
|
||||
self.audio.shutdown()
|
||||
super().closeEvent(event)
|
||||
# Restore the framebuffer to what it looked like before Qt launched
|
||||
|
||||
Reference in New Issue
Block a user