Skip to content

Add native Linux support (CLI + GTK app) - #22

Open
melcodesdev wants to merge 19 commits into
NicholasSlattery:linux-supportfrom
melcodesdev:main
Open

Add native Linux support (CLI + GTK app)#22
melcodesdev wants to merge 19 commits into
NicholasSlattery:linux-supportfrom
melcodesdev:main

Conversation

@melcodesdev

@melcodesdev melcodesdev commented Aug 14, 2026

Copy link
Copy Markdown

Native Linux support (CLI + GTK app)

Adds Linux alongside Windows and macOS. Platform code lives in src/linux/,
mirroring src/macos/, and reuses the shared tracking core and the POSIX UDP
transport. No Windows or macOS source file is deleted, and neither build changes
behaviour.

Exactly what this touches outside src/linux/, so you can judge it quickly:

File Change Effect on Windows and macOS
include/sony_head_tracker/types.hpp +12, new FilterConfig fields None. outputSource{0,1,2}, outputSign{1,1,1}, levelOutput{false} are identity, so the filter behaves exactly as before unless a user opts in.
src/orientation.cpp +13 / -1 None by default. Applies the fields above; with the identity defaults the result is unchanged.
CMakeLists.txt +30 None. A new if(LINUX) block only, mirroring the existing if(APPLE) one.
README.md +59 / -0 Adds a ### Linux subsection under Quick start and under Build, third, in the order the platforms arrived, exactly mirroring how macOS was added. Nothing existing is reordered, reworded or removed.
docs/PROTOCOL.md, .gitignore, CHANGELOG.md small Documentation and ignore rules only.

I verified PR #19's added maths tests (pitch/roll coverage) still pass against
these changes: 11/11.

Opening this against linux-support as you asked in #9. Thanks for bringing the
branch up to date with main, that made it a clean fast-forward.

Apologies for the long gap between saying this was ready and actually opening it.
I was away on holiday. In the meantime two people in #9 tried the fork and
reported back, which is reflected in the testing notes below.

What it adds

  • CLI bridge (probe / dump / bridge): a hidraw backend and a HID report
    descriptor parser (the Linux stand-in for HidP_* / IOKit element parsing),
    emitting the same OpenTrack UDP and JSON wire formats as the other platforms.
  • GTK4 / libadwaita app: live attitude indicator, guided axis calibration,
    recenter, a desktop-aware global recenter shortcut, per-game OpenTrack setup for
    Steam and Proton, and one-click rootless device access via a udev rule.
  • Automatic Bluetooth HID connect. Sony headsets often connect audio only, so
    no /dev/hidraw* node appears and the tracker looks absent. The app asks BlueZ
    to attach the HID profile and rescans.
  • Optional level compensation that stops looking up and down from bleeding
    into roll. Off by default, so nothing changes unless it is enabled.
  • Build: a CMake if(LINUX) block mirroring the macOS one, plus a
    dependency-free Makefile with a DESTDIR/PREFIX aware install, and Linux CI.
  • Packaging (packaging/): AUR PKGBUILD (makepkg verified), an RPM spec (live
    on Fedora COPR), Debian packaging, and an AppImage builder. Happy to drop these
    if you would rather own distribution yourself.
  • Docs: docs/LINUX.md, docs/ARCHITECTURE.md, PORTING.md, and Linux
    sections in the README.

Portability work

Audited against the distributions Linux gamers actually run (Steam, July 2026):
SteamOS 22.2%, CachyOS 14.3%, Arch 8.3%, Mint 8.1%, Bazzite 7.7%.

  • make portable links libstdc++ statically. SteamOS 3.8 caps at GLIBCXX_3.4.34
    and exactly three std::format symbols exceed it, so without this the binary
    will not load on a Steam Deck. The default build is unchanged and still links
    dynamically, which is what distribution packages want.
  • The GUI checks for libadwaita 1.4 and for a missing typelib up front and reports
    it in a window, because the desktop entry is Terminal=false and stderr is never
    seen by someone launching from a menu.
  • aarch64 was only ever blocked by packaging metadata, not by the code.

