The Track My Indoor Workout Application
Sep 25, 2020
We are living in a pandemic time right now, and one of our favorite times spending together with my wife - attending to CycleBar spinning classes together - had to come to an end. She has a Les Mills subscription though (we like body pump, she does aerobics too) and we wanted to transition to Les Mills RPM spinning classes. For that I bought an opened box Sunny SF-B901 (chain drive, friction resistance) for $140 and an opened box return of Sunny SF-B1423 (belt drive, friction resistance) for $130 for her. They worked excellent especially given their price range, my wife’s even had a very basic console which displayed cadence and a few measurements. At high resistance they gave some high pitch humming noise but I could dampen that with a few foam pieces around the resistance controller bowdens and a little WD-40 on the resistance area. I’d advise these bikes to anyone, except that the SF-B1423 was small for my 6’4” height and tall inseam, while my SF-B901 could be too big for 5” or under athletes.
I wanted to step up though and I started to watch public auctions close to me, until I landed an unbelievable Precor Spinner® Chrono™ Power for $700 - needed some mechanical fix - and a ripped seat Schwinn IC4 for $400. Both bikes have a belt drive system, magnetic resistance and also decent display. I like to record my activities, I’m syncing my workouts from Wear OS SUUNTO7 to Suunto’s ecosystem, which is synced to Strava. I also also use MapMyRun / MapMyFitness. When I measured my indoor exercises on my watch that won’t have the power readings, the cadence and distance measurements to name a few. Precor’s Youtube video showed that the Wahoo Fitness app could unofficially record the workout. That Youtube video does not allow any commenting and I cannot even favorite it for some reason, not even mentioning that it talks about an ancient earlier version of Wahoo’s app.
I immediately noticed however that the Wahoo app did not pick up the power meter reading and the cadence data. It had distance, speed, heart rate (which is optional), but not the two metrics I was mentioned. Power meter is such an advanced feature you don’t want to miss and cadence is an essential data as well. On top of that some of the cycling related challenges on Strava only accept GPS tracked activities. Certain virtual riding ecosystems offer rides in real world locations (like London) and I didn’t want to be left behind such systems while I have the required data to calculate GPS. Since Precor videos didn’t allow any commenting (or even favoriting) I started a Reddit thread on my issues. It turned out that there’s another app, but that requires a monthly subscription.
I decided to write my own application. Since I just had a lot of fun porting Deal-O-Round over from Play-N to Flutter it was a straightforward decision to pick Flutter again: this will allow the release of an iOS version later. I started to research pub.dev and Medium articles about packages which would be able to cover my needs: bluetooth scanning and data reading, OAuth or Strava integration and FIT / GPX / TCX file generation.
There are at least half a dozen bluetooth packages and I picked flutter_blue to first develop a stub I could use to reverse engineer Precor Spinner® Chrono™ Power’s BLE (Bluetooth Low Energy) protocol. First I confirmed that the bike’s console indeed supplied the data through BLE. It’s important to note that BLE and regular Bluetooth are two different standards residing under the same umbrella. I studied Wahoo Fitness Equipment’s BLE protocol (https://docplayer.net/42079050-Wahoo-fitness-equipment-profile.html) and knowing that the Wahoo Fitness app partially supports my bike I expected that the bike’s protocol will be almost the same or very similar. If you look at the Wahoo protocol it’s very versatile but it is complicated. For example the byte readings start with four bytes of flags, and based on certain features present or not (for example if there is heart reading or not) the whole payload has a variable length.
I spent many hours in the saddle to hone the protocol down and fortunately Precor’s is less complicated than Wahoo’s: the actual measurement payload has fixed length with a couple of flags so if there’s no heart rate reading then those bytes are simply zeroed out. That’s true for all metrics and that’s how I also identify paused state: if speed becomes zero then the workout is paused. After I got the protocol in the bag I moved onto the user interface and other features.
It’s not advisable to accumulate all the workout data in memory, because the phone can crash or run out of battery. So during the workout I continuously persist all the Record snippets into an SQLite database. The advised way in Android is using JetPack’s Room. Flutter has already two popular similar libraries: moor and floor, both of them relying on sqflite and doing similar persistence code generation. I picked Floor, but Moor could be just as good as well. On the integration front I discovered strava_flutter and rw_tcx. Originally I planned on writing FIT files, because they are more compact being a binary format. However I discovered that Strava Upload API allows gzipped version of the TCX and GPX based textual formats. Seeing rw_tcx I decided to go for TCX and studied the related XML schema definition files. In the end I forked both packages and spinned out my own version to accommodate needs like generating in-memory file, compression on-the-fly during generation and uploading, or offer download from the in-memory file without materializing it on disk.
These were the most important pillars of the app, but I also contributed to some packages along the way (see listview_utils contribution) and I’d advise you to also embrace the all-around helper get package.
Talking about the future: the app is capable of supporting other bikes, or even treadmills. Simulating the workout on a track opens up the possibility to let the user configure the location to any velodrome or running track around the world. I’m also exploring possibilities to manufacture an iOS release, but that’s kind of a tough thing to do with as closed ecosystem as Apple’s. I don’t have MacBook or OSX devices, currently I’m looking at Bitrise and Codemagic if they can help, but without an actual OSX device that’d be a steep jump: I’m sure there’d be issues to resolve on my end before a usable build would come out. I’m also curious to receive any user suggestions and improvement requests.