GNSS + IMU fusion (UKF) for Android, outputting high-rate NMEA 0183 over TCP — built for feeding RaceChrono with 1–25 Hz position data on track (cars and motorcycles).
GNSS (FusedLocationProvider, ~1 Hz from HAL) ──┐
├─► 6-state UKF [pos NED, vel NED]
IMU linear accel @ 100–200 Hz ── rotated to ───┘ │
NED via ROTATION_VECTOR or FSensor orientation ▼
output ticker @ 1–25 Hz
│
NMEA GGA + RMC (+VTG/GSA)
│
TCP server (default 127.0.0.1:5555)
+ optional file logging
- The phone's GNSS HAL only delivers ~1 Hz. The extra rate is IMU dead-reckoning between fixes via the UKF predict step, not interpolation — so latency is low and positions move with real vehicle dynamics (important for lap timing).
- Runs as a foreground service (location type, partial wakelock); the notification shows the output rate and current position.
- Mount auto-calibration: gravity low-pass gives the phone's "down"; correlating device acceleration with GNSS speed changes during one firm brake/accel gives "forward". After that the app reports vehicle-frame long/lat G and motorcycle lean angle regardless of how the phone is mounted. The UKF itself is mount-independent (accel is rotated to NED with the orientation sensor).
- FSensor option: toggle to use KalebKE/FSensor
Kalman-fused orientation instead of Android's
ROTATION_VECTOR.
- Start FusionGNSS, press Start, wait for fix.
- RaceChrono → Settings → GPS/receiver → NMEA over TCP/IP →
host
127.0.0.1, port5555(default; both configurable in the app). - Keep "Localhost only" on when RaceChrono runs on the same phone (lowest latency, nothing exposed on the network). Turn it off to serve another device over Wi-Fi.
| Setting | Default | Notes |
|---|---|---|
| Output rate | 10 Hz | 1–25 Hz slider |
| TCP port | 5555 | |
| Localhost only | on | off = bind 0.0.0.0 |
| Log NMEA to file | off | Android/data/com.fusiongnss/files/logs/ |
| FSensor orientation | off | uses KalebKE/FSensor Kalman fusion |
| Raw GPS provider | off | LocationManager.GPS_PROVIDER instead of Fused (no smoothing, lowest GNSS latency) |
| VTG / GSA | off | GGA + RMC are always sent |
Open in Android Studio (Ladybug+) and run. minSdk 30 (Android 11), Kotlin + Jetpack
Compose Material 3, Play Services Location. FSensor comes from JitPack — if the
v2.2 tag fails to resolve, check the latest tag on JitPack and adjust
app/build.gradle.kts.
- NMEA timestamps are GNSS UTC (from
Location.time), extrapolated between fixes with the monotonic clock — independent of the phone's wall clock. - NMEA lat/lon uses 4 decimal minute digits (~0.2 m). RaceChrono accepts this fine.
- HDOP in GGA is synthesized from UKF position covariance, not true DOP.
- Disable battery optimization for the app for long sessions.
- IMU-only drift between 1 Hz fixes is bounded (<1 s horizon) so accuracy stays close to GNSS accuracy while adding real inter-fix dynamics.