Like many others navigating the work-from-home era, the shift in routine significantly cut down on my usual physical activity. To combat this and incorporate more movement into my day, I decided to try an Under Desk Bike. I opted for a model that seemed promising: quiet operation, adjustable resistance, stable design, and crucially, Bluetooth connectivity for workout data tracking.
If you’re thinking about getting an under desk bike to boost your activity levels while working, they can be a good option. However, it’s worth noting that with a standard height desk, you might find your knees hitting the underside. Fortunately, my desk setup, which includes a higher chair and footrest – a configuration I use to avoid a keyboard tray – provided the perfect clearance for comfortable pedaling.
The Bluetooth feature was a key selling point for me. Having real-time data on speed and distance is a great motivator. The bike came with its own smartphone app, but it wasn’t ideal. The app felt clunky, had poor user reviews, required account creation, and aggressively pushed premium subscriptions. While these issues were minor annoyances, my main problem was the inconvenience of constantly reaching for my phone and navigating menus every time I wanted a quick workout at my desk. Leaving my phone screen on for extended periods wasn’t appealing either, and the thought of my workout data being locked into a subpar app was frustrating.
There had to be a better way. The solution became clear: I needed to create my own desktop application to track my under desk bike workouts exactly how I wanted.
The Project: Building a Bespoke Workout Dashboard
Inspired by a fascinating article about rescuing a high-end bike with a Raspberry Pi, I suspected my under desk bike might operate on similar principles. If I could tap into its Bluetooth communication from my computer, I could develop a tailored application to display and log my workout data directly on my desktop.
Objectives
- Real-time Workout Data Display: Create a small, unobtrusive window on my desktop to show live workout metrics.
- Workout Data Logging: Record workout data into a local SQLite database for performance analysis, goal setting, and to keep myself motivated.
- Automatic Workout Detection: Ideally, the app should automatically start and stop tracking based on pedaling activity.
Challenges
- Desktop Bluetooth Deficiency: My desktop computer didn’t have 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 challenge would be the core learning experience of this project.
Step 1: Decoding the Bluetooth Mystery
My first step was to dive into research. This led me to an invaluable tool: nRF Connect. This Android application is a Bluetooth Swiss Army knife, allowing you to scan for nearby Bluetooth devices, connect to them, explore their services, and monitor data transmission. Using nRF Connect, I quickly discovered that my under desk bike was using a Nordic UART Service (NUS). This was excellent news, suggesting the bike was essentially streaming workout data as a simple serial stream over Bluetooth.
However, simply connecting with nRF Connect and subscribing to data changes didn’t immediately yield workout information. While the connection was established, no data was being transmitted. This meant more investigation was needed to understand the communication protocol.
To get to the bottom of this, I needed to capture and analyze the raw Bluetooth communication between the official app and the bike. After some online searching, I found a detailed guide on enabling Bluetooth Host Controller Interface (HCI) logging on Android. I followed these steps to enable logging, started screen recording on my phone, performed a few workouts using the standard app, and then downloaded the Bluetooth logs for analysis.
Using Wireshark, a powerful network protocol analyzer, and cross-referencing the logs with my screen recordings, I was able to decipher the communication pattern. I observed that the app initiates communication by sending command packets to the bike, and the bike responds with data packets. During a workout, the app repeatedly sends a specific command, prompting the bike to send back workout data.
Step 2: Establishing a Connection from Desktop
From the Wireshark analysis, I identified six distinct command packets sent by the official app. My next goal was to create a basic console application to mimic this communication from my desktop. This would involve connecting to the under desk bike, sending these identified commands, and listening for responses, effectively testing if I could replicate the app’s behavior on my computer.
For this project, I chose .NET 5.0 on Windows because it provides seamless access to the Windows Runtime Bluetooth API. Integrating this API into a .NET application is surprisingly straightforward, making it an efficient choice for Bluetooth interaction on Windows.
Step 3: Unpacking the Workout Data
Through analyzing the Bluetooth logs, I started to understand the command structure. The first command, which I labeled “Connect,” was crucial and needed to be sent before any other commands. After establishing the initial connection with this command, I noticed that subsequent commands had to be sent at least once per second to maintain the Bluetooth connection; otherwise, the bike would disconnect.
Start Command: f9 d0 00 c9 Response: f9 e0 00 d9
The next command, which I called “Hold,” seemed to act as a keep-alive signal to maintain the connection. Each time this command was sent, the bike responded with two identical packets.
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 commands sent only once, which I named “Info1” and “Info2.” These likely provided static information about the bike, such as model details 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
By this point, a pattern emerged in the command and response structure. Every packet started with the byte F9
. The subsequent byte indicated the command or response type; the higher nibble was D
for commands and E
for responses. The third byte specified the packet length (excluding the header), and the byte after that appeared to be a checksum for data integrity.
The most crucial commands for workout tracking were “Start Workout” and “Continue Workout.” The “Start Workout” command is sent when a workout begins. Then, the app continuously sends the “Continue Workout” command to receive real-time data updates from the under desk bike throughout 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 f0
With the command structure understood, I moved on to interpreting the workout data itself. I concatenated all the data packets from a workout session, removed the headers, and biked for about 10 minutes to collect a substantial dataset.
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
- Data Field Identification: Analyzing the raw data revealed a structure with repeating patterns and fluctuating values. I noticed several constant zero values and a few consistent non-zero values that appeared to be static or less relevant to real-time workout metrics.
- Byte vs. Word Values: It became clear that some data points were represented as single bytes, while others spanned two bytes (words), requiring proper interpretation based on their positions in the data stream.
- Dynamic and Static Values: Some values changed continuously, reflecting real-time workout data like speed and RPM, while others remained constant or changed slowly, possibly representing accumulated workout time or distance. Notably, some values were directly linked to pedaling activity, registering zero when I stopped pedaling.
Based on these initial observations, I developed code to parse the data stream into nine distinct fields, differentiating between byte and word values. I then ran my console application during a workout, capturing these parsed values into a CSV (Comma Separated Values) file. Importing this CSV data into Excel allowed for visual analysis and pattern recognition. Most data fields became readily understandable through this process. The most time-consuming part was realizing that the bike reported distance and speed in imperial units (miles) rather than metric units (kilometers).
Through this analysis, I identified the nine key data fields in the workout data stream in the following order:
- Workout Second: The current second within the minute of the workout, cycling from 0 to 59 repeatedly. If the data sampling rate is faster than once per second, this value might remain constant across multiple samples.
- Distance (hundredths of a mile): The total distance pedaled, measured in hundredths of a mile.
- Workout Time (seconds): The elapsed workout time in seconds, starting from zero and incrementing as long as pedaling continues. Pausing pedaling freezes this value.
- Speed (tenths of a mile per hour): The current speed, measured in tenths of a mile per hour.
- Rotations Per Minute (RPM): The cadence, or pedaling rate, in rotations per minute.
- Unknown (byte): This value seems related to workout intensity or speed. When not pedaling, it increments by one each second and occasionally resets to a specific value before continuing to count up.
- Speed Value (0-9): Another speed-related value, ranging from 0 to 9, possibly representing resistance level or speed zone.
- Unknown Average Value 1 (byte): Appears to be an average speed-related value calculated over the entire workout, but its precise meaning is unclear.
- Unknown Average Value 2 (byte): Similar to the previous value, likely another average related to speed over the workout duration, but its exact interpretation remains unknown.
With these insights, I determined that only three commands were essential for my desktop application: “Connect,” “Start Workout,” and “Continue Workout.” Furthermore, a deep understanding of the command functions wasn’t necessary; simply sending the correct byte sequences in the right order was sufficient to extract the workout data I needed.
Step 4: Assembling the Desktop Workout App
The final stage was to structure my reverse engineering work into a well-defined API and build a user-friendly desktop application. I chose WPF (Windows Presentation Foundation) for the user interface development, as its MVVM (Model-View-ViewModel) architecture is ideal for creating maintainable and testable applications. To handle local data storage for workout history, I leveraged Entity Framework Core to quickly set up an SQLite database.
Key Features of the Custom Under Desk Bike App:
- Real-time Workout Metrics: Displays current workout time, speed, distance, and RPM during active pedaling.
- Workout Averages: After a workout, the display switches to show average speed, distance, and RPM for the completed session.
- Automatic Workout Start/Stop: Intelligently detects when the under desk bike’s Bluetooth activates upon pedaling, automatically starting workout tracking and displaying the app window.
- Pause and Auto-End: Pauses the workout timer when pedaling stops. The workout time display blinks during pauses and automatically ends the workout after one minute of inactivity.
- Manual Controls: Provides “Start” and “Stop” buttons for manual workout control, offering flexibility beyond automatic detection.
- Daily Distance Tracking: Shows daily accumulated distance for goal setting and daily progress visualization.
- System Tray Integration: Minimizing the app window sends it to the system tray, keeping it running in the background without cluttering the taskbar.
- System Tray Menu: Includes a system tray icon with a context menu for quick access to app functions and settings.
- Always-on-Top Option: Allows the app window to be set to “Always on Top” via a right-click context menu for persistent visibility.
- Automatic Startup: Option to configure the application to launch automatically upon Windows login for seamless daily use.
- Workout Data Persistence: Saves detailed workout information and all sampled data points from the bike to a local SQLite database for historical tracking and analysis.
Conclusion: Pedal to Progress
The custom desktop workout app has exceeded my expectations. It delivers all the functionalities I envisioned, providing a seamless and personalized workout tracking experience for my under desk bike. Now, the only challenge left is to see just how many miles I can pedal while working!