Use events and commands to control custom onboard leds and buttons. Can be used to communicate with an external Python script via a WebSocket API running in a container alongside LVA.
7.8 KiB
Jabra Speak 410
Peripheral controller for the Jabra Speak 410 USB speakerphone, running alongside the Linux Voice Assistant (LVA) container.
The controller connects to the Jabra Speak 410 via USB HID, drives its LED ring to reflect the current assistant state, and maps its hardware buttons to LVA commands. It also optionally integrates with PipeWire for volume control and hardware mute detection.
Credit: This integration was written by @machineonamission. Source repository: machineonamission/lva-jabra-speak-410.
This board connects over USB — no GPIO, no SPI, no HAT required. It works on any Linux host.
Hardware
| Component | Details |
|---|---|
| Connection | USB (Vendor 0x0b0e) |
| Microphone | 360° omni, enumerates as USB audio |
| Speaker | Built-in 360° speaker |
| LED ring | Integrated ring (state-driven via USB HID) |
| Buttons | Call / hang-up, mute, volume up, volume down |
LED states
The Jabra Speak 410 has an internal telephony state machine that maps LED patterns to button behaviour. The controller uses this to mirror the assistant state:
| LVA state | LED pattern | Description |
|---|---|---|
| Idle | Off | Default state |
| Wake word detected | Flashing | Ring flashes |
| Listening | 3 green | Ring solid green |
| Thinking | Flashing | Ring flashes |
| TTS speaking | Partial flash | Ring partially flashing |
| TTS finished | Off | Returns to default |
| Muted | All red | Entire ring solid red |
| Pipeline error | Red flash (×4) | Ring rapidly flashes red then returns to off |
| Timer ringing | Flashing | Ring flashes |
| No HA connection | Flashing | Ring flashes |
Note: The Jabra Speak 410 is a telephony device with a fixed firmware state machine. The LED patterns are constrained by its HID protocol — custom per-pixel colours are not possible.
Button behaviour
Call button
Context action — starts listening when idle; dismissed by hang-up gesture during active pipeline.
Hang-up / flash button
| Current state | Command sent |
|---|---|
| Timer ringing | stop_timer_ringing |
| Wake word / listening / thinking / TTS speaking | stop_pipeline |
| Music / media playing | stop_media_player |
Volume Up/Down buttons
Controlled by VOLUME_CONTROL setting (see configuration). The controller can either adjust the PipeWire sink volume directly, or send volume_up / volume_down commands to LVA.
Mute button
Togges microphone mute. Sends mute_mic when unmuted, unmute_mic when muted.
Mute button caveat: The Jabra Speak 410 does not expose the mute button state over HID when not in an active call. The controller detects mute by reading the raw audio stream from PipeWire — all-zero audio indicates hardware mute has engaged. This requires PipeWire to be exposed to the container (see installation).
Installation
Step 1 — Add udev rule
Run once on the host:
sudo tee /etc/udev/rules.d/99-jabra.rules << 'EOF'
SUBSYSTEM=="usb", ATTR{idVendor}=="0b0e", MODE="0666", GROUP="plugdev"
EOF
sudo udevadm control --reload-rules
sudo udevadm trigger
Verify the device is visible:
lsusb | grep 0b0e
# Example: Bus 001 Device 007: ID 0b0e:0412 GN Netcom ...
Step 2 — Expose PipeWire to the container
The mute button detection requires PipeWire. Follow the LVA audio server documentation to expose the PipeWire socket.
Find your runtime directory:
echo $XDG_RUNTIME_DIR
# Typically: /run/user/1000
Update the volume mounts in docker-compose.yml if your path is different from /run/user/1000.
Step 3 — Configure LVA to use the Jabra microphone
The Jabra Speak 410 enumerates as a standard USB audio device. Pass the device to LVA:
--audio-input-device "Jabra SPEAK 410"
Or in LVA's compose environment:
environment:
- AUDIO_INPUT_DEVICE=Jabra SPEAK 410
Step 4 — File structure
Jabra Speak 410/
├── docker-compose.yml
└── (no build required — uses pre-built image)
Step 5 — Start
cd jabra_speak_410
Option A — Run with Docker Compose (recommended)
docker compose up -d
Check logs:
docker compose logs -f
Option B — Run directly with Python
pip install hidapi websocket-client pycaw pycaw[pipewire]
export LVA_WS_URL=ws://localhost:6055
export XDG_RUNTIME_DIR=/run/user/1000
export PULSE_SERVER=unix:/run/user/1000/pulse/native
python main.py
Configuration
All options are set via environment variables in docker-compose.yml:
| Variable | Default | Description |
|---|---|---|
LVA_WS_URL |
ws://localhost:6055 |
LVA peripheral API WebSocket URL |
XDG_RUNTIME_DIR |
/run/user/1000 |
PipeWire runtime directory (must match volume mount) |
PULSE_SERVER |
unix:/run/user/1000/pulse/native |
PulseAudio / PipeWire-Pulse socket path |
PW_SINK |
@DEFAULT_SINK@ |
PipeWire sink for volume control |
VOLUME_CONTROL |
(unset) | Volume button mode — see below |
Volume control modes
| Value | Behaviour |
|---|---|
pipewire |
Adjusts PipeWire sink volume directly via wpctl on each button press |
lva |
Sends volume_up / volume_down commands to LVA |
| (unset) | Volume button presses are ignored |
Warning: Do not use
VOLUME_CONTROL=lvaif your host OS is also processing HID volume events from the Jabra — this will result in double volume changes.
Drivers summary
| Component | Driver needed | How |
|---|---|---|
| Jabra HID (LEDs + buttons) | udev rule | 99-jabra.rules on host |
| Microphone (USB audio) | None | Enumerates as USB audio device automatically |
| Mute detection | PipeWire socket | Expose $XDG_RUNTIME_DIR to container |
Known limitations
Mute button detection requires PipeWire to be exposed to the container. Without it, the mute button will not work. This is a firmware limitation of the Jabra Speak 410 — the device does not report the mute button state over HID unless it believes it is in an active call.
Volume button exclusivity — hidapi takes full ownership of the USB HID interface. External tools that rely on HID volume events (such as alsa_volume_from_usb_hid) will not function while this container is running.
LED pattern constraints — The Jabra Speak 410 firmware enforces a telephony state machine. LED patterns are limited to what the device's HID protocol supports; custom animations or per-pixel colour control are not possible.
Troubleshooting
Device not found at startup
- Confirm
lsusb | grep 0b0eshows the device. - Check the udev rule is applied:
ls -la /dev/bus/usb/<bus>/<device>should show groupplugdevwith mode0666. - Restart the container after plugging in the device.
Mute button has no effect
- Confirm the PipeWire socket volume mount is correct and the path exists on the host:
ls $XDG_RUNTIME_DIR/pipewire-0. - Run with debug logging and look for
pw-record not found— this meanswireplumberis not installed in the container. The pre-built image includes it. - Check that
XDG_RUNTIME_DIRin the container environment matches the volume mount path.
Double volume changes
Set VOLUME_CONTROL to pipewire instead of lva, or leave it unset. The lva mode conflicts with OS-level HID volume processing.
LVA not reachable
- Confirm LVA is running and port 6055 is open:
nc -zv localhost 6055. - With
network_mode: host,localhostresolves to the host itself. - If LVA is on a different machine, change
LVA_WS_URLto its IP address.