Skip to content
607011Public

About

Unchain is an iOS app to control BLE FTMS capable devices like indoor bike trainers or treadmills

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Latest commit

 

History

119 Commits

Folders and files

Repository files navigation

Unchain

CI

Unchain app icon

Lean iOS app for controlling smart trainers (Kickr & co.) via the open FTMS Bluetooth standard (Fitness Machine Service) — not a Wahoo-proprietary protocol, so it works in principle with any FTMS-capable trainer.

MIT licensed. If Unchain's useful to you, a GitHub Sponsors or Buy Me a Coffee contribution toward keeping it going (mainly an Apple Developer Program membership) is welcome, always optional — the app itself never asks.

Status

Unchain has grown through two rough phases – MVP bike-trainer control, then treadmill/FTMS/workout-file support and beyond – tracked as a running, dated decision log rather than a snapshot. See STATUS.md for a list of supported smart trainers, the full feature history and the App Store readiness checklist.

Generating the project

The Xcode project itself isn't kept in the repo; it's generated from project.yml via XcodeGen (the usual way to avoid .xcodeproj merge conflicts).

brew install xcodegen
cd /Users/olau/Workspace/bahoo-kickass
make generate
open Unchain.xcodeproj

Then, in Xcode, select your own team under Signing & Capabilities.

make generate is xcodegen generate plus one more step: Scripts/stamp-build-info.sh stamps the current git describe string, an auto-incrementing build number (total commit count), and a build timestamp into the freshly (re)generated Generated/Info.plist — shown at the bottom of the Devices screen (see AppVersionInfo) so a specific build's origin is always identifiable at a glance. Running plain xcodegen generate instead still works, it just leaves that footer showing whatever it last said.

Known Xcode pitfalls on this Mac (already resolved)

Two issues have already come up here and been fixed — in case they recur after an Xcode update, here are the fixes:

  • xcodebuild/swift/Xcode.app crash with Symbol not found: _XPCTypeBool (CoreDevice.framework vs. Mercury.framework) → fixed with sudo xcodebuild -runFirstLaunch. If that doesn't help, additionally run sudo rm -rf /Library/Developer/PrivateFrameworks/CoreDevice.framework and run the command again.
  • "The developer disk image could not be mounted on this device" when running on a real iPhone → usually a stale cache. Fix with:
    mv ~/Library/Developer/DeveloperDiskImages ~/Library/Developer/DeveloperDiskImages.bak
    Then restart Xcode and reconnect the device — Xcode automatically downloads a fresh disk image from Apple.

Getting workout files (.erg/.mrc)

A ramp test for FTP estimation is bundled with the app (Program tab → "Load Sample Ramp Test"). For anything else, you need ready-made .erg/.mrc files – most providers require an account for that – one exception:

  • cyclingintervals.com — free structured workouts available directly as .erg, .mrc, .zwo, or .fit downloads, no registration/login required.
  • trainerday.com (formerly ErgDB) — very large library, but downloads usually require login.

Running

Bluetooth doesn't work in the simulator — the app must run on a real iPhone.

DEVICE auto-detects whichever iPhone is currently connected via USB (connectionProperties.transportType == 'wired' in xcrun devicectl list devices – distinguishes a live USB connection from a Wi-Fi-paired-but-not- connected device, which otherwise looks the same in the device list), so make run picks up Oliver's phone or his wife's without needing DEVICE=... every time it's the other one. Falls back to a fixed default if nothing's connected by USB right now; override on the command line the same way as always for a not-currently-plugged-in device, e.g. make run DEVICE=Eipättpro.

make run/make install/make debug only ever push Unchain.app (with UnchainWatch.app embedded inside it) to the iPhone – that's not the same as the paired Watch picking up the new embedded build, which normally depends on the Watch app's own "Automatic App Install" sync and doesn't reliably (or quickly) reflect a fresh development build. Use make install-watch/make run-watch/make debug-watch to push straight to the paired Watch instead, the same way the plain targets do for the iPhone (WATCH_DEVICE defaults to watchOLA, override the same way DEVICE works). Bike vs. treadmill behavior on the Watch itself still can't be tested in the Simulator either – needs a real, paired Watch.

Idea for later: elevation lookup for GPX tracks without <ele>

GPX-route riding (see STATUS.md) is deliberately offline-only for now: a track missing elevation data is rejected rather than filled in from the internet. If that turns out to be too limiting in practice:

  • Plain OpenStreetMap has no elevation data itself (it's a vector map, not a DEM), so this needs an actual elevation dataset. The best option found so far is Copernicus DEM GLO-30 — 30 m resolution, near-global coverage, distributed as ready-to-use Cloud-Optimized GeoTIFF tiles directly on AWS S3 (AWS's Open Data Sponsorship Program: no login, no cost, freely licensed).
  • Rather than a live per-point API call, the nicer fit for this app's offline-first design is a one-time regional tile download: fetch the 1°×1° GeoTIFF tile(s) covering a track's bounding box once, cache them on-device, and do the elevation lookup locally against the cached raster afterwards. Every later GPX import in that region then needs no network at all — including out on a ride with no signal — which a live lookup wouldn't give you. iOS has no built-in GeoTIFF/COG reader (no MapKit/CoreLocation support for this), but a point lookup via the raster's own geotransform only needs a small, purpose-built parser, not a general raster library.
  • Fallback if that turns out to be impractical: a live SRTM-based API like Open-Elevation or Open Topo Data (both open-source, self-hostable) — simpler to integrate, but back to a network call on every import, indefinitely, not just the first one per region.
  • Either way, this would be the first feature needing network access at all — the app has been Bluetooth/HealthKit/local-file-only until now, worth flagging explicitly to the user rather than adding quietly.
  • Not every trainer supports Indoor Bike Simulation — check via the feature overlay (ℹ️ next to the trainer name, look for "Indoor Bike Simulation (Grade)" under Adjustable Targets) before relying on it; a graceful fallback (e.g. resistance-% approximated from the gradient) isn't implemented, since FTMS doesn't guarantee a fixed, predictable relationship between resistance level and felt difficulty (see the resistance-mapping fix earlier in this project's history) the way it does for grade simulation.

Considered and rejected: an HR+-style heart-rate-locked control mode

TrainerDay has an "HR+" mode: set a target heart rate, and it continuously adjusts the power sent to the trainer to hold you there — a closed-loop controller, unlike anything else in this app (Power/Resistance/Grade/Program are all either manual or a fixed, pre-planned schedule; nothing else reacts live to a sensor's own noisy signal). Deliberately not building this: heart rate is a poor control input, however useful it is as a monitoring one (which is exactly what HeartRateZone already covers – live and post-workout, never driving anything). It's confounded by too much that has nothing to do with the current training stimulus – riding position, sleep, heat, caffeine, daily form, cardiac drift even at genuinely constant power over time – so a controller reacting to it in real time would be reacting to noise as much as signal, and heart rate's own lag behind a power change (tens of seconds, more at low fitness) makes naive proportional control (raise power while HR is below target) prone to overshoot: it keeps pushing power up while HR is still catching up, then over-corrects down once it finally arrives. Power and Grade are already exactly the two direct, unconfounded control variables a trainer can hit precisely — there's nothing an HR-based layer on top would add that either doesn't already provide more reliably.

About

Unchain is an iOS app to control BLE FTMS capable devices like indoor bike trainers or treadmills

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages