henaxxx/a2854-siri-remote-linux

GitHub: henaxxx/a2854-siri-remote-linux

Stars: 2 | Forks: 0

# a2854-siri-remote-linux ![Apple Siri Remote (A2854, 3rd gen) under a ceiling spotlight controlled by this project](https://static.pigsec.cn/wp-content/uploads/repos/2026/05/eff3b6df9a011049.jpg) A Python bridge that uses the **Apple Siri Remote (3rd generation, model A2854)** on Linux — buttons, d-pad, power button, and the Siri voice stream — and wires it into a small smart-home rig (TP-Link Tapo plugs, an Apple HomePod via AirPlay, push notifications via `ntfy`, an optional LLM voice agent via Groq). ## Prior art (read this first) If all you want is a clean, well-engineered A2854 driver on Linux, **use [azais-corentin/siri-remote](https://github.com/azais-corentin/siri-remote)**. It is a Rust implementation that, as of May 2026, already covers: - A2854 buttons, - the touchpad with full X/Y/pressure/multi-finger/rotation/hover, - the Siri microphone as a PipeWire `Audio/Source`, - pairing including the BLE RPA churn. I only became aware of that project after publishing this one. The core "make a 3rd-gen Siri Remote work on Linux, voice included" claim belongs to it, not to this repo. Earlier work that this project also builds on: - **[Yanndroid/SiriRemote-Linux](https://github.com/Yanndroid/SiriRemote-Linux)** — established the basic shape of Linux button/touchpad input for earlier Siri Remote generations, and documented the BlueZ 20-byte HIDP limit that blocked audio at the time. - **[Jack-R1/SiriRemoteVoiceControl](https://github.com/Jack-R1/SiriRemoteVoiceControl)** — voice decode on **macOS** for **gen-2 / gen-4** remotes via PacketLogger; the "length-prefixed Opus inside a fixed-length notification" shape was already known there. ## What this repo still adds Given the above, the things that may still be useful here are: - **A different read path.** This bridge does not use BlueZ's GATT D-Bus API at all on the read side; it reads ATT `Handle Value Notification` frames directly out of `btmon` over a PTY. That works around the BlueZ HoG plugin monopolizing the HID service, without having to talk D-Bus or write a kernel module. - **A Python reference**, in case Rust is not your stack. - **An end-to-end smart-home glue**: Tapo plug control via the `tapo` SDK, HomePod control via a persistent in-process `pyatv` client, voice through Groq Whisper + LLM + ntfy, all running under systemd with auto-reconnect and a kiosk dashboard. - **A written-out timeline**, including all the failed approaches. See [TIMELINE.md](TIMELINE.md). ## A2854 button hex map All values observed at GATT handle `0x003a`, 2-byte notification followed by `0000` on release. | Button | Press value | |---|---| | Back | `4000` | | TV | `0100` | | Play/Pause | `0001` | | Volume Up | `0200` | | Volume Down | `0400` | | Mute | `8000` | | Power | `1000` | | D-pad Center | `0800` | | D-pad Up | `0002` | | D-pad Down | `0008` | | D-pad Right | `0004` | | D-pad Left | (untested — physically broken on the unit used) | | Siri (long-press) | sends audio frames on handle `0x0036` | ## gen-3 audio wrapper format Each notification on handle `0x0036` is exactly 100 bytes: offset size meaning 0 2 stream marker / session id (e.g. 1a 5c, 95 04, 00 00) 2 2 sequence number, little-endian 4 1 opus packet length in bytes (N) 5 N opus packet, starting with TOC byte 0xB8 5+N ... zero padding up to 100 bytes `0xB8` decodes (per RFC 6716, Table 2) as: - config 23 → **CELT-only, wideband, 20 ms** frames - mono - 1 frame per packet Opus output is canonically 48 kHz; the WB (wideband) bandwidth flag limits the actual audio content to roughly 8 kHz. The included [`decode_siri_voice.py`](decode_siri_voice.py) currently asks the decoder for a 16 kHz output rate, which is enough to recover WB content. azais-corentin's `siri-remote` exposes the full 48 kHz output through PipeWire if you want it untouched. This differs from the gen-2/4 wrapper described by Jack-R1 (which starts with `1B 23 00 00 10`), but the *idea* of length-prefixed Opus inside a fixed-length notification is the same. See [`decode_siri_voice.py`](decode_siri_voice.py) for a reference decoder (btmon snoop → WAV). ## Architecture (current rig) A2854 Siri Remote │ BLE GATT notifications ▼ Linux mini-PC (Ubuntu 24.04, BlueZ 5.72) │ ├─ btmon ── pty.fork() ── notification parser ──┬─ button → Tapo P105 / HomePod │ └─ audio → Opus decode → WAV │ │ │ ▼ │ Groq Whisper → Groq LLM → ntfy │ └─ FastAPI dashboard on :8080 (kiosk display) The `pty.fork()` around `btmon` exists because: 1. The BlueZ HoG plugin attaches itself to the HID service of an A2854 and refuses to publish the characteristics over the generic GATT D-Bus API. `bleak`, `busctl` and friends can therefore not subscribe to button or audio notifications. 2. Reading HCI traffic via `btmon` works, but `subprocess.Popen` will block-buffer its stdout. `stdbuf` does not help because `sudo` strips `LD_PRELOAD`. 3. Spawning `btmon` under a PTY restores line-streaming and keeps the parser responsive. ## Hardware tested - Apple Siri Remote 3rd gen (model **A2854**, used unit, left button physically broken) - Mini-PC with Intel chipset Bluetooth `8087:0a2a` (BT 4.2, **no** dongle required after tuning `MinConnectionInterval` and USB autosuspend) - Apple HomePod mini, pairing status "NotNeeded" — controlled via `pyatv` - TP-Link Tapo P105 (×2) — controlled locally via the `tapo` Python SDK ## Quick start sudo apt install -y bluez libopus0 ffmpeg python3-venv git clone https://github.com//a2854-siri-remote-linux.git cd a2854-siri-remote-linux python3 -m venv .venv . .venv/bin/activate pip install -e . cp .env.example .env $EDITOR .env Pair the remote (hold it close to the adapter): a2854-pair --pair --seconds 60 --min-rssi -50 Allow `btmon` without a password (the bridge needs raw HCI access): # /etc/sudoers.d/btmon-youruser youruser ALL=(ALL) NOPASSWD: /usr/bin/btmon Run the bridge: a2854-bridge Or install the systemd units from `systemd/*.example` after editing the paths. ## File layout | File | Purpose | |---|---| | `a2854_bridge.py` | Main bridge: button parsing, voice path, controllers | | `a2854_pair.py` | BLE detection + pairing helper for A2854 | | `decode_siri_voice.py` | Standalone btmon snoop → WAV decoder | | `bt_observe.py` | Connection-state observer | | `capture_buttons.sh` | btmon capture helper for new-button identification | | `tapo_test.py` | Tapo CLI sanity check | | `whisper_test.py`, `whisper_ntfy_test.py` | Voice pipeline checks | | `dashboard.py` | FastAPI status dashboard on `:8080` | | `systemd/*.example` | service unit templates | | `udev/*.example` | USB autosuspend rule for the Intel BT chipset | ## Acknowledgements - [azais-corentin/siri-remote](https://github.com/azais-corentin/siri-remote) — the more complete A2854 implementation, in Rust. See [Prior art](#prior-art-read-this-first). - [Yanndroid/SiriRemote-Linux](https://github.com/Yanndroid/SiriRemote-Linux) — established that the Linux side is reachable at all on earlier generations. - [Jack-R1/SiriRemoteVoiceControl](https://github.com/Jack-R1/SiriRemoteVoiceControl) — established the length-prefixed-Opus shape on macOS for gen-2 / gen-4. - [`pyatv`](https://github.com/postlund/pyatv), [`python-tapo`](https://github.com/mihai-dinculescu/tapo), [`opuslib`](https://github.com/orion-labs/opuslib), [BlueZ](http://www.bluez.org/), and the `btmon` authors. ## License [MIT](LICENSE).