Files
linux-voice-assistant/tests/xvf3800_hid_mute_probe.py
T
imonlinux 44091bbc8b Add files via upload
What it does:

Uses hidapi (pip install hidapi) to:

Enumerate devices with VID/PID 0x2886:0x001a (Seeed / XVF3800).

Open the first matching HID interface (or a specific --path).

Read reports in a loop and print them as timestamps + hex blobs.

You (or testers) run it, press/release the XVF mute button a few times, and paste the output into the Discussion so we can see which byte/bit is the mute state.
2025-12-06 18:32:30 -08:00

183 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""ReSpeaker XVF3800 HID mute/button probe.
This script helps reverse-engineer how the XVF3800 exposes its mute button over USB HID.
It is meant to be run from the linux-voice-assistant virtualenv on a system where the
XVF3800 is connected via USB.
Requirements:
pip install hidapi
Usage examples:
# Basic usage (auto-detect first 2886:001a HID interface)
python tests/xvf3800_hid_mute_probe.py
# Explicit vendor/product (optional, defaults are correct for XVF3800)
python tests/xvf3800_hid_mute_probe.py --vendor-id 0x2886 --product-id 0x001a
Once running, press and release the XVF3800 mute button a few times. The script will
print raw HID reports in hex. Share that output in the Discussion so we can identify
which byte/bit corresponds to the mute state.
"""
import argparse
import binascii
import datetime as dt
import sys
import time
from typing import List, Optional
try:
import hid # type: ignore[import]
except Exception as exc: # pragma: no cover - environment dependent
print("ERROR: Failed to import 'hid' (hidapi).", file=sys.stderr)
print(" Install it with: pip install hidapi", file=sys.stderr)
print(f" Details: {exc}", file=sys.stderr)
sys.exit(1)
def _fmt_vid_pid(vid: int, pid: int) -> str:
return f"0x{vid:04x}:0x{pid:04x}"
def list_matching_devices(vendor_id: int, product_id: int) -> List[dict]:
devices = []
for dev in hid.enumerate():
if dev.get("vendor_id") == vendor_id and dev.get("product_id") == product_id:
devices.append(dev)
return devices
def choose_device(devices: List[dict]) -> Optional[dict]:
"""Pick a device from the list; if only one, return it, else pick the first.
We log all candidates so users can see what's on their system.
"""
if not devices:
return None
print("Found the following matching HID interfaces for XVF3800:")
for idx, dev in enumerate(devices):
path = dev.get("path")
serial = dev.get("serial_number") or "<none>"
mfg = dev.get("manufacturer_string") or "<unknown>"
prod = dev.get("product_string") or "<unknown>"
iface = dev.get("interface_number", -1)
print(
f" [{idx}] path={path!r}, serial={serial!r}, iface={iface}, "
f"manufacturer={mfg!r}, product={prod!r}"
)
print()
print("NOTE: Using index 0 by default. If this is not the HID interface, ")
print(" rerun with --path <exact path> from the list above.\n")
return devices[0]
def open_device(
vendor_id: int,
product_id: int,
path: Optional[bytes] = None,
) -> hid.device:
"""Open a HID device for the given VID/PID (and optional path)."""
dev = hid.device()
if path is not None:
print(f"Opening HID device by path: {path!r}")
dev.open_path(path)
else:
print(f"Opening HID device by VID/PID: {_fmt_vid_pid(vendor_id, product_id)}")
dev.open(vendor_id, product_id)
dev.set_nonblocking(True)
return dev
def main() -> int:
parser = argparse.ArgumentParser(
description="Probe HID reports for the ReSpeaker XVF3800 mute button."
)
parser.add_argument(
"--vendor-id",
type=lambda x: int(x, 0),
default=0x2886,
help="USB vendor ID (default: 0x2886 for Seeed)",
)
parser.add_argument(
"--product-id",
type=lambda x: int(x, 0),
default=0x001a,
help="USB product ID (default: 0x001a for XVF3800)",
)
parser.add_argument(
"--path",
type=str,
default="",
help="Optional exact HID path to open (overrides VID/PID selection)",
)
parser.add_argument(
"--timeout-ms",
type=int,
default=500,
help="Read timeout in milliseconds (default: 500)",
)
args = parser.parse_args()
print("=== XVF3800 HID Mute Probe ===")
print(f"Vendor/Product: {_fmt_vid_pid(args.vendor_id, args.product_id)}")
if args.path:
chosen_path: Optional[bytes] = args.path.encode("utf-8")
else:
devices = list_matching_devices(args.vendor_id, args.product_id)
if not devices:
print(
"No HID interfaces found for XVF3800 (VID/PID). Make sure the board is plugged in.",
file=sys.stderr,
)
return 1
chosen = choose_device(devices)
if chosen is None:
print("No device chosen.", file=sys.stderr)
return 1
chosen_path = chosen.get("path")
print("Using device:")
print(f" path={chosen_path!r}")
print(f" serial={chosen.get('serial_number')!r}")
print(f" interface_number={chosen.get('interface_number')}")
print()
try:
dev = open_device(args.vendor_id, args.product_id, chosen_path)
except Exception as exc: # pragma: no cover - hardware dependent
print(f"ERROR: Failed to open HID device: {exc}", file=sys.stderr)
return 1
print("Listening for HID reports. Press and release the XVF3800 mute button a few times.")
print("Press Ctrl+C to stop.\n")
try:
while True:
data = dev.read(64, timeout_ms=args.timeout_ms)
if data:
now = dt.datetime.now().strftime("%H:%M:%S.%f")[:-3]
raw = bytes(data)
hex_str = binascii.hexlify(raw).decode("ascii")
# Trim trailing zeroes for readability
hex_str = hex_str.rstrip("0") or hex_str
print(f"[{now}] len={len(raw):02d} report={hex_str}")
time.sleep(0.01)
except KeyboardInterrupt:
print("\nStopping probe.")
finally:
try:
dev.close()
except Exception:
pass
return 0
if __name__ == "__main__": # pragma: no cover - script entry
raise SystemExit(main())