Skip to content

Releases: batubozkan/pods-head-tracker

v0.5.5 — Roll axis + ear auto-pause

Choose a tag to compare

@batubozkan batubozkan released this 20 Aug 21:36

Everything since v0.4, confirmed on real hardware (Pi Zero 2 W + AirPods 4 ANC).

Roll axis (ROLL=1)

The bridge can now send roll (head tilt, ear to shoulder) together with yaw and pitch. The formula comes from per-motion CSV captures on AirPods 4: the packet field o1 contains pitch+roll, the pitch pair (o2+o3)/2 contains pitch−roll, and half of their difference is the roll angle. Measured quality: 0.2° of noise with a still head, 3–4° of unwanted roll during pure yaw/pitch movement, a correct ±40° response to tilts, and no drift. Off by default; on other AirPods models, verify the mapping once with the probe protocol in the README.

Ear-detection auto-pause

  • While no bud is in an ear, the bridge holds the last pose — pulling the buds out is itself head motion, and that garbage no longer reaches the game. Status shows paused - buds out of ear.
  • When the buds return, the neutral pose is re-captured automatically (a re-seated bud never sits exactly where it was). Face forward as you put them in.
  • Opt out with EAR_PAUSE=0 / EAR_RECENTER=0.

Setup and daily-use polish

  • install.sh suggests your AirPods MAC from the Pi's bonded devices when the config still holds the placeholder (suggestion only — it never writes the address).
  • install.sh now installs airpods_ht_probe.py too, and gained --uninstall [--purge].
  • The bridge gained --config FILE for manual runs outside systemd. Precedence: CLI > environment > file.
  • HT_START=both now sticks to the winning START variant for the whole process (reconnects no longer alternate) and logs a hint: line with the value to persist.
  • The probe gained --csv PATH / --label for per-motion sensor recordings.

Tests and CI

  • 49 → 102 tests. The main loop, connect_l2cap(), and the CLI/config validation are now covered by a fake-socket/fake-clock harness with zero real sleeps.
  • CI runs on Python 3.9 / 3.11 / 3.13 — the Pythons of Raspberry Pi OS Bullseye, Bookworm, and Trixie.

Upgrade

scp *.py *.sh *.service *.conf.example pi@<PI_HOST>:~/
ssh pi@<PI_HOST> 'sudo ./install.sh'

Your configuration file is never overwritten. To enable roll, add ROLL=1 and restart the service.

Full changelog: v0.4...v0.5.5

v0.4 — Selectable start packet (HT_START)

Choose a tag to compare

@batubozkan batubozkan released this 19 Aug 14:15

One config line for AirPods that connect fine but never stream: the new HT_START option lets the bridge send the other head-tracking start packet — or alternate between both until data arrives.

New

  • HT_START=alt|def|both (config file or --ht-start): selects which START packet begins the sensor stream. alt is the previous hard-coded behavior and stays the default — nothing changes for working setups. def sends the LibrePods packets.h variant instead. both alternates between the two on every dry re-send until data arrives.
  • Self-diagnosing journal lines: each re-send now names the variant (no data - re-sent start DEF (buds in-ear?)), and when the stream finally starts in both mode the bridge logs stream started after start DEF — telling you exactly which packet your model answered, so you can pin HT_START to it permanently.
  • README gained a Cautions entry for the symptom ("no data with the buds in your ears") with the HT_START=both recovery procedure.

Why this exists

Issue #2 showed a setup where everything on the wire was healthy — L2CAP channel open, battery reports decoding, ear detection working — yet the sensor stream never started, no matter how often the bridge re-sent its start command.

The AACP protocol (reverse-engineered, via LibrePods) has two known start packets, and which one actually begins the stream depends on the AirPods model/firmware. The bridge hard-coded START_ALT because that is the variant confirmed on the tested hardware (AirPods 4 ANC); START_DEF — the LibrePods default — was only reachable through the diagnostic probe. Any model that only answers DEF was therefore permanently silent in the bridge while looking perfectly connected.

Rather than swap one hard-coded packet for another, the start command is now configurable, with a both mode that doubles as its own diagnostic: run it once, read which variant your buds answered in the journal, pin it. Two constraints shaped the design:

  • ALT always goes first in both mode. Probing showed that DEF sent before ALT makes the buds emit a different packet family that contaminates the ALT stream — so the alternation order is a protocol requirement, not a style choice.
  • The default stays alt. Existing installations see zero behavior change; both is opt-in for exactly the "connected but silent" symptom.

Upgrading from v0.3

scp the new files to the Pi (README step 2), then sudo ./install.sh. No config changes needed — HT_START defaults to the previous behavior. Only set HT_START=both if the journal shows no data - re-sent start continuously while the buds are in your ears.

v0.3 — Re-centering + battery

Choose a tag to compare

@batubozkan batubozkan released this 25 Jul 13:28

Re-centering without touching SSH — the bridge now re-centers itself every time you press Start in OpenTrack — and the AirPods' battery is finally visible on the PC side: in the journal, and one systemctl status away.

