Introduction: Open (Bicycle) Grade Simulator - OpenGradeSIM


A certain well known US based fitness company (Wahoo) recently brought out a great indoor training aid that raises and lowers the front of the bike on the turbo trainer according to the simulated grade of hill that the user is riding (the Kickr Climb).

Looks amazing but sadly this is not available to us all as you will need 1) a top of the range Wahoo trainer and 2) £500 cash to make this yours.

I broke a clavicle (never put a road cyclist on a mountain bike) so I had more miles on the trainer and more time to tinker and thought this could be a fun project.

The commercial unit simulates -5% to +20% so I wanted to come close to that but on 10% of the budget!

This is designed around my Tacx Neo but any trainer that broadcasts its power and speed data via ANT+ or BLE could be made to work (I reckon!).

Since the wheel base on my road bike measures exactly 1000mm I'd need to lift the forks by 200mm to simulate 20% (see pic) so a 200mm linear actuator would do. The bike + rider weight is unlikely to exceed 100kg and since this is distributed between the axles and most is on the back 750N will lift 75kg and should be ok. Faster actuators are available for more money but this one cost me around £20 and manages 10mm/sec. Actuators with potentiometers that can be used as simple servos are also 2 to 3 times more expensive.


3D print (PLA or ABSetc) of the through axle adapter part:

100mm of 3/4 inch 10 swg aluminium tube stock (for a through axle frame)

80mm of 6mm stainless steel bar stock

3D print (PLA or ABSetc) of the shoe for the linear actuator part:

3D print of the Case for the H-bridge

