Hyperborea is an Android app that bridges ICON Fitness exercise equipment to Zwift and other fitness platforms. It reads live exercise data from the equipment's console over USB serial using the FitPro protocol and re-broadcasts it as standard fitness protocols — Bluetooth Low Energy FTMS and a WiFi TCP stream — so apps like Zwift see a normal smart trainer. It also forwards resistance/incline targets back to the equipment for ERG-style control.
It runs on the equipment's own Android console (the primary target is the NordicTrack S22i, a 22"
landscape console) and works with any ICON Fitness device that speaks the FitPro protocol over USB
(USB vendor ID 0x213C) — bikes, treadmills, and ellipticals. These consoles ship a range of
Android versions in the field — anything from Android 5.1 to Android 9+ — and Hyperborea supports
the whole range.
Important
Disclaimer. This is an independent project. It is not affiliated with, authorized by, or endorsed by ICON Health & Fitness, iFit, NordicTrack, ProForm, FreeMotion, Schwinn, Zwift, or any other company. "iFit", "NordicTrack", "ProForm", "FitPro", "Zwift", and other names are trademarks of their respective owners and are used here only for identification. The software is provided "as is" with no warranty (see LICENSE). You are responsible for complying with the terms of service and warranties of your equipment and the platforms you connect to, and for any modifications you make to your device. Use at your own risk.
| Dashboard | Profile picker | Ride detail | Settings |
|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
More screenshots are in screenshots/.
Requires the Android SDK (and JDK 17+). Create a local.properties in the repo root pointing at
your SDK (see local.properties.example):
sdk.dir=/path/to/Android/SdkThen:
./gradlew :app:assembleStandardDebug # debug APK -> app/build/outputs/apk/standard/debug/
./gradlew test # run all unit tests
./gradlew lint # static analysisAlways build the standard product flavor. Release builds (./gradlew :app:assembleStandardRelease)
are signed with release.jks if present, otherwise with the debug key — see
local.properties.example for the optional release-signing and
self-update/diagnostics settings. The ./gradlew prepareRelease task runs the full
clean → lint → test → build → package pipeline and requires a release keystore.
Hyperborea installs as a regular Android APK over ADB — no root required. The release/Hyperborea/
directory contains deploy scripts (deploy.sh, deploy.ps1, deploy.cmd) that:
adb install -r -gHyperborea onto the console.adb shell pm disable-user --user 0 …the iFit apps that compete for the FitPro USB device (standalone, ERU, GlassOS service, and friends —com.ifit.launcheris left enabled so the home button still works).adb rebootto make sure no iFit processes survive.
All of step 2 works as the unprivileged shell user; the only prerequisite is an ADB connection
to the console. On consoles where ADB isn't enabled out of the box, community tools like atvTools
can enable it without rooting the device.
BLE caveat. On consoles whose BSP ships with config_bluetooth_le_peripheral_mode_supported=false
in framework-res.apk, getBluetoothLeAdvertiser() returns null and FTMS broadcast over BLE won't
work without a /vendor/overlay/ fix — and that overlay push does require root. WiFi broadcast
(TCP, port 36866) works on every device. The previous deployment flow installed Hyperborea as a
privileged system app at /system/priv-app/Hyperborea/ and pushed the BLE-peripheral overlay
along with it; that path lives on the archive/priv-app-deployment
branch if you need to revive it for a rootable console.
:app → :core ← :hardware:fitpro
↓ :broadcast:ftms
↓ :broadcast:wifi
↓ :ecosystem:ifit
↓ :sensor:hrm
└── wires everything together via Hilt
:core— pure Kotlin (no Android deps): domain interfaces, data types, orchestration logic.:hardware:fitpro— the FitPro USB-serial hardware adapter (115200 baud).:broadcast:ftms— BLE GATT server advertising FTMS (service0x1826).:broadcast:wifi— WiFi TCP server for fitness apps.:ecosystem:ifit— coexistence with the stock iFit apps (stop the standalone app, disable the crash-looping update receiver) while Hyperborea owns the USB device.:sensor:hrm— optional external BLE heart-rate monitor support.:app— Compose UI (Material 3, landscape), the foreground service, the orchestrator wiring, and the Android platform implementations of the:coreinterfaces.
Data flows hardware → orchestrator → each broadcast sink; control targets flow back the other way.
See AGENTS.md for the design conventions and module details.
Two features are disabled unless you configure them at build time (see
local.properties.example):
- In-app self-update — checks a manifest URL (
server.url) and downloads APK updates from an allowed prefix (r2.base.url). With noserver.urlconfigured, auto-update is off. - Support-diagnostics upload — POSTs a logs+system bundle (no personal data) to
${server.url}/api/support/uploadfrom the in-app "Get help" action. Off whenserver.urlis empty.
Neither feature sends authentication; a fork hosting its own infrastructure can point these at its
own endpoints (and add certificate pinning in app/src/main/res/xml/network_security_config.xml).
See CONTRIBUTING.md.
MIT © 2026 Jonathan Ball.



