CASE STUDIESProjekt własny
CapCam: telefon na czapce jako head tracker do gier
Śledzenie głowy w symulatorach wyścigowych bez kamery i diod IR. Android czyta żyroskop, składa pozę z pozycją noszenia telefonu i wysyła ją po UDP do opentracka, który podaje się grom za TrackIR.
- Kiedy
- 2026
- Rola
- Autor: aplikacja, protokół, narzędzia pomiarowe
- Stack
- KotlinJetpack ComposeAndroidUDPopentrack
- Linki
- repozytoriumAPK
200 Hz
maksymalna częstotliwość wysyłki
48 B
pakiet opentracka: sześć liczb f64
12
sposobów zamocowania telefonu w jednym modelu
Problem
Chciałem w BeamNG i Assetto Corsa rozglądać się ruchem głowy, ale TrackIR to drogi sprzęt, a rozwiązania na kamerę potrzebują światła i diod. Telefon ma żyroskop, Wi-Fi i da się go przyczepić do daszka czapki. Opentrack przyjmuje dane po UDP i podaje je grom przez protokół TrackIR, więc brakowało tylko aplikacji po stronie telefonu.
Problemy wyszły szybko. Żyroskop dryfuje w osi yaw, bo fuzja bez magnetometru nie ma odniesienia do północy. Pierwsze próbki po starcie sensora są nieświeże. Telefon może leżeć płasko na daszku, stać pionowo na czole albo bokiem na kasku, a każda pozycja zamienia osie. Android traktuje żądaną częstotliwość sensora jako podpowiedź, nie gwarancję. Ekran świecący pod daszkiem przeszkadza, a bez niego nie ma jak wcisnąć „recenter”. I wreszcie: użytkownicy wpisują adres z „whatismyip” albo mają włączony VPN, i pakiety nie docierają.
Podejście
Dryf uznałem za cechę, nie błąd do kompensowania: zamiast walczyć z fizyką, recentrowanie jest tanie i zawsze pod ręką. Źródłem orientacji jest TYPE_GAME_ROTATION_VECTOR (żyroskop plus akcelerometr), więc pitch i roll trzymają się grawitacji, a yaw zeruje się offsetem kwaternionowym wokół osi Z. Tracker przez pierwsze 500 ms zwraca tożsamość i nie ustawia „przodu”, dopóki fuzja się nie ustabilizuje.
Pozycję noszenia opisałem jednym modelem zamiast ręcznych zamian osi: kierunek, w który patrzy ekran, razy krawędź, która jest „górą”, daje 12 montaży. Macierz głowa→urządzenie powstaje z iloczynu wektorowego, kwaternion metodą Shepperda, a orientacja głowy to złożenie obrotu urządzenia w świecie z obrotem głowy względem urządzenia. W interfejsie zostały trzy presety, przełącznik „telefon obrócony o 180°” i pozycja własna.

Rozwiązanie
- Protokół: 48-bajtowy pakiet opentracka (sześć f64 little-endian: x, y, z, yaw, pitch, roll) na port 4242 oraz własny 36-bajtowy format z kwaternionem, sekwencją i znacznikiem czasu na port 4243 dla modów. Jeden DatagramPacket reużywany, cały potok sensor→tracker→UDP na jednym HandlerThread z priorytetem wyświetlania i WifiLock w trybie low latency.
- Limiter częstotliwości trzyma harmonogram „kiedy należy wysłać” zamiast odstępu od ostatniej wysyłki: sensor na 101 Hz przy limicie 100 nie traci co drugiej próbki. Luz ćwierć interwału, bez nadrabiania po przerwie. Panel ostrzega, gdy realna częstotliwość spada poniżej 80 procent wybranej.
- Znaki osi dopasowane do opentracka (yaw dodatni to w prawo) przez dekompozycję R = Rz(−yaw)·Rx(pitch)·Ry(roll), z testem na ciągłość przy przejściu przez ±180°.
- Tryb czarnego ekranu: nakładka zakrywa wszystko z paskami systemowymi, jasność 0,01, FLAG_KEEP_SCREEN_ON, a przyciski głośności przechwycone w onKeyDown robią recenter z telefonem na głowie. Tap odsłania interfejs na 10 sekund.
- Wibracja potwierdzająca nie docierała, gdy użytkownik wyłączył haptykę dotyku: atrybuty wibracji zmienione na USAGE_HARDWARE_FEEDBACK.
- Wykrywanie sieci: ConnectivityManager z NetworkCapabilities rozpoznaje VPN i porównuje podsieć wpisanego adresu z adresem telefonu, odświeżając co 3 sekundy. Komunikat mówi wprost, od czego ma się zaczynać adres PC.
- Czułość per oś 0,5–3× i „side boost”: za progiem (domyślnie 20°) czułość rośnie gładko kwadratowo, więc środek jest spokojny, a skrajne spojrzenie obraca kamerę dalej, niż da się skręcić głowę bez utraty ekranu z oczu.
Efekt
Potwierdzone w BeamNG.drive, Assetto Corsa, American Truck Simulator i Richard Burns Rally. Wysyłka do około 200 Hz z opcjami 60 i 100 Hz dla baterii, APK około 1,2 MB po R8, Android 8.0 wzwyż. Do mierzenia jittera i strat napisałem monitor UDP w Node, który liczy p50 i p99 odstępów między pakietami.
Czego się nauczyłem
- Parametr częstotliwości sensora w Androidzie to podpowiedź; limiter musi zakładać, że próbki przyjdą szybciej i nierówno.
- Domyślna wibracja „dotykowa” jest wyciszana ustawieniem użytkownika. Sygnał potwierdzenia potrzebuje innego kanału.
- Dobrze dobrany model (12 montaży z dwóch wektorów) zastąpił rosnącą listę przypadków szczególnych.