Tested

Validated on a WH-1000XM5, including Assetto Corsa Competizione through Proton.
Two other people in #9 have confirmed the port on their own hardware and
distributions (XM-6 earbuds on a Debian base, and FlightGear). The Linux test
suite passes and the shared core is untouched.

Happy to split this up, drop the packaging, or rework anything that does not fit
how you want the project to go.

melcodesdev and others added 19 commits July 12, 2026 10:01
A command-line bridge and a GTK/libadwaita desktop app for Linux, added
alongside the Windows and macOS builds without changing them.

- Platform layer in src/linux/ (mirrors src/macos/): a hidraw backend, a HID
  report-descriptor parser standing in for Windows HidP_* / macOS IOKit element
  parsing, and the logger/string shims. Reuses the shared core and the POSIX UDP
  transport (src/macos/output_udp_posix.cpp).
- CMake if(LINUX) target plus a dependency-free Makefile; a Linux CI workflow.
- Final Euler output correction (outputSource/outputSign in FilterConfig, applied
  in orientation.cpp) driven live by the GUI's Calibrate wizard; identity by
  default so Windows/macOS behaviour is unchanged.
- udev rule for rootless hidraw access, a desktop-aware global recenter shortcut,
  and per-game OpenTrack setup for Steam/Proton.
- Docs: docs/LINUX.md (setup and troubleshooting), docs/ARCHITECTURE.md (how the
  pipeline works), PORTING.md, and Linux sections in the README.

Validated on a WH-1000XM5, including Assetto Corsa Competizione through Proton.
Builds clean and the Linux test suite passes.
- make install (DESTDIR/PREFIX-aware) does a self-contained system install:
  the CLI, GUI, scripts, udev rule, desktop entry, icon, and bin launchers.
- GUI resolves scripts/extras from an install prefix as well as a repo checkout.
- Makefile appends -std=c++20/-Iinclude/-pthread with override, so a packager's
  exported CXXFLAGS (makepkg, dpkg-buildflags) no longer drops them.
- packaging/aur: a PKGBUILD (+ .install, .SRCINFO) building the Linux target.
  makedepends git/gcc/make; depends gtk4/libadwaita/python-gobject.
- packaging/appimage/build-appimage.sh assembles an AppImage from the system
  install via linuxdeploy + the GTK plugin, bundling Python and gi. Marked as a
  starting point (GTK4/PyGObject AppImages need iteration); a README documents
  the approach and the reliable make install fallback.
- docs/LINUX.md gains an Installing section (AUR, system install, AppImage,
  from source).
Sony headsets advertise the Bluetooth HID service that carries the head tracker,
but BlueZ often connects only audio, so no /dev/hidraw node appears and the app
looks stuck on 'not detected / grant permission'. scripts/connect-headset-hid.sh
asks BlueZ to connect the HID profile (ConnectProfile 0x1124) on a connected Sony
device. The GUI calls it automatically on a failed scan (rate-limited) and offers
it as a Reconnect headset banner button and a Reconnect head tracker menu item.
The Sony trackers report only orientation (no gyro/accel), so the headset's
mounting tilt cannot be leveled against gravity. It shows up as looking up/down
bleeding into roll (a pure head pitch splits across pitch and roll in a fixed
ratio). Emitting the recentered orientation as a world-frame relative rotation
(now * center^-1) instead of body-frame (center^-1 * now) cancels the mounting
tilt: a pure pitch stays pure. Verified in simulation (20 deg pitch -> pitch 20,
roll 0, vs roll -1.6/yaw -10 before).

