Emulating the Peloton (PeloMon, Part II)

Debugging on real hardware is the worst.

TL;DR - How long it takes the Bike to respond to its head unit, building event-driven and hardware timing-accurate emulators of the Peloton Bike to assist in development, and a Peloton decoder library in C.

Third in a series. See the project GitHub, to be updated through the series.

Why Emulation?

In Part I I decoded the signaling that the Peloton Bike and its head unit use to communicate with each other, and found an easy-to-handle serial protocol with three distinct modes of behavior: one-time messages sent at boot, a silent period when no ride is taking place, and a repeating loop of messages sent during a ride. Given that the protocol is straightforward, why bother writing an emulator?

Simply put, testing and debugging on the real Peloton hardware sucks:

For development purposes, then, having an emulator handy really makes the dev cycle more ergonomic both from a programming and literal perspective.

How to build a Peloton emulator

There are two general strategies that I implemented for the emulator:

There are advantages and disadvantages to both architectures:

The event-driven emulator can run on the same board, in the same codebase, with the rest of the PeloMon and thus functions purely in software — only one thing to compile and upload. However, it comes with limitations: because it’s on the same board, it takes up part of the limited code space available; because it’s software-only, it will not test the communications logic; and because it’s all running on the same microcontroller, it’s very difficult to get it to be timing-accurate, and logic in the emulator and in the main listener will impact each others’ timing. (Strictly speaking, one could make better use of the timer interrupts to get at better timing, but that’s still quite tricky to interleave with the rest of the code…and I didn’t feel like writing a general preemptive multitasking system for the ATMega.)

A hardware timing-accurate emulator has two obvious pros: it gets the timing true to the real Peloton, and it tests more of the system. Beyond that, when run on a separate board, it also frees up code space for the main PeloMon and means there’s no timing interference beyond what you would see in real usage. However, it’s not without its own tradeoffs: you now need a second board (which means more wires); it’s tricky to debug multi-board timing without a logic analyzer; and it’s still not going to be 100% accurate to the real thing — in particular because we’re driving normal logic levels with the micro rather than negative voltage signals with a UART. (You could add a UART too, but that’s just getting to be too much!)

Peloton timing

Of course, in order to build a timing-accurate emulator, one needs to know what the timing should be.

The timing between messages is easy, and fixed. During the bootup sequence (the 0xFE unknown message, 0xF5 bike ID request, and the 0xF7xx resistance table requests), the head unit sends one request very consistently every 200ms. During a ride, the request rate switches to once per 100ms, with cadence, power output, and resistance requests in round robin.

The latency between the HU’s request and the bike’s response is less obvious, but can be measured using a logic analyzer. PulseView offers the ability to export its annotation tracks (from the UART decoder) with timestamps, allowing me to look at the timing from the end of the HU’s last stop bit in a request to the start of the bike’s first start bit.

Request-response latency on the Peloton

Top: Request-response latency on bootup sequence
Center: Request-response latency over the course of a ride
Bottom: Histogram of latency during a ride by request type

The figure above shows the pattern of request/response latencies, in microseconds, during the bike’s bootup pattern and during a ride. My initial hypothesis was that some requests might have different latency for responses if the HU request triggered a physical measurement versus reading values already in memory. However, the figure above shows that this likely isn’t the case: there are no discernible patterns in the bootup sequence, and during the ride, response latencies for cadence, output, and resistance all appear to be uniformly distributed from 200 to 2700 microseconds, independently of request type. (Though there is a short tail out to 3 milliseconds.) While it’s possible that the bike controller is taking measurements only on request, the uniformity of the latency across request types suggests that either the measurements are extremely fast or that it measures in the background outside the request-response cycle. Bootup responses seem somewhat faster, at under 1 millisecond.

These latency distributions have been integrated into the hardware emulator.

Source Code Release

Peloton emulator source code

In the emulators section of the PeloMon repository, I’ve added code for both the software event-driven and hardware timing-accurate emulators. The hardware emulator is intended to be flashed to an Arduino Uno or similar, but there are instructions in the sketch describing how to modify parameters and usage for a 3.3V board or alternative device.

The software emulator includes both the emulator itself as well as a small chunk of code to parse Peloton messages from the head unit or the bike. Like the rest of the code, this is written in AVR C but could be trivially ported to other languages.

Both emulators have READMEs with additional documentation, so rather than take up more space here, I’ll leave you with a picture of debugging with the hardware emulator:

PeloMon and hardware emulator

Questions or comments? Drop me a line on Twitter and tag it #pelomon!