Like many others adapting to new routines, maintaining my regular exercise has been a challenge. To combat this, I decided to try an under desk exercise bike, hoping to integrate some cardio into my workday. The model I chose has proven to be quite good – it’s quiet, stable, offers adjustable resistance, and includes Bluetooth for workout tracking.
If you’re thinking about getting an under desk bike, it’s worth considering. While they might not work seamlessly with every desk setup (standard desks can lead to knee bumps), my setup happens to be ideal. I’ve always preferred keeping my keyboard on the desk surface rather than using a keyboard tray, which allows me to raise my chair and use a footrest comfortably. This configuration provides ample legroom for pedaling comfortably at my desk.
The Bluetooth connectivity was a key feature for me; tracking metrics like speed and distance is a great motivator. The bike comes with its own mobile app, but honestly, it left much to be desired. The app felt clunky, user reviews were mixed, it required account creation, and it constantly pushed subscriptions for workout classes. While these are minor annoyances, my main issue was convenience. I didn’t want to constantly grab my phone and navigate through menus every time I wanted a quick workout session at my desk. Furthermore, the idea of my workout data being locked within a mediocre app was unappealing.
The solution became clear: I needed to build my own desktop application.
The DIY Under Desk Bike App Project
Inspired by the resourcefulness demonstrated in “Unbricking a $2,000 Bike With a $10 Raspberry Pi,” I suspected my under desk bike operated on similar principles. If I could establish a Bluetooth connection from my computer, I could develop a tailored application to meet my specific needs.
Project Goals
- Real-time Workout Display: Create a compact desktop window showing essential workout data at a glance.
- Data Logging and Analysis: Record workout data to a local SQLite database for performance tracking, goal setting, and workout history review.
- Automatic Workout Detection: Ideally, the app should automatically start and stop tracking workouts based on pedaling activity.
Initial Hurdles
- Desktop Bluetooth Deficiency: My primary workstation lacked built-in Bluetooth capabilities.
- Bluetooth LE Inexperience: I had no prior experience working with Bluetooth Low Energy (LE) technology.
The Bluetooth issue was easily resolved with a USB Bluetooth adapter from Amazon. The Bluetooth LE learning curve, however, would be tackled head-on as part of this project.
Step 1 – Diving into Bluetooth Research
My first step was to gather as much information as possible. This led me to nRF Connect, a powerful Bluetooth exploration app for Android, available on the Google Play Store. This app is invaluable for examining nearby Bluetooth devices, allowing connections, displaying device profiles, and logging communication data. Using nRF Connect, I discovered that my under desk bike utilizes a Nordic UART Service (NUS) chip, detailed in the Nordic Semiconductor documentation. This was excellent news, suggesting the bike communicated using a straightforward serial data stream over Bluetooth.
Initially, simply connecting with nRF Connect didn’t yield any data. While I could establish a connection and subscribe to data changes, no information was being transmitted. This required deeper investigation into the communication protocol.
Next, I decided to analyze the official app’s Bluetooth traffic to understand how it interacted with the bike. Following the guide “How to get the Bluetooth Host Controller Interface logs from a modern Android phone,” I enabled Bluetooth logging on my Android phone. By simultaneously screen-recording and logging Bluetooth activity during workouts, I could then download and analyze these logs.
Using Wireshark, a powerful network protocol analyzer, and comparing the logs with my screen recordings, I was able to decipher the communication between the app and the bike. I observed a pattern: the app sends command packets, and the bike responds with data packets. During active workouts, the app repeatedly sends a specific command, prompting the bike to send back workout data.
Step 2 – Establishing Desktop Connection with the Bike
From the Wireshark analysis and Bluetooth logs, I identified six distinct commands sent by the official app. My next objective was to build a basic console application to connect to the bike, transmit these commands, and process the responses. This would serve as a proof of concept, verifying that I could replicate the app’s behavior from my desktop environment.
For this project, I chose the .NET 5.0 framework on Windows because it provides direct access to the Windows Runtime Bluetooth API. Integrating this API into a desktop application is well-documented and relatively straightforward, as explained in Microsoft’s guide on “Modernizing Desktop Apps for Windows 10“.
Step 3 – Decoding and Interpreting Workout Data
The first command, which I labeled the Connect
command, proved essential and must be sent before any other commands. After initiating the connection, subsequent commands needed to be sent at intervals of less than one second to maintain the connection; otherwise, the bike would disconnect.
Start Command: f9 d0 00 c9 Response: f9 e0 00 d9
The next command, the Hold
command, appeared to be a keep-alive signal, ensuring the connection remained active. The bike responded with two identical packets each time this command was sent.
Hold Command: f9 d1 05 02 00 00 00 00 d1
Response #1: f9 e1 10 07 00 00 00 00 00 00 02 00 03 37 00 00 2a
Response #2: f9 e2 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 eb
Following these were two one-time commands, named Info1
and Info2
, likely used to retrieve model information or calibration parameters.
Info1 Command: f9 d3 0d 01 00 00 2c 00 00 3c 00 a0 00 00 00 00 e2 00 00 00
Response #1: f9 e3 01 00 dd
Response #2: f9 e3 0c 00 00 00 00 00 00 00 00 00 00 00 00 e8
Info2 Command: f9 d4 0f 02 00 00 00 00 00 00 00 00 00 00 00 00 1f 0f 0c 00
Response: f9 e4 02 00 00 df
A pattern started to emerge in the command and response structure. Each packet began with the byte F9
. The subsequent byte indicated the command or response type, with the higher nibble being D
for commands and E
for responses. The third byte represented the packet length (excluding the header), and the byte after that seemed to be a checksum for data integrity.
The most crucial commands were Start Workout
and Continue Workout
. The Start Workout
command is sent at the beginning of a session, and then the Continue Workout
command is sent repeatedly to sample data from the bike during the workout.
Start Cmd: f9 d5 0d 01 00 00 00 00 00 00 00 00 00 00 00 00 dc 00 00 00
Response #1: f9 e5 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 ef
Response #2: f9 e6 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ef
Response #3: f9 e7 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f0
Continue Cmd: f9 d5 0d 00 00 00 00 00 00 00 00 00 00 00 00 00 db 00 00 00
Response #1: f9 e5 10 00 09 00 03 00 07 00 00 00 99 00 00 53 00 00 01 ee
Response #2: f9 e6 10 00 00 00 00 00 06 00 00 00 00 00 00 00 00 00 2f 24
Response #3: f9 e7 10 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f0
With the commands identified, I began to focus on interpreting the workout data. I aggregated the data packets from the Continue Workout
command responses, removing the headers. Then, I cycled for about 10 minutes, capturing data throughout the workout.
00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 EF
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EF
00 00 00 00 00 00 00 00 00 00 00 00 00 F0
01 00 00 00 00 00 00 00 00 00 00 00 00 00 01 F0
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EF
00 00 00 00 00 00 00 00 00 00 00 00 00 F0
02 00 00 00 00 00 00 00 00 00 00 00 00 00 01 F1
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 EF
00 00 00 00 00 00 00 00 00 00 00 00 00 F0
03 00 00 00 01 00 00 00 BC 00 00 66 00 00 01 15 00 00 00 00 07 00 00 00 00 00 00 00 00 00 19 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
03 00 01 00 01 00 00 00 BB 00 00 65 00 00 01 14 00 00 00 00 07 00 00 00 00 00 00 00 00 00 19 0F 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
04 00 01 00 02 00 00 00 BE 00 00 67 00 00 01 1B 00 00 00 00 07 00 00 00 00 00 00 00 00 00 25 1B 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
05 00 01 00 03 00 00 00 C1 00 00 69 00 00 01 22 00 00 00 00 07 00 00 00 00 00 00 00 00 00 2D 23 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
06 00 02 00 04 00 00 00 C3 00 00 6A 00 00 01 28 00 00 00 00 07 00 00 00 00 00 00 00 00 00 32 28 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
07 00 02 00 06 00 00 00 C1 00 00 69 00 00 01 28 00 00 00 00 07 00 00 00 00 00 00 00 00 00 36 2C 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
07 00 03 00 06 00 00 00 C1 00 00 69 00 00 01 29 00 00 00 00 07 00 00 00 00 00 00 00 00 00 36 2C 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
08 00 03 00 07 00 00 00 BE 00 00 67 00 00 01 26 00 00 00 00 07 00 00 00 00 00 00 00 00 00 39 2F 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
09 00 03 00 08 00 00 00 BA 00 00 65 00 00 01 22 00 00 00 00 07 00 00 00 00 00 00 00 00 00 3B 31 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
0A 00 04 00 09 00 00 00 B0 00 00 5F 00 00 01 15 00 00 00 00 07 00 00 00 00 00 00 00 00 00 3C 32 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
0B 00 04 00 0A 00 00 00 B5 00 00 63 00 00 01 20 00 00 00 00 07 00 00 00 00 00 00 00 00 00 3D 33 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
0B 00 05 00 0A 00 00 00 B8 00 00 64 00 00 01 25 00 00 00 00 07 00 00 00 00 00 00 00 00 00 3D 33 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
0C 00 05 00 0C 00 00 00 BB 00 00 66 00 00 01 2D 00 00 00 00 07 00 00 00 00 00 00 00 00 00 3E 34 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
0D 00 06 00 0D 00 00 00 BF 00 00 68 00 00 01 36 00 00 00 00 07 00 00 00 00 00 00 00 00 00 3F 35 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
0E 00 06 00 0E 00 00 00 BF 00 00 68 00 00 01 38 00 00 00 00 07 00 00 00 00 00 00 00 00 00 40 36 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
0F 00 06 00 0F 00 00 00 BF 00 00 68 00 00 01 3A 00 00 00 00 07 00 00 00 00 00 00 00 00 00 41 37 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
0F 00 07 00 0F 00 00 00 BE 00 00 67 00 00 01 39 00 00 00 00 07 00 00 00 00 00 00 00 00 00 41 37 00 00 00 00 00 00 00 00 00 00 00 00 00 F0
...
Data Insights
Analyzing this raw data, several key observations emerged:
- Consistent Zeros and Constants: A significant portion of the data consisted of constant zero values or other fixed values, which could be safely ignored for workout metrics.
- Byte and Word Values: The data stream contained a mix of single-byte and multi-byte (word) values, indicating different data types.
- Dynamic and Pedal-Related Values: Some values changed continuously during pedaling, directly reflecting workout intensity, while others remained static or incremented slowly over time. Crucially, some values were zero when not pedaling, clearly indicating active workout metrics.
Based on these insights, I developed code to parse the data into nine distinct fields, differentiating between byte and word values. I then used the console app to collect data during another workout, exporting it to CSV format for analysis in Excel. By examining the trends and patterns in these fields, the meaning of most values became apparent. The most challenging aspect was realizing that the bike reports measurements in imperial units (miles, mph) rather than metric.
Here’s a breakdown of the nine data fields I identified, in order:
- Workout Second: The current second within the minute (0-59), repeating every minute. If data sampling is faster than once per second, this value might remain constant across multiple samples.
- Distance (Hundredths of a Mile): Cumulative distance traveled, measured in hundredths of a mile.
- Workout Time (Seconds): Total workout duration in seconds, starting from zero and continuously incrementing as long as pedaling continues. This value pauses when pedaling stops.
- Speed (Tenths of a Mile per Hour): Instantaneous speed in tenths of a mile per hour.
- Rotations Per Minute (RPM): Cadence, measured in rotations per minute.
- Unknown Byte 1: Appears related to workout speed, but its precise meaning is unclear. When stationary, it increments by one each second and occasionally resets, then continues counting.
- Speed Level (0-9): A quantized speed value, ranging from 0 to 9.
- Unknown Byte 2 & 3: Two additional unknown byte values. Both seem to represent some form of average speed over the entire workout, but their exact calculation remains unclear.
With these data points identified, I concluded that for my custom app, I only needed three core commands: Connect
, Start Workout
, and Continue Workout
. Furthermore, fully understanding the intricacies of all commands wasn’t necessary; simply sending the correct byte sequences in the proper order was sufficient for data acquisition.
Step 4 – Building the Complete Desktop Application
The final phase involved structuring my findings into a reusable API (Application Programming Interface) and developing a user-friendly WPF (Windows Presentation Foundation) application for the desktop interface. WPF’s MVVM (Model-View-ViewModel) pattern was ideally suited for this type of application architecture. For efficient local data storage, I leveraged Entity Framework Core to rapidly implement SQLite database integration for workout data persistence.
Screenshot of the custom under desk bike desktop application displaying workout metrics.
Key Features of the Custom Desktop App:
- Real-time Workout Metrics: Displays workout time, current speed, distance, and RPM during active sessions.
- Workout Averages: Upon workout completion, the display dynamically switches to show average workout metrics.
- Automatic Workout Detection: The app intelligently detects bike activation via Bluetooth (triggered by pedaling), automatically initiating workout tracking and displaying the application window.
- Workout Pausing and Auto-Stop: Workout tracking pauses when pedaling ceases, with the workout time blinking to indicate paused state. If paused for more than one minute, the workout automatically ends.
- Manual Controls: Start and Stop buttons provide manual workout session control.
- Daily Distance Tracking: A daily distance summary is displayed for goal setting and motivation.
- System Tray Integration: Closing the application window minimizes it to the system tray for background operation.
- Context Menu: A system tray icon with a context menu provides quick access to app functions.
- Always on Top Option: A right-click context menu within the application window allows toggling “Always on Top” mode.
- Automatic Startup: Option to configure the application to launch automatically upon system login.
- Workout Data Persistence: Workout summaries and detailed sample data from the bike are saved to a local SQLite database for historical tracking and analysis.
Conclusion: Pedal Your Way to Productivity
The custom desktop application has exceeded my expectations and perfectly fulfills my needs. It seamlessly integrates workout tracking into my workday, providing real-time feedback and data logging without the clunky interface of the original mobile app. Now, the only challenge left is to see just how far I can pedal while working!
Alt text for images:
http://images/header.png
: Conceptual banner image representing an under desk bike workout with data visualization and code elements.http://images/screenshot.png
: Screenshot of the custom-built desktop application interface displaying real-time workout metrics from an under desk bike, including time, speed, distance, and RPM.