Updated with knightrider LED
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user