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:
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import struct
|
import struct
|
||||||
|
import time
|
||||||
from typing import List, Optional, Sequence, Tuple
|
from typing import List, Optional, Sequence, Tuple
|
||||||
|
|
||||||
import usb.core # type: ignore[import]
|
import usb.core # type: ignore[import]
|
||||||
@@ -33,6 +34,14 @@ PARAMETERS = {
|
|||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
"VERSION": (48, 0, 3, "ro", "uint8"),
|
"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)
|
# GPO_SERVICER_RESID (LED controls)
|
||||||
# ---------------------------------------------------------------------
|
# ---------------------------------------------------------------------
|
||||||
@@ -58,6 +67,14 @@ class _ReSpeaker:
|
|||||||
def __init__(self, dev: "usb.core.Device") -> None: # type: ignore[name-defined]
|
def __init__(self, dev: "usb.core.Device") -> None: # type: ignore[name-defined]
|
||||||
self.dev = dev
|
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
|
# Encoding / decoding helpers
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -175,6 +192,69 @@ def _find_device(vid: int = _ReSpeaker.VID, pid: int = _ReSpeaker.PID) -> Option
|
|||||||
return _ReSpeaker(dev)
|
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:
|
class XVF3800LedBackend:
|
||||||
"""High-level LED backend for the XVF3800."""
|
"""High-level LED backend for the XVF3800."""
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user