henaxxx/a2854-siri-remote-linux
GitHub: henaxxx/a2854-siri-remote-linux
Stars: 2 | Forks: 0
# a2854-siri-remote-linux

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).