diff --git a/docs/linux-voice-assistant-xvf3800-mute.md b/docs/linux-voice-assistant-xvf3800-mute.md new file mode 100644 index 0000000..4c091b7 --- /dev/null +++ b/docs/linux-voice-assistant-xvf3800-mute.md @@ -0,0 +1,223 @@ +# XVF3800 Mute Button & USB Control Integration + +This document explains how the **ReSpeaker XVF3800 4‑Mic USB Array** integrates its +built‑in mute button and red mute LED with the **Linux Voice Assistant (LVA)**. + +The goal is: +- Pressing the XVF3800’s hardware mute button updates the LVA mic state. +- Muting/unmuting the LVA (via Home Assistant, MQTT, or other inputs) updates + the XVF3800’s mic mute circuit and red LED. +- No extra binaries (like the vendor `xvf_host`) are required — LVA talks to + the XVF3800 directly via **USB control transfers** using `pyusb`. + +--- + +## 1. How it Works (High‑Level) + +The XVF3800 exposes a vendor control protocol over USB that can read and write +internal GPIO pins (GPO). One of these pins drives both: + +- the microphone mute circuit +- the red mute LED on the XVF3800 front panel + +LVA’s `XVF3800ButtonController` does three things: + +1. **Reads hardware mute state periodically** + It polls the XVF3800’s GPO state (pin X0D30) in a background thread using + `pyusb`. If the value changes, LVA treats that as “the user pressed the + XVF3800 mute button” and publishes a `set_mic_mute` event. + +2. **Mirrors LVA mute → XVF3800** + It subscribes to `mic_muted` / `mic_unmuted` events. Whenever LVA’s mute + state changes (for example, via MQTT, HA UI, or a GPIO button), the + controller writes the new state back to X0D30 so that the mic mute circuit + and red LED stay in sync. + +3. **Initial sync on startup** + On first successful read, the controller treats the *hardware* mute state + as the source of truth and publishes `set_mic_mute` so LVA starts aligned + with whatever the XVF3800 is currently doing. + +No PulseAudio/PipeWire configuration is required for this feature; it only +depends on **USB access to the XVF3800** and the `pyusb` library. + +--- + +## 2. Prerequisites + +- You are running a branch of **linux‑voice‑assistant** that includes: + - `linux_voice_assistant/xvf3800_button_controller.py` + - The updated `__main__.py` and `config.py` wiring for `XVF3800ButtonController` +- The ReSpeaker XVF3800 is connected over USB and appears in `lsusb` as: + - `2886:001a Seeed Technology Co., Ltd. reSpeaker XVF3800 4-Mic Array` +- The LVA `.venv` includes `pyusb` (already present in `pyproject.toml`): + ```bash + source .venv/bin/activate + pip install pyusb + ``` + +On most systems, user‑mode processes can talk to the XVF3800 over USB without +`sudo` once the device is enumerated. If you see `USBError: [Errno 13] Access denied`, +you may need a udev rule — that’s outside this document but worth noting. + +--- + +## 3. Enabling the XVF3800 Mute Integration + +The integration is controlled via the `button` section of `config.json`. +Two “modes” are supported: + +- `"gpio"` – legacy Raspberry Pi GPIO button (ReSpeaker 2‑Mic HAT, etc.) +- `"xvf3800"` – USB‑based mute integration for the ReSpeaker XVF3800 + +### 3.1 Example `config.json` for XVF3800 + +Edit your LVA configuration file (usually `linux_voice_assistant/config.json`) +and add or adjust the `button` block like this: + +```jsonc +{ + "app": { + "name": "Linux Voice Assistant" + }, + + // ... other sections (audio, wake_word, esphome, led, mqtt, etc.) ... + + "button": { + "enabled": true, + "mode": "xvf3800", + // Optional: how often to poll the XVF3800 GPO state (seconds) + "poll_interval_seconds": 0.05 + } +} +``` + +Notes: + +- `"enabled": true` turns on button support in general. +- `"mode": "xvf3800"` switches from the GPIO‑based button controller to the + USB‑based XVF3800 controller. +- `poll_interval_seconds` is optional and defaults to `0.01` if omitted. Values + between 0.02 and 0.1 are typically fine. + +The GPIO‑specific fields (`pin`, `long_press_seconds`) are ignored in +`xvf3800` mode — the XVF3800’s onboard mute button is treated as a +**dedicated mute toggle only**, not a wake/stop button. + +--- + +## 4. Runtime Behaviour + +Once enabled and the LVA service is restarted, the mute integration behaves as follows: + +### 4.1 Pressing the XVF3800 Mute Button + +1. The XVF3800 firmware changes the GPO state of pin X0D30 and lights or clears + the red mute LED. +2. The `XVF3800ButtonController` sees the changed GPO values the next time it + polls USB. +3. It publishes a `set_mic_mute` event on the LVA `EventBus` with the new state. +4. The `MicMuteHandler`: + - Updates `ServerState.mic_muted` and `mic_muted_event` (pausing/resuming the + audio capture thread). + - Publishes `mic_muted` / `mic_unmuted` events used by other components + (LED controller, MQTT, etc.). + - Notifies the MQTT controller so the HA “Mute Microphone” entity stays in sync. + +From the user’s perspective: + +- The red LED on the XVF3800 tracks mic mute. +- The HA “Mute Microphone” switch also tracks the hardware button. +- The audio engine actually stops capturing when muted. + +### 4.2 Toggling Mute from Home Assistant / MQTT / Other Inputs + +1. Home Assistant toggles the LVA “Mute Microphone” switch (or another input + publishes a `set_mic_mute` event). +2. The `MicMuteHandler` applies the new mute state, as usual. +3. It emits a `mic_muted` or `mic_unmuted` event. +4. The `XVF3800ButtonController` receives this event and writes the new state + back to X0D30 via USB. +5. The XVF3800’s mic mute circuit and red LED update to match LVA. + +This keeps **hardware**, **LVA state**, and **Home Assistant UI** aligned. + +--- + +## 5. Coexistence with Other Buttons + +The XVF3800 mute integration is intentionally narrow in scope: + +- It does **not** implement short vs. long press behaviour. +- It does **not** trigger wake‑word, stop‑word, or playback controls. +- It is purely “mute on / mute off” with bidirectional sync. + +If you also have a GPIO button (e.g. a ReSpeaker 2‑Mic HAT momentary switch) +wired for wake/stop behaviour, you can: + +- Leave `button.mode: "gpio"` and **not** use the XVF3800 mute integration, or +- Move wake/stop to the GPIO button controller and leave the XVF3800 button + as mute‑only by setting `mode: "xvf3800"` and handling wake/stop elsewhere. + +At the moment, `button.mode` is **single‑valued** — it selects either the +GPIO controller or the XVF3800 controller, not both at once. If you ever want +to support both concurrently, that would be a separate enhancement (e.g. a +list of button backends). + +--- + +## 6. Troubleshooting + +### 6.1 No USB Access / Permission Errors + +Symptoms in the logs (journalctl or console): + +```text +Failed to initialize XVF3800 USB client; mute button integration will be disabled +USBError: [Errno 13] Access denied (insufficient permissions) +``` + +Check: + +- `lsusb` shows `2886:001a` while LVA is running. +- The LVA service is running as the user who owns the `.venv` and has + access to USB devices. +- If necessary, add a udev rule that grants access to `idVendor=0x2886` + and `idProduct=0x001a` for your LVA user, then reload udev and + replug the device. + +The controller will keep retrying USB initialization; once permissions are +fixed, it should connect without restarting LVA. + +### 6.2 Mute Button Works but HA Entity Doesn’t Update + +- Verify MQTT is enabled and configured correctly in `config.json`. +- Confirm that the HA “Mute Microphone” entity exists and belongs to the + correct LVA device. +- Check logs for `MicMuteHandler` and `MqttController` messages when pressing + the XVF3800 mute button. + +### 6.3 HA Mute Switch Works but XVF3800 LED Doesn’t Change + +- Ensure `button.enabled` is `true` and `"mode": "xvf3800"` in `config.json`. +- Look for log lines like: + - `Initializing XVF3800ButtonController (mode=xvf3800)` + - `Connected to ReSpeaker XVF3800 for mute control` +- If you see only GPIO logs, confirm that the branch you’re running includes + `xvf3800_button_controller.py` and the updated `__main__.py` wiring. + +--- + +## 7. Summary + +With `mode: "xvf3800"`, the Linux Voice Assistant treats the ReSpeaker +XVF3800’s onboard mute button and red LED as a **first‑class mute control**: + +- Pressing the XVF3800 mute button updates LVA and Home Assistant. +- Muting via Home Assistant or other inputs updates the XVF3800 hardware. +- The audio engine, LEDs, and MQTT entities stay in sync with the device’s + actual hardware mute state. + +This integration is fully optional and coexists with existing audio and LED +behaviour — it simply adds a reliable, hardware‑based mute path for the +XVF3800‑based satellites.