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

Teacher Notes

Teachers! Did you use this instructable in your classroom?
Add a Teacher Note to share how you incorporated it into your lesson.

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

Be the First to Share


    • Instrument Contest

      Instrument Contest
    • Make it Glow Contest

      Make it Glow Contest
    • STEM Contest

      STEM Contest

    10 Discussions


    1 day ago

    Nice project, I worked on something just like this several years ago, with a larger actuator to simulate downhills too, and an old AP2 module conected to an arduino Nano (and a logic converter from 5 to 3.3v!!). Same motor driver. As @fragasaurus says, I recommend to speak ANT directly, but instead of an AP2, use a nRF52 module. You won't even need an arduino.
    Although I'm not 100% sure if every manufacturer implements the protocol in this way, I remember from when I implemented the FE-C protocol in one product I used to work for, there is a common page in ANT, number 70 (Request data page) that allows to ask for a specific data page in the trainer.
    In my trainer implementation, if you asked for data page 51 (Track resistance), the trainer would respond with the values for slope and coefficient of rolling resistance that were being used which would give you exactly what you need without having to guess.
    You can try quite easily if your Tacx Neo supports that with SimulANT, simulating a Fitness Equipment Display or with ANTwareII if you prefer.
    Another approach you can try is connecting over FTMS BLE protocol with your device while the trainer is connected over ANT with the simulation software and see if you can read the characteristic that holds the incline value from there.

    1 reply

    Reply 10 hours ago

    This is Gold dust! I’m really grateful for the info - I’m sure this will help someone who is looking to build something similar. What a great community we have here.


    1 day ago

    Nice implementation. I've been working on something similar but am trying to speak ANT+ with the use of the nRF24AP2 chip instead of converting it to BLE. From a cost perspective it should be quite a bit less because it wouldn't require the CABLE component. Was there a particular reason you need the CABLE in the middle or was it just a faster way to get it going? Again, great job.

    3 replies

    Reply 1 day ago

    Hi, thanks. I looked for a well documented ANT+ arduino solution but the only options involved soldering to smd modules so I was a bit skeptical. Would be much neater and cheaper though. I’ll watch with interest for you solution! I never intended to use the CABLE bridge but it solved my authentication problem without having to abandon the Nano 33 module. This instructable is partly in the hope of helping people avoid the dead ends I found myself in! Have you found direct data for Incline on the trainer? I know ANT+ supports it but I suspect, like with BLE, it is seldom implemented.


    Reply 1 day ago

    I haven't found direct incline data but the project is something that was shelved and I'm getting back to. I will post once I have more info. Thanks again.


    Reply 1 day ago

    FTMS is the new bluetooth service to control the trainer and new devices like yours.


    2 days ago

    Great article. I was wondering if you simulate negative gradients / descents? The actuator you specified only has 200mm of travel which would suggest it only does 20% up and doesn't have any travel for the negative.


    2 days ago

    I´m working on it and I think your project is a good start.


    2 days ago

    I have a “dumb” trainer with speed, cadence and power sensors, but I don’t have the resistance part. Could I simulate the resistance in my trainer with your projet and a stepper motor and some magnets??? Nice job!!!

    1 reply

    Reply 2 days ago

    I have an old 'exercise bike' which uses permanent neodymium magnets at varying distance from a spinning steel flywheel. This creates resistance by inducing eddy currents in the flywheel and very silent and smooth it is too. I was wondering about replacing the fixed magnets with a nice big electromagnet like you'd use to hold a door and then varying the power via PWM and an big old MOSFET. The permanent magnets and stepper sound good too!

    The great thing about your set up is that you already have speed and power sensors so once you can control the resistance you can run in 'ERG' mode or isokinetic and the arduino would be a great platform for that.

    The really amazing project imho would be an open source smart trainer that could also take commands from eg ZWIFT by advertising the TPMS service over BlueTooth Low energy.

    Top tip - unless and until the ArduinoBLE library supports authentication I strongly suggest that you work on the ESP32 platform as the library for those will work with commercial sensors and the Nano BLE just doesn't (see my discussions on GitHub with Sandeep Mistry). I wasted several evenings on this assuming that I was doing something wrong.

    Can't wait to see your instructable!