Update xvf3800_led_backend.py

Extends the existing XVF3800 USB control backend beyond LEDs to support startup preflight commands needed for audio reliability.

Implements command execution internally (no reliance on xvf_host.py) using the same USB control-transfer approach already used for LED features.
This commit is contained in:
imonlinux
2026-01-05 18:57:52 -08:00
committed by GitHub
parent 3ba4e748ee
commit b8a3ce2c39
@@ -16,6 +16,7 @@ from __future__ import annotations
import logging
import struct
import time
from typing import List, Optional, Sequence, Tuple
import usb.core # type: ignore[import]
@@ -33,6 +34,14 @@ PARAMETERS = {
# ---------------------------------------------------------------------
"VERSION": (48, 0, 3, "ro", "uint8"),
# Firmware / device control
"REBOOT": (48, 7, 1, "wo", "uint8"),
"SAVE_CONFIGURATION": (48, 9, 1, "wo", "uint8"),
# Audio manager output routing (category, source) per channel
"AUDIO_MGR_OP_L": (35, 15, 2, "rw", "uint8"),
"AUDIO_MGR_OP_R": (35, 19, 2, "rw", "uint8"),
# ---------------------------------------------------------------------
# GPO_SERVICER_RESID (LED controls)
# ---------------------------------------------------------------------
@@ -58,6 +67,14 @@ class _ReSpeaker:
def __init__(self, dev: "usb.core.Device") -> None: # type: ignore[name-defined]
self.dev = dev
def close(self) -> None:
"""Release any underlying libusb resources (best-effort)."""
try:
usb.util.dispose_resources(self.dev)
except Exception:
pass
# ------------------------------------------------------------------
# Encoding / decoding helpers
# ------------------------------------------------------------------
@@ -175,6 +192,69 @@ def _find_device(vid: int = _ReSpeaker.VID, pid: int = _ReSpeaker.PID) -> Option
return _ReSpeaker(dev)
class XVF3800USBDevice:
"""Small helper for issuing non-LED XVF3800 control commands via PyUSB.
This is intentionally minimal and does *not* depend on xvf_host.py.
"""
def __init__(self, vid: int = _ReSpeaker.VID, pid: int = _ReSpeaker.PID):
self._rsp = _find_device(vid=vid, pid=pid)
def close(self) -> None:
try:
self._rsp.close()
except Exception:
pass
# --- Device control -----------------------------------------------------
def reboot(self) -> None:
"""Reboot the XVF3800 firmware (resets parameters to defaults)."""
self._rsp.write("REBOOT", [1])
def save_configuration(self) -> None:
"""Persist current runtime settings to flash."""
self._rsp.write("SAVE_CONFIGURATION", [1])
# --- Audio routing ------------------------------------------------------
def set_audio_mgr_op_l(self, category: int, source: int) -> None:
"""Set the L output channel source selection (category, source)."""
self._rsp.write("AUDIO_MGR_OP_L", [int(category) & 0xFF, int(source) & 0xFF])
def set_audio_mgr_op_r(self, category: int, source: int) -> None:
"""Set the R output channel source selection (category, source)."""
self._rsp.write("AUDIO_MGR_OP_R", [int(category) & 0xFF, int(source) & 0xFF])
# --- Wait helpers -------------------------------------------------------
@staticmethod
def wait_for_reenumeration(
vid: int = _ReSpeaker.VID,
pid: int = _ReSpeaker.PID,
timeout_s: float = 12.0,
settle_s: float = 1.0,
) -> None:
"""Wait for the XVF3800 to disappear and then reappear on USB."""
start = time.time()
# Wait for it to disappear (best effort)
while time.time() - start < timeout_s:
if usb.core.find(idVendor=vid, idProduct=pid) is None:
break
time.sleep(0.1)
# Wait for it to reappear
while time.time() - start < timeout_s:
if usb.core.find(idVendor=vid, idProduct=pid) is not None:
break
time.sleep(0.1)
# Give kernel/userspace audio stack a moment to settle
if settle_s > 0:
time.sleep(settle_s)
class XVF3800LedBackend:
"""High-level LED backend for the XVF3800."""