Opt-in via FilterConfig.levelOutput (bridge --level, control-socket LEVEL, and a
Settings toggle), off by default so existing calibrations and the Windows/macOS
builds are unchanged. Recenter level-and-forward after enabling.
Fold in the fixes found while building the release AppImage: fetch appimagetool
and run tools extract-and-run (no FUSE), tolerate a missing gtk-4.0 modules dir,
bundle the full Python stdlib plus gi/cairo (not the host site-packages), write
the AppRun outside the AppDir, bundle the Adwaita icon theme, and clear the
plugin-forced GTK_THEME so libadwaita styles natively. Ignore build artifacts.
packaging/deb-rpm/build-packages.sh turns 'make install' into native .deb and
.rpm packages via fpm (correct per-distro deps, udev reload hook). README covers
the downloadable-files path and the hosted-repo options (Fedora COPR, openSUSE
OBS, Launchpad PPA) for real apt/dnf-by-name installs.
packaging/rpm/sony-head-tracker.spec builds the CLI + GUI from make/make install
for Fedora COPR (open signup) and openSUSE OBS, so users get a real dnf repo:
'dnf copr enable ...' then 'dnf install sony-head-tracker'. Clarify in the guide
that repo-add is one-time and official-archive inclusion is a separate process.
debian/ (control, rules, changelog, source/format, postinst) builds the CLI +
GTK app via debhelper for Debian/Ubuntu. README covers openSUSE OBS (apt repo,
multi-distro), a Launchpad PPA, and a plain local .deb. Needs g++ 13+, so target
Debian 13 / Ubuntu 24.04 and newer.
This fork is the Linux port, so the README now opens with it: a Linux
platform badge, a note on what this repository is, and a full
"Quick start (Linux)" walkthrough (install options and runtime packages,
the desktop app steps, the CLI, udev device access, OpenTrack/Proton, and
the global recenter shortcut) ahead of everything else.

Windows and macOS quick starts move under "Other platforms", the Build
section leads with Linux, and the shared sections (compatibility, probe,
orientation, protocol notes) now mention Linux first rather than reading
as Windows-only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018FbWNUH7k88GfN7JDwcmbn
…h-first-dmzh9n