New

  • Auto-recenter on OpenTrack Start (serial transport): the bridge notices the COM port being read again after a stop of ≥ 3 s and re-captures the neutral pose. Sit straight, press Start, done — no token, no configuration. The 3 s floor keeps a brief PC stall from re-centering you mid-race; RECENTER_ON_START=0 turns it off.
  • Re-center any time, on any transport: sudo systemctl kill --signal=SIGUSR1 pods-head-tracker re-centers without a restart. On serial, OpenTrack's Hatire dialog can also send a RECENTER_CMD token (default RECENTER) — put it in the Reset command field and the dialog's Reset button re-centers the bridge mid-session. (OpenTrack's Center hotkey never reaches the device — its serial center/zero commands are dead code upstream — so the hotkey stays PC-side software centering.)
  • Battery reporting: the bridge decodes the battery notifications the AirPods push over the same connection — a journal line on every change plus a heartbeat every 15 minutes (BATTERY_LOG_SECS), and a low-battery warning at 20 % (BATTERY_WARN_PCT) that repeats at 15/10/5 %.
  • Live status: systemctl status pods-head-tracker now shows the bridge state at a glance, e.g. Status: "streaming | battery L 82% R 80%".
  • Ear detection: in-ear transitions are logged, and the stream-start command goes quiet (30 s cadence) while the buds are known to be out of ear.

Fixed

  • The stream-start watchdog counted receive timeouts, so any non-tracking traffic on the channel could keep it from ever re-sending the start command. It now keys on head-tracking silence directly.

Upgrading from v0.2

scp the new files to the Pi (README step 2), then sudo ./install.sh. No config changes needed — the new knobs (RECENTER_ON_START, RECENTER_CMD, BATTERY_LOG_SECS, BATTERY_WARN_PCT) all default sensibly. The battery/ear packet formats come from LibrePods, like the rest of the AACP protocol knowledge.

v0.2 — USB audio

v0.2 — USB audio Pre-release
Pre-release

Choose a tag to compare

@batubozkan batubozkan released this 23 Jul 22:25

The Pi now doubles as a USB sound card: Windows game audio travels over the same USB cable as the head-tracking data and comes out of the same AirPods — audio and tracking together, one cable, one pair of buds.

New

  • USB audio (optional): setup-usb-audio.sh replaces the g_serial gadget with a composite one — serial + UAC2 sound card, built at boot by pods-usb-gadget.sh/.service — and pods-usb-audio.sh/.service forwards the audio to the AirPods over Bluetooth A2DP (BlueALSA, SBC, 48 kHz). Nothing to install on Windows: the Pi shows up as a normal USB playback device next to the usual COM port.
  • AUDIO_ENABLE=0 in the config turns audio off while keeping head tracking. Never running setup-usb-audio.sh keeps the plain serial-only setup — nothing changes for tracking-only users.
  • AUDIO_LATENCY_MS tunes the audio loop buffer (default 50; 20 tested clean on a Pi Zero 2 W with tracking active). AUDIO_VOLUME sets the buds' Bluetooth volume ceiling — day-to-day loudness is the normal Windows volume slider.

Fixed

  • Wi-Fi/SSH starvation: every Bluetooth connect attempt to in-case AirPods monopolizes the Pi's shared 2.4 GHz radio for ~5 seconds, and the old 1.5-second retry loop could kill Wi-Fi outright. The bridge now backs off 10 s when the buds don't answer, and the audio forwarder never pages at all — it waits for the buds to appear on the link (they reconnect on their own when leaving the case).

Upgrading from v0.1

scp the new files to the Pi (README step 2), then sudo ./setup-usb-audio.sh and reboot. The COM port number changes once (the composite gadget is a new USB device) — re-select it in OpenTrack's Hatire settings. Serial-only setups need no changes at all.

Expect normal Bluetooth-audio latency (~150–300 ms end to end): great for immersion and music, not for competitive audio cues. See the README's "USB audio" section for setup, routing, and troubleshooting.

First Public Release

First Public Release Pre-release
Pre-release

Choose a tag to compare

@batubozkan batubozkan released this 23 Jul 12:48

Turns AirPods into a head tracker for OpenTrack,using a Raspberry Pi as the Bluetooth bridge — built for sim racing (iRacing etc.),works with anything OpenTrack feeds.

Highlights

  • AirPods head-tracking over Bluetooth - the Pi opens the AirPods' private AACP L2CAP channel via BlueZ and decodes the orientation stream (protocol derived from LibrePods)
  • Two transports to OpenTrack:
    • Wi-Fi UDP - OpenTrack's "FreePIE UDP receiver" input, zero cables
    • USB serial - the Pi becomes a USB serial gadget (a plain COM port onWindows) speaking the "Hatire Arduino" protocol. No network in the path,no dropouts; one-shot setup-usb-serial.sh does the Pi-side setup
    • TRANSPORT=both streams over both at once.
  • Boot-and-forget - systemd service, auto-reconnects, re-centers on restart.
    All personal settings live in /etc/pods-head-tracker.conf; the unit is generic.
  • Zero dependencies - Python 3 standard library only.

Tested

Pi Zero 2 W (Raspberry Pi OS, BlueZ 5.82) + AirPods 4 (ANC) + OpenTrack onWindows 11, both transports. Should work with every AirPods model after AirPods (2nd gen) and on any Linux/BlueZ machine - reports welcome.

Known limitations

  • Yaw + pitch only - roll isn't derivable from the sensor data and is sent as 0.
  • One pair of AirPods per bridge.

Full setup guide in the README