- Rust 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| contrib | ||
| src | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| README.md | ||
siriremote
A small Linux client for Apple TV Siri Remotes, written in Rust. BlueZ owns pairing and the BLE connection; this program only subscribes to GATT reports and optionally publishes a virtual keyboard and mouse.
This design does not invoke bluetoothctl, open raw HCI sockets, compile a
version-specific BlueZ helper, or disconnect a connection owned by BlueZ.
Supported hardware
The button and touch decoder supports the 3rd-generation Siri Remote
(004c:0315). Its event layout is vendor-specific; earlier generations are
not selected automatically.
Build
On Arch Linux:
sudo pacman -S --needed base-devel bluez bluez-utils dbus
cargo build --release
The binary is target/release/siriremote.
One-time BlueZ setup
BlueZ's hog plugin normally claims BLE HID reports for the kernel. The Siri
Remote uses vendor-defined reports that the kernel does not translate to input
events, and it requires an enable write that the plugin denies to applications.
Disable only that plugin:
sudo mkdir -p /etc/systemd/system/bluetooth.service.d
sudo cp contrib/bluetooth.service.override.conf \
/etc/systemd/system/bluetooth.service.d/siriremote.conf
sudo systemctl daemon-reload
sudo systemctl restart bluetooth.service
This affects other Bluetooth Low Energy keyboards and mice using HID-over-GATT. Classic Bluetooth input devices are unaffected. Remove the override when it is no longer wanted:
sudo rm /etc/systemd/system/bluetooth.service.d/siriremote.conf
sudo systemctl daemon-reload
sudo systemctl restart bluetooth.service
Pair and trust the remote once through BlueZ:
bluetoothctl
power on
agent on
default-agent
scan on
# Hold MENU/BACK + Volume Up on the remote, then use its displayed address:
pair AA:BB:CC:DD:EE:FF
trust AA:BB:CC:DD:EE:FF
quit
Usage
Print decoded events:
./target/release/siriremote events
Use a specific paired remote:
./target/release/siriremote --address AA:BB:CC:DD:EE:FF events
Create a virtual keyboard and mouse:
sudo ./target/release/siriremote control
control maps media, volume, navigation, back, home, and screen-lock buttons
to Linux input keys. Select sends Enter in D-pad mode, which works naturally in
TV interfaces such as Kodi; it sends a left mouse click when touchpad mouse
control is enabled. Touchpad mouse control starts disabled so touch events do
not interfere with D-pad navigation.
HDMI-CEC TV power
With a Linux CEC device, the remote's Power button controls the TV over HDMI-
CEC instead of emitting Linux KEY_COFFEE. This is useful for LG TVs, which
may not answer power-status requests while in standby.
For a Pulse-Eight USB-CEC Adapter, install v4l-utils and
linuxconsoletools, then install the supplied udev rule and inputattach unit:
sudo install -m 0644 contrib/70-pulse8-cec.rules /etc/udev/rules.d/
sudo install -m 0644 contrib/pulse8-cec-inputattach@.service /etc/systemd/system/
sudo install -m 0644 contrib/pulse8-cec.conf /etc/modprobe.d/
sudo systemctl daemon-reload
sudo udevadm control --reload
# Replug the adapter, or replace ttyACM0 with its actual device name:
sudo udevadm trigger --action=add --subsystem-match=tty --sysname-match=ttyACM0
The unit owns the adapter's /dev/ttyACM* serial node and creates a native
/dev/cec0 device. Do not run cec-client while that unit is active. Confirm
the new device with cec-ctl -A. Configure the physical address for the TV
port used by the adapter; for HDMI 2 this is 2.0.0.0:
sudo cec-ctl -d /dev/cec0 -p 2.0.0.0 --playback -o 'Siri Remote'
Start control mode with --cec-device /dev/cec0, or install the supplied
systemd drop-in to set it for the service:
sudo install -D -m 0644 contrib/siriremote-cec.conf \
/etc/systemd/system/siriremote@AA:BB:CC:DD:EE:FF.service.d/cec.conf
sudo systemctl daemon-reload
sudo systemctl restart 'siriremote@AA:BB:CC:DD:EE:FF.service'
The program shares the adapter's existing logical address with Kodi and never reconfigures it, so Kodi's CEC settings remain in control.
Some LG models acknowledge CEC polls but do not report their power state. For
those TVs, siriremote alternates broadcast Standby and the legacy Power key,
starting with Standby when the service starts. If the service restarts while
the TV is already off, press Power once to resynchronize it; the next press
wakes the TV.
MQTT TV power
When HDMI-CEC is configured, control mode can also accept TV power commands
over MQTT. The topic and payload conventions are compatible with ScreenMQTT:
<hostname>/backlight/commandacceptson,off,standby, andsuspend.wake,sleep,enabled, anddisabledare accepted as aliases.<hostname>/backlight/stateretainsunknown,on, oroffat QoS 1.
MQTT is opt-in. Set MQTT_HOST in the same environment used by the service;
MQTT_PORT defaults to 1883:
MQTT_HOST=mqtt.example.net
MQTT_PORT=1883
The service publishes unknown after startup because TVs that do not answer
CEC power-status requests cannot be queried reliably. A successful remote or
MQTT power command updates the retained state. Repeated MQTT commands for the
same known state are suppressed so QoS 1 redelivery does not toggle the TV a
second time.
Optional settings are MQTT_TOPIC_PREFIX (default
<hostname>/backlight), MQTT_CLIENT_ID (default
siriremote-<hostname>), MQTT_USERNAME, and MQTT_PASSWORD. A password
requires a username. Broker connection failures are retried in the background
and do not interrupt Bluetooth remote input.
For the systemd service, add the variables to /etc/siriremote.env and restart
the instance. For example:
sudoedit /etc/siriremote.env
sudo systemctl restart 'siriremote@AA:BB:CC:DD:EE:FF.service'
Publish a wake command with any MQTT client:
mosquitto_pub -h mqtt.example.net -q 1 \
-t "$(hostname)/backlight/command" -m on
Hold the Siri button for at least 500 ms to record from the remote microphone.
On release, the audio is decoded locally, sent to OpenAI's whisper-1 through
the Rust async-openai crate, and the transcription is typed into the focused
application through the virtual keyboard. Whisper is prompted with example
search queries; the result is lowercased and stripped of trailing sentence
punctuation before it is typed. Set the key in the environment; do not put it
in a command line or source file:
export OPENAI_API_KEY='...'
sudo --preserve-env=OPENAI_API_KEY ./target/release/siriremote control
Audio exists only in memory while recording and is sent as an in-memory WAV upload. Text injection supports the US keyboard layout's ASCII characters; other characters are skipped and reported on stderr. The Siri press itself is not forwarded as an input event.
A Siri press shorter than 500 ms toggles touchpad mouse control, regardless of
whether the remote emitted any microphone frames. When enabled, a one-finger
swipe moves the mouse. --sensitivity controls the unaccelerated base speed and
defaults to 12; lower values move faster.
Only /dev/uinput requires elevated access; the BlueZ event mode normally runs
as the logged-in user.
The program reconnects when the remote wakes and deliberately leaves the BlueZ connection alone when it exits.
systemd service
For an always-on installation, install the binary and templated service:
sudo install -m 0755 target/release/siriremote /usr/local/bin/siriremote
sudo install -m 0644 contrib/siriremote@.service \
/etc/systemd/system/siriremote@.service
printf '%s\n' uinput | sudo install -m 0644 /dev/stdin \
/etc/modules-load.d/siriremote.conf
sudo modprobe uinput
Store the OpenAI key in a root-only environment file:
sudo touch /etc/siriremote.env
sudo chown root:root /etc/siriremote.env
sudo chmod 0600 /etc/siriremote.env
sudoedit /etc/siriremote.env
# Add: OPENAI_API_KEY=...
The service instance is the paired remote's Bluetooth address. Enable it after installing the BlueZ override described above:
sudo systemctl daemon-reload
sudo systemctl enable --now 'siriremote@AA:BB:CC:DD:EE:FF.service'
The service runs as root because control needs access to /dev/uinput.
KillSignal=SIGINT lets the program release held virtual keys during a clean
service stop.
Protocol attribution
Apple does not publish the vendor report layouts. The input-enable byte and 3rd-generation button/touch report formats were independently reverse- engineered by the GPL-3.0 project azais-corentin/siri-remote. This implementation uses those protocol facts but has its own transport, structure, and decoding code; no Python source was used.