3D print of the Case for Arduino (Version 1 with keypad) (Version 2 as shown (

Laser cut piece of 3mm clear acrylic 32 x 38mm to keep you from sweating all over the electronics (did that, not ideal).

Some bleeding blocks (adapted to leave the pads in) to prevent you accidentally pushing the calliper pistons out of your Shimano disc brakes in your enthusiasm

Linear Actuator 750N 200mm travel eg Al03 Mini Linear Actuators from

L298N H bridge (like:

Arduino Nano IoT 33 order 73-4863

2 key membrane keyboard eg

IIC I2C Logic Level Converter Bi-Directional Module 5V to 3.3V For Arduino eg

12V 3A DC power supply - the ones for LED lighting work great!

NPE CABLE Ant+ to BLE bridge

3D printable clip for the CABLE bridge

1.3" OLED LCD Display Module with IIC I2C Interface 128x32 3.3V

Step 1: Some Mathematics

We need to calculate the incline being simulated. I had hoped that the trainer would advertise this data along with speed, power, cadence etc. however the trainer simply sets resistance to maintain power output according to the software on the tablet, computer etc being used to control it. I had no way to easily capture the 'simulated grade' from the software so I would have to work backwards...

The forces acting on the bike and rider are a combination of resistive losses and the power needed to climb the hill. The trainer reports speed and power. If we can find the resistive losses at a given speed then the remaining power is being used to climb the hill. The power to climb depends on the weight of the bike and rider and the rate of ascent and so we can work back to the incline.

First I used the amazing to find some data points for resistive power loss at typical speeds. Then I transformed the speed domain to produce a linear relationship and found the best fit line. Taking the equation of the line we can now calculate power (W) from resistance = (0.0102*(Speedkmh^2.8))+9.428.

Take the power from resistance from the measured power to give power of 'climbing'.

We know the speed of ascent in km/hr and convert this to SI units of m/s (divide by 3.6).

Incline is found from: Incline (%) =((PowerClimbing/(WeightKg*g))/Speed)*100

where acceleration of free fall g = 9.8m/s/s or 9.8 N/kg

Step 2: Get Some Data

The incline calculation requires speed and power. I used an Arduino Nano 33 IoT to connect to the trainer via BLE to receive this. I got very stuck initially as the current v.1.1.2 version of the native ArduinoBLE library for this module does not handle authentication in any form which means most(?) commercial BLE sensors will not pair with it.

The solution was to use an NPE Cable ANT+ to BLE bridge ( which keeps the built in BLE of the trainer free for the training app to communicate over and requires no authentication on the BLE side.

The BLE power characteristic is pretty straightforward as power in watts is contained in the second and third bytes of the transmitted data as a 16 bit integer (little endian ie least significant octet first) . I applied a moving average filter to give 3s average power - just like my bike computer shows - as this is less erratic.

if (powerCharacteristic.valueUpdated()) {

// Define an array for the value

uint8_t holdpowervalues[6] = {0,0,0,0,0,0} ;

// Read value into array

powerCharacteristic.readValue(holdpowervalues, 6);

// Power is returned as watts in location 2 and 3 (loc 0 and 1 is 8 bit flags)

byte rawpowerValue2 = holdpowervalues[2];       // power least sig byte in HEX

byte rawpowerValue3 = holdpowervalues[3];       // power most sig byte in HEX

long rawpowerTotal = (rawpowerValue2 + (rawpowerValue3 * 256));

 // Use moving average filter to give '3s power'

powerTrainer = movingAverageFilter_power.process(rawpowerTotal);

The BLE speed characteristic (Cycling Speed and Cadence) is one of those things that makes you wonder what on earth the SIG was smoking when they wrote the specification.

The Characteristic returns a 16 byte array that contains neither speed nor cadence. Instead you get wheel revolutions and crank revolutions (totals) and time since last event data in 1024ths of a second. So more maths then. Oh, and the bytes are not always present so there is a flag byte at the start. Oh, and the bytes are little endian HEX so you need to read backwards multiplying the second byte by 256, third by 65536 etc. then adding them together. To find speed you need to assume a standard bike wheel circumference to know distance....

if (speedCharacteristic.valueUpdated()) {

//  This value needs a 16 byte array

      uint8_t holdvalues[16] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} ;

//  But I'm only going to read the first 7

      speedCharacteristic.readValue(holdvalues, 7);

byte rawValue0 = holdvalues[0];      // binary flags 8 bit int
byte rawValue1 = holdvalues[1];      // revolutions least significant byte in HEX   
byte rawValue2 = holdvalues[2];      // revolutions next most significant byte in HEX
byte rawValue3 = holdvalues[3];      // revolutions next most significant byte in HEX
byte rawValue4 = holdvalues[4];      // revolutions most significant byte in HEX
byte rawValue5 = holdvalues[5];      // time since last wheel event least sig byte 
byte rawValue6 = holdvalues[6];      // time since last wheel event most sig byte

      if (firstData) {

// Get cumulative wheel revolutions as little endian hex in loc 2,3 and 4 (least significant octet first)

WheelRevs1 = (rawValue1 + (rawValue2 * 256) + (rawValue3 * 65536) + (rawValue4 * 16777216));

// Get time since last wheel event in 1024ths of a second

Time_1 = (rawValue5 + (rawValue6 * 256));

        firstData = false;

      } else {
// Get second set of data         

long WheelRevsTemp = (rawValue1 + (rawValue2 * 256) + (rawValue3 * 65536) + (rawValue4 * 16777216));

long TimeTemp = (rawValue5 + (rawValue6 * 256));

if (WheelRevsTemp > WheelRevs1) {           // make sure the bicycle is moving

WheelRevs2 = WheelRevsTemp;

Time_2 = TimeTemp;

firstData = true;}
// Find distance difference in cm and convert to km          <br><br>float distanceTravelled = ((WheelRevs2 - WheelRevs1) * wheelCircCM);
          <br>float kmTravelled = distanceTravelled / 1000000;
 // Find time in 1024ths of a second and convert to hours
          <br>float timeDifference = (Time_2 - Time_1);
          <br>float timeSecs = timeDifference / 1024;
          <br>float timeHrs = timeSecs / 3600;
// Find speed kmh
          <br>speedKMH = (kmTravelled / timeHrs);

The Arduino sketch is hosted at GitHub (

Step 3: Hardware 1 the Linear Actuator

The through axle on my disc brake road bike specifies a 19.2mm axel to clear the 12mm through axle with 100mm between the forks.

Stock 3/4 inch 10swg aluminium tube is a perfect fit and a nice chap called Dave on ebay ( supplied and cut it to length for me for a few pounds.

The actuator has a 20mm bar with a 6mm hole so the 3D printed part links the aluminium tube to a 6mm steel bar and since the forces are 90% compression some PLA / ABS is up to the challenge.

If you run a standard quick release setup then something like this ( would avoid having to redesign this component.

The boot is designed to fit into the raiser block supplied with my Tacx trainer but would probably fit into many similar raisers or you can just edit the TinkerCad file to suit your requirement.

Step 4: Hardware 2 - the H-Bridge

These L298N H bridge board that are very common online have a built in 5V regulator which is great for powering the Arduino from the 12V power supply required for the linear actuator. Unfortunately the Arduino Nano IoT board is 3.3V signalling hence the need for a logical level convertor (or an optoisolator since the signals are unidirectional only).

The case is designed to accept the power connectors commonly used in LED lighting applications. I butchered a USB extension lead to make it possible to connect / disconnect the Arduino head unit easily and whilst I was sure to use the power lines for power and the data lines for the 3.3V signalling I would honestly advise AGAINST this as I'd hate someone to fry their USB ports or peripherals by plugging them in by mistake!

Step 5: Hardware 3 the Control Electronics (Arduino)

The case for the Arduino OLED and logic level converter has a standard 1/2 turn Garmin style mount on the back to allow it to be mounted securely to the bike. An 'out front' mount will allow the unit to be tilted up or down to 'zero' the accelerometer position or a line of code just to auto zero at the start would be easy to add.

The case has a spot for a membrane keypad - this is used to set the combined rider and bike weight. You can just set this programmatically especially if you don't share a trainer with anyone.

It might be nice to implement a 'manual' mode. Perhaps pressing both buttons could initiate a manual mode and then the buttons could increase / decrease incline. I'll add this to the to-do list!

The STL file of the case is, again, available on Thingiverse (see the supplies section for link).

The Arduino sketch is hosted at GitHub (

You can print a neat little clip for your CABLE bridge from here

Step 6: 'The Rear Drop Outs'

Many people have raised the issue of the rear drop out rubbing as the bike moves. Some trainers have an axle that moves (like the Kickr) but many do not.

Currently my best solution for me is to mount some standard 61800-2RS deep groove bearings (about £2 each) on the quick release adapters and then mount the through axel drop outs on these (see pics) with an over size QR skewer

The bearings need a thin shim washer eg M12 16mm 0.3mm between the adapter and the bearing.

They fit perfectly and rotate with the bike and the skewer independently of the trainer.

At the moment this changes the offset on the drive side by a couple of mm so you'll need to re-index

I am designing custom parts (see pdf plan) to machine (on my future brother-in-law's lathe when he has an hour to help!). These are not tested yet!!! But grinding 1mm off the inner surface of the stock drive side QR adapter is a quick fix with no special tools ;)

Made with Math Contest

Participated in the
Made with Math Contest