Exercise bike part 2. Investigating
TLDR:Incline and resistance can be controlled by the user. The console sends messages to the control board using a RS485 protocol. Values are sent using HEX, B0 to B9, C0-C6. Resistance messages are prefaced by BAB6 and Incline by BAB4. Response messages are prefaced by BAB5 and include a cadence value.
The exercise bike has a stout base and the bike sits on a pivot in the centre of this base. There is a motor behind the pivot that extends or retracts a threaded screw, causing the bike to pivot forward 15 degrees or back 15 degrees.
There is a flywheel behind the seat that is composed of inner rings of cast iron or steel, and an outer ring of anodized aluminum. The flywheel is driven by a J profile poly v belt. Tension is maintained on the belt by an idler wheel that is located close to the rear hub. The rear pulley is not fixed, unlike some bikes (like Keisler) but allows you to freewheel and take a break from pedalling.
Resistance is created by powerful magnets which are behind the seatpost. They straddle the aluminum flywheel and are adjusted towards the wheel or away, creating an eddy current in the aluminum flywheel. No physical contact and very quiet. Adjustment of the magnets is performed by buttons on the handlebars that activate a small servo style motor. Slick. The magnet position can be changed by pressing the gear up or down button on the handle bars. The bike has 26 "gears" which seem almost imperceptible but really increase the difficulty. Gear 26 is a struggle when the bike is at zero incline, but at +15degree incline, gear 26 is almost impossible to turn over unless I stand up on the bike.
When you start riding, the bike console will cycle through Time, Distance, Speed, Pace, Watts and RPM.
So how does it all work? Is there a way to control the bike remotely? What about the bluetooth connection? Let's take a closer look...
There are two main interesting areas on the proform bike. The upper console and the lower control board.
The lower control board has the power supply section, a number of inputs, some relays, power outputs to the control motors and a microcontroller. The inputs are the small gauge wires. There are 4 wires that connect to the console (blue/green/black/red), 5 wires that connect to the resistance motor (black/white/red/orange/blue), 3 wires that connect to the incline motor(green/black/red) and 2 to a reed switch on the crank (black/grey). So the lower board seems to be in charge of adjusting the resistance and incline. It also measures pedal revolutions (cadence) with a reed switch triggered by a magnet on the crank. Importantly, it communicates with the upper console with the 4 wires.
The upper console:
The console is the user interface of the bike. It accepts and translates button presses, displays info about the speed, time, distance, watts, etc and communicates with the lower board.
I am interested in what ways the bike could be controlled beyond what is possible with the console and the iFit app.
I tried listening to the bluetooth communications with Wireshark, but there was little to see there. I tried connecting to the bluetooth via the NRF connect app, but the bluetooth services were not accessible. Is the bluetooth secured somehow? Beyond me at this point.
I decided to focus efforts on the four cables that go between the console and the lower control board. Measuring the wires with a multimeter, I determined that the red was +9V, the blue was +3.3V, and the green and black were at ground. So I hooked up my oscilloscope (Hitachi V212) to the wires and pressed some buttons... And there was a signal on both the blue and green wires. But, my oscilloscope is only 20MHz, analogue and can't store traces, so....
To my electronics box -- I tried to use my Bus Pirate as a logic analyzer, but it's resolution and sample size were too low. So I ordered a knock off Logic Analyzer from Amazon and waited....
Once my logic analyzer arrived, I was able to capture some useful data (once I was over the learning curve of Pulseview). So I hooked up my analyzer to the console wires and pressed some gear and incline buttons while riding the bike (important: pedals must turn or the bike doesn't do anything!). The first capture I got showed two data lines. One went HIGH and at the same time the other went LOW. Hmmmm... Quick google search, maybe it is RS48... Capture a few more sessions and analyze just one of the lines...
So, much confusion later, it turns out that the messages are indeed RS485 at a speed of 38400 baud, 8 bits, no parity, 1 stop bit and least significant bit first (LSB). And then we can decode the messages from the lower control board to the console. Mostly a series of messages like this:
BAB6B1B0B6B0B1B0B5B0B0B2C4B6B68D8A
BAB5B1B0B3B0B0B0B2B0B0B0B0C1C18D8A
BAB5B1B0B3B0B2B0B1B0B2B0B0B0B0C1B78D8A
BAB5B1B0B3B0B0B0B2B0B0B0B0C1C18D8A
BAB5B1B0B3B0B2B0B1B0B2B0B0B0B0C1B78D8A
BAB5B1B0B3B0B0B0B2B0B0B0B0C1C18D8A
BAB5B1B0B3B0B2B0B1B0B2B0B0B0B0C1B78D8A
BAB5B1B0B3B0B0B0B2B0B0B0B0C1C18D8A
BAB5B1B0B3B0B2B0B1B0B2B0B0B0B0C1B78D8A
BAB5B1B0B3B0B0B0B2B0B0B0B0C1C18D8A
BAB5B1B0B3B0B2B0B1B0B2B0B0B0B0C1B78D8A
BAB5B1B0B3B0B0B0B2B0B0B0B0C1C18D8A
BAB5B1B0B3B0B2B0B1B0B2B0B0B0B0C1B78D8A
BAB5B1B0B3B0B0B0B2B0B0B0B0C1C18D8A
After a few more captures and decoding the signals I was able to decipher the communication protocol. Messages are sent from the console to the control board twice per second while the bike is being ridden.
To change the incline, the following command would be sent:
0xBA, 0xB4, 0xB1, 0xB0, 0xB6, 0xB0, 0xB0, 0xB0, 0xB0, 0xB1, 0xB0, 0xB1, 0xC3, 0xB9, 0xC3, 0x8D, 0x8A
That would change the incline down one notch to -0.5. The important
bytes are the second (0xB4) which indicates that the command is to
change the incline and 12 to 15 (0xB1, 0xC3, 0xB9, 0xC3) which tell the
console which level to shift to in absolute terms. There are 61
different codes(!) for bytes 12 to 15 corresponding to each half step in
the incline of the bike.
To change the gear (resistance), this command would be sent:
0xBA, 0xB6, 0xB1, 0xB0, 0xB6, 0xB0, 0xB0, 0xB0, 0xB0, 0xB1, 0xB0, 0xB0, 0xB0, 0xC2, 0xB8, 0x8D, 0x8A
This would change the "gear" to #2. Note the important second byte
(0xB6) indicating that a gear change is sent and bytes 12 to 15 indicate
the level to move the resistance servo. I mapped out 22 levels for the
gears, but the bytes are not sequential, unlike the incline levels so I
believe there are many more levels of resistance possible (more on this
later).
0xBA, 0xB5 prefaced messages are from the control board responding to the incline or resistance change or sending info about the current cadence of the rider.
Pretty simple. But with caveats... There are only certain
values that are used by the console and control board for the incline and resistance and cadence data --
B0-B9, C0-C6. I don't know what base that is, but okay, we'll deal with
it.
With this knowledge, I have created a small Arduino circuit and sketch
which is capable of controlling the incline and gears of the bike.
<code>
int incline = 30; //(levels from -15 to +15 by half increments. Index for zero is 30)
int gear = 0;
//these are the bytes for gear changes.
//bytes 12 to 15. There could be more levels in between these, note the pattern of bytes for incline
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22
byte gearCommand1[] ={0xB0, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB4, 0xB4, 0xB4, 0xB4};
byte gearCommand2[] ={0xC6, 0xB1, 0xB4, 0xB7, 0xC1, 0xC4, 0xB0, 0xB3, 0xB6, 0xB8, 0xC2, 0xC5, 0xB1, 0xB4, 0xB7, 0xC1, 0xC4, 0xC6, 0xB2, 0xB5, 0xB8, 0xC2};
byte gearCommand3[] ={0xB8, 0xB8, 0xB8, 0xB7, 0xB7, 0xB7, 0xB7, 0xB7, 0xB6, 0xB6, 0xB6, 0xB6, 0xB6, 0xB6, 0xB5, 0xB5, 0xB5, 0xB5, 0xB5, 0xB4, 0xB4, 0xB4};
byte gearCommand4[] ={0xB5, 0xB3, 0xB0, 0xC4, 0xC1, 0xB7, 0xB4, 0xB1, 0xC5, 0xC3, 0xB9, 0xB6, 0xB3, 0xB0, 0xC4, 0xC1, 0xB7, 0xB5, 0xB2, 0xC6, 0xC3, 0xB9};
//these are the bytes for incline changes
//note that "0" is number 31 below and changes are 0.5 degree increments on the bike
// 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61
byte inclineCommand1[] = {0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB0, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB1, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB2, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3, 0xB3};
byte inclineCommand2[] = {0xC6, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5, 0xB6, 0xB7, 0xB8, 0xB9, 0xC1, 0xC2}; //what are the actual codes?...
byte inclineCommand3[] = {0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC2, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xC1, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB9, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB8, 0xB7, 0xB7, 0xB7};
byte inclineCommand4[] = {0xB9, 0xB8, 0xB7, 0xB6, 0xB5, 0xB4, 0xB3, 0xB2, 0xB1, 0xB0, 0xC6, 0xC5, 0xC4, 0xC3, 0xC2, 0xC1, 0xB9, 0xB8, 0xB7, 0xB6, 0xB5, 0xB4, 0xB3, 0xB2, 0xB1, 0xB0, 0xC6, 0xC5, 0xC4, 0xC3, 0xC2, 0xC1, 0xB9, 0xB8, 0xB7, 0xB6, 0xB5, 0xB4, 0xB3, 0xB2, 0xB1, 0xB0, 0xC6, 0xC5, 0xC4, 0xC3, 0xC2, 0xC1, 0xB9, 0xB8, 0xB7, 0xB6, 0xB5, 0xB4, 0xB3, 0xB2, 0xB1, 0xB0, 0xC6, 0xC5, 0xC4};
byte inclineDownButton = 10;
byte inclineUpButton = 11;
byte gearDownButton = 9;
byte gearUpButton =8;
void setup() {
// put your setup code here, to run once:
Serial.begin(38400);
pinMode(inclineDownButton, INPUT_PULLUP); //
pinMode(inclineUpButton, INPUT_PULLUP); //
pinMode(gearDownButton, INPUT_PULLUP); //
pinMode(gearUpButton, INPUT_PULLUP); //
}
void loop() {
bool gearDown = digitalRead(gearDownButton);
bool gearUp = digitalRead(gearUpButton);
bool inclineDown = digitalRead(inclineDownButton);
bool inclineUp = digitalRead(inclineUpButton);
if (gearDown==LOW){
gear = gear - 1;
if (gear < 0){ gear = 0;}
Serial.write(0xBA);
Serial.write(0xB6);
Serial.write(0xB1);
Serial.write(0xB0);
Serial.write(0xB6);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(0xB5);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(gearCommand1[gear]);
Serial.write(gearCommand2[gear]);
Serial.write(gearCommand3[gear]);
Serial.write(gearCommand4[gear]);
Serial.write(0x8D);
Serial.write(0x8A);
delay(500);
}
if (gearUp==LOW){
gear = gear + 1;
if (gear > 21){ gear = 21;}
Serial.write(0xBA);
Serial.write(0xB6);
Serial.write(0xB1);
Serial.write(0xB0);
Serial.write(0xB6);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(0xB5);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(gearCommand1[gear]);
Serial.write(gearCommand2[gear]);
Serial.write(gearCommand3[gear]);
Serial.write(gearCommand4[gear]);
Serial.write(0x8D);
Serial.write(0x8A);
delay(500);
}
if (inclineDown==LOW){
incline = incline - 1;
if (incline < 0){ incline = 0;}
Serial.write(0xBA);
Serial.write(0xB4);
Serial.write(0xB1);
Serial.write(0xB0);
Serial.write(0xB6);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(0xB1);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(inclineCommand1[incline]);
Serial.write(inclineCommand2[incline]);
Serial.write(inclineCommand3[incline]);
Serial.write(inclineCommand4[incline]);
Serial.write(0x8D);
Serial.write(0x8A);
delay(500);
}
if (inclineUp==LOW){
incline = incline + 1;
if (incline > 60){ incline = 60;} //max 60 entries in the incline array
Serial.write(0xBA);
Serial.write(0xB4);
Serial.write(0xB1);
Serial.write(0xB0);
Serial.write(0xB6);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(0xB1);
Serial.write(0xB0);
Serial.write(0xB0);
Serial.write(inclineCommand1[incline]);
Serial.write(inclineCommand2[incline]);
Serial.write(inclineCommand3[incline]);
Serial.write(inclineCommand4[incline]);
Serial.write(0x8D);
Serial.write(0x8A);
delay(500);
}
}
</code>


