Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-07-18 18:08:01 -07:00
contrib Add HDMI-CEC TV power control 2026-07-12 19:01:48 -07:00
src adds mqtt support 2026-07-18 18:08:01 -07:00
.gitignore Add Siri hold-to-dictate input 2026-07-12 17:34:37 -07:00
Cargo.lock adds mqtt support 2026-07-18 18:08:01 -07:00
Cargo.toml adds mqtt support 2026-07-18 18:08:01 -07:00
README.md adds mqtt support 2026-07-18 18:08:01 -07:00

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/command accepts on, off, standby, and suspend.
  • wake, sleep, enabled, and disabled are accepted as aliases.
  • <hostname>/backlight/state retains unknown, on, or off at 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.