From b8a3ce2c39eabdb9b58bc34f2e4529cd116b888e Mon Sep 17 00:00:00 2001 From: imonlinux <39863321+imonlinux@users.noreply.github.com> Date: Mon, 5 Jan 2026 18:57:52 -0800 Subject: [PATCH] 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. --- linux_voice_assistant/xvf3800_led_backend.py | 80 ++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/linux_voice_assistant/xvf3800_led_backend.py b/linux_voice_assistant/xvf3800_led_backend.py index f3a9469..31bfd81 100644 --- a/linux_voice_assistant/xvf3800_led_backend.py +++ b/linux_voice_assistant/xvf3800_led_backend.py @@ -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."""