docs: lead the README with the Linux walkthrough
Reported by nar3n12 (issue NicholasSlattery#9): the Grant device access button did nothing on a
Debian-based distro, and they fell back to running the app with sudo. The old
code discarded pkexec's stdout, stderr and exit code and collapsed every failure
into one dead-end sentence, so neither the user nor we could tell what went wrong.

- Send the rule to root through a quoted heredoc instead of interpolating a path
  into a root shell. That also fixes the AppImage case (root cannot read the
  squashfuse mount the rule lives on) and paths containing a quote.
- Chain the udevadm steps with || markers, so a reload failure no longer reports
  that the rule was not installed when it was.
- Guard for a missing pkexec, add a timeout, a re-entrancy guard, and a hint when
  the prompt may be behind the window.
- Classify the failure (no pkexec, no polkit agent, not authorized, cancelled,
  setuid, timeout) and always offer a Show me how dialog with a copy-and-paste
  sudo command that embeds the rule, so it works in every install layout.
- If the rule installs but access still does not appear, diagnose it: rule does
  not match this headset (offers to add a matching rule), not in the input group,
  session not active on the seat, udev did not reload, or reconnect needed.
- Keep the failure banner on screen instead of letting the idle rescan erase it.
From a 12-family distro audit (Debian/Arch/RPM/SUSE/musl/non-systemd/NixOS/
immutable/SteamOS/legacy/arch-axes/desktops). Highest reach per effort first:

- GUI: gate on libadwaita >= 1.4 with an actionable message. 'Adw 1' only pins
  the API series, so Ubuntu 22.04 (1.1), Debian 12 and derivatives (1.2) and
  Leap 15.5 imported fine then died with a bare AttributeError mid-UI.
- Makefile: CXX defaults to c++ (clang++ on Chimera, which ships no GCC);
  probe for -latomic (32-bit ARM has no 8-byte atomic); PYTHON is overridable
  (NixOS, Guix, Leap's python311); install no longer uses GNU-only 'install -D',
  which BSD-derived install (Chimera) rejects.
- aarch64: nothing in the code was x86 specific, only packaging metadata said so.
  PKGBUILD arch adds aarch64, debian/control becomes Architecture: any, and the
  deb/rpm builder derives the arch from the host.
- debian/control: 'g++ (>= 13)' was inert because Debian's g++ carries epoch 4,
  so g++ 12 satisfied it. Now '(>= 4:13) | g++-13 | g++-14'. libadwaita and
  gir1.2-adw-1 gain the >= 1.4 floor the GUI actually needs.
- AppImage: honour sys.platlibdir, so an image built on Fedora, RHEL or SUSE
  (lib64) is no longer dead on arrival.
The desktop entry is Terminal=false, so raising SystemExit for the libadwaita
version gate wrote to stderr that nobody sees: launching from the application
menu on an older distribution would have failed completely silently. Report
startup blockers through a small GTK window, falling back to zenity, kdialog or
notify-send, and keep printing to stderr for terminal users.

Found by adversarially verifying the previous commit's gate.
…case

Adversarial review of the gate found the remedy it suggested was circular: it
told users to use the AppImage, but an AppImage cannot bundle glibc, and every
release shipping libadwaita < 1.4 also ships an older glibc than these binaries
need (verified: our binary requires GLIBC_2.38; Ubuntu 22.04 has 2.35, Debian 12
has 2.36, Leap 15.5 has 2.31). It now names releases that actually work and
points at the command line bridge, which genuinely does not need libadwaita.

Also:
- Handle a missing GTK or libadwaita typelib (packaged separately on Debian and
  Ubuntu as gir1.2-gtk-4.0 and gir1.2-adw-1). That raised at gi.require_version
  before the gate could run, so it was another invisible failure. It now reports
  through zenity, kdialog or notify-send, using no GTK call.
- Apply the libadwaita >= 1.4 floor consistently: it was only in debian/control,
  so the rpm spec, the AUR PKGBUILD and the fpm builder still installed onto
  systems where the app then refused to start.
- Report the micro version too, and drop a stale AppImage status note.
Steam's July 2026 Linux breakdown puts SteamOS Holo at 22.24 percent, the single
largest platform this app's users run, ahead of CachyOS 14.31, Arch 8.29, Mint
22.3 8.12 and Bazzite 7.73. Our binary would not load there.

The reason is narrow. SteamOS 3.8 ships gcc 15.1.1 and glibc 2.41, so glibc is
not the problem, but its libstdc++ caps at GLIBCXX_3.4.34 and exactly three of
our undefined symbols exceed that, all of them std::format internals. Linking
libstdc++ and libgcc statically removes the ceiling entirely (verified: no
GLIBCXX references remain, GLIBC stays at 2.38 which SteamOS exceeds).

- make portable (or PORTABLE=1) links the C++ runtime statically. 284K becomes
  2.5M, which is the right trade for a redistributable binary.
- The default build is unchanged and still links dynamically, which is what
  distribution packages require.
- The AppImage now builds the portable CLI, so it runs on the Deck.

This also settles a question the other way: rewriting the std::format call sites
(167 occurrences across 17 files, not the 21 first estimated) would be a large
change to code Windows and macOS also compile, and it would buy distributions
that barely appear in this population. Ubuntu 22.04 and Mint 21 are not in the
Steam top 16 at all, and the Mint that is (22.3) ships libadwaita 1.5.
The previous version reordered the README to lead with Linux, which rewrote 120
lines of the upstream front page. Restore it exactly and add Linux the same way
macOS was added: a third subsection under Quick start and under Build, in the
order the platforms arrived. Net effect on the upstream README is now +59 -0.

The Linux-first arrangement stays on the fork, where it belongs.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant