How i pwn3d my sp1n b1k3 part1 the HW:
I bought a used Life Fitness / ICG IC6 because I liked the bike (and the price), but I didn't like how closed the console was.
Before opening anything I did what I usually do with unfamiliar hardware: I searched for every useful piece of information I could find. Service and maintenance manuals, FCC filings, regulatory documents, photos, old product pages and anything else that could help me understand what was hiding between the pedals and the display.
By the time I opened my own console I already had a rough picture of the system. The teardown was not a random fishing expedition anymore. I knew what I was looking at and what questions I still needed to answer.
Inside the console
The console is built around a TI AM335x, the same processor family used by the BeagleBone Black, and it boots from an SD card.
That was good news. It meant this was not some completely mysterious one-off computer. It was an embedded Linux system built around hardware with plenty of public documentation.
The bigger question was still outside the console: where were cadence, resistance and the rest of the bike data actually coming from?
Following the four wires
The bike connects to the console through only four populated wires, so that became the next place to look.
The IC6 is self powered. Turning the pedals wakes the electronics and brings up the power going to the console. For bench measurements I simply had to keep the bike awake by turning the pedals when needed.
Probing the four wires quickly separated them into two groups. One line was the bike-side power supply, one was ground, and the remaining two behaved like a digital two-wire bus.
Both communication lines sat close to 5 V when idle. That looked very much like an I2C bus with pull-ups.
I did not need to spend much time worrying about which of the two signal wires was SDA and which was SCL. There were only two possibilities. If they were crossed there was no valid I2C communication; swap them and the bus worked.
At this point the picture was much simpler than when I started:
pedaling
|
v
bike electronics wake up
|
+---- power ----> console
|
+---- I2C -----> console
Using a Leonardo as the bridge
I wanted a simple way to talk to the bus without involving the original console, so I used the Arduino Leonardo you can see in the opening photo.
The Leonardo was a convenient choice because the ATmega32U4 has native USB and the board uses 5 V logic. I powered the Leonardo from the computer over USB and connected only ground, SDA and SCL to the bike. I did not connect the Leonardo 5 V output to the bike power line.
I also disconnected the original console while doing this. I wanted one master talking to the bike, not two devices trying to control the same I2C bus.
The Leonardo can be programmed as a USB to I2C adapter. One useful project for the ATmega32U4 is I2C-Mega-USB, which lets Linux see the board as an I2C adapter.
That made the next step pleasantly boring.
i2cdetect -l
lists the I2C adapters that Linux can see. After finding the bus number assigned to the Leonardo, I could scan it with:
i2cdetect -y BUS_NUMBER
The bike answered at two addresses:
0x40
0x48
That was the point where the four mystery wires stopped being mystery wires. I now had a powered digital bus, two responding devices and a normal computer interface I could experiment with.
A very small scanner
A full Linux USB-I2C bridge is convenient, but it is not required just to see which addresses answer. The Leonardo can do that by itself and print the result over its normal USB serial connection.
This is a minimal version of that test. It scans once at 50 kHz and reports only the addresses that acknowledge.
#include <Wire.h>
void setup() {
Serial.begin(115200);
unsigned long start = millis();
while (!Serial && millis() - start < 3000) {
}
Wire.begin();
Wire.setClock(50000);
Serial.println(F("IC6 I2C scan"));
for (uint8_t address = 0x08; address <= 0x77; address++) {
Wire.beginTransmission(address);
uint8_t result = Wire.endTransmission();
if (result == 0) {
Serial.print(F("device at 0x"));
if (address < 0x10) {
Serial.print('0');
}
Serial.println(address, HEX);
}
}
Serial.println(F("done"));
}
void loop() {
}
On this bike the interesting result is:
device at 0x40
device at 0x48
From addresses to useful bike data
Finding 0x40 and 0x48 did not magically tell me what every byte meant. It just gave me two doors to knock on.
From there I could start making controlled reads, change one thing on the bike at a time and watch what changed. Pedal faster. Pedal slower. Move the resistance knob. Let the bike stop. Wake it again. Compare the responses.
That is how the useful pieces started separating from each other: cadence, brake position, power-module state and the other information needed by a replacement console.
I kept the early work read-only wherever possible. There is no reason to start writing calibration or service values while you are still learning what the normal ride data looks like.
The part that belongs on GitHub
This post is the story of how I got from a closed console to a usable bike interface. I do not want to maintain another copy of the complete protocol here.
The current wiring, packet documentation and example readers live in the GitHub repository:
Life Fitness / ICG IC6 I2C interface
That includes the wiring reference, the protocol notes and the current examples.
The original goal has not changed much. I wanted the good mechanical part of the IC6 without being locked into the original console. Once the bike data is available through a simple interface, the display can be an Arduino experiment, an ESP32, a Raspberry Pi, a Jetson or something that does not exist yet.
This is an independent interoperability project and is not affiliated with, authorized by, sponsored by or endorsed by Life Fitness or Indoor Cycling Group. Product and company names are used only to identify compatible hardware.
No comments:
Post a Comment