DIY Indoor Bike Smart Trainer

Introduction: DIY Indoor Bike Smart Trainer

About: I use a lot of code from examples and projects online, so if I make something that could save others some time, I share it.


This project started as a simple modification to a Schwinn IC Elite indoor bike which uses a simple screw and felt pads for the resistance settings. The problem I wanted to solve was that the pitch of the screw was to large, so the range from not being able to pedal to the wheel spinning completely free was just a couple degrees on the resistance knob. At first I changed the screw to M6, but then I would have to make a knob, so why not just use a left over NEMA 17 stepper mottor to change the resistance? If there is already some electronics, why not add a crank power meter and a bluetooth connection to a computer to make a smart trainer?

This proved more dificult than expected, because there were no examples on how to emulate a power meter with an arduino and bluetooth. I ended up spending around 20h on programming and interpreting the BLE GATT specifications. I hope that by providing an example I can help someone not waste this much time on trying to understand what exactly "Service Data AD Type Field" means...


The whole project is on GitHub:

I highly recommend using Visual Studio with a VisualGDB plugin if you plan on doing something more serious than just copy-pasting my code.

If you have questions about the program, please ask, I know that my minimalistic comments may not help much.


Thanks to stoppi71 for his guide on how to make a power meter. I did the crank according to his design.


The materials for this project highly depend on what bike are you are modifying, but there are some universal parts.


  1. ESP32 Module
  2. HX711 Weight sensor ADC
  3. Strain gauges
  4. MPU - gyroscope
  5. A small Li-Po battery (around 750mAh)
  6. Heat shrink sleeve
  7. A4988 Stepper driver
  8. 5V regulator
  9. An arduino barrel jack
  10. 12V arduino power supply


  1. NEMA 17 stepper (needs to be quite powerful, >0.4Nm)
  2. M6 rod
  3. 12864 lcd
  4. WeMos LOLIN32
  5. Tact switches


For doing this you probably could get away with using only a 3D printer, however you can save a lot of time by laser cutting the case, and you also can make PCBs. The DXF and gerber files are on GitHub, so you can order those locally. The coupler from the threaded rod to the motor was turned on a lathe and this might be the only problem, as the part needs to be quite strong to pull on the pads, but there is not a lot of space in this particular bike.

Since making the first bike, I acquired a milling machine which allows me to make slots for the sensors in the crank. It makes gluing them a bit easier and also protects them if something were to hit the crank. (I've had these sensors fall of a few times so I wanted to be safe.)

Step 1: The Crank:

It's best to just follow this tutorial:

You basically need to glue the sensors to the crank in four places and connect those to the sides of the board.

The proper connections are already there so you just have to solder the pairs of wires directly to these eight pads on the board.

To connect to the sensors use the thinnest wire possible - the pads are very easy to lift. You need to glue the sensors first and leave just enough of them outside to solder, then cover the rest with epoxy. If you try to solder before gluing, they curl and break.

To assemble the PCB:

  1. Insert goldpins from the bottom (the side with traces) into all holes except those vertical near the bottom.
  2. Place the three boards (ESP32 on top, then MPU, the HX711 on the bottom) so the goldpins stick through both holes.
  3. Solder the headers to the boards on top
  4. Cut off the goldpins from the bottom. (Try cutting them first before assembly, so you know your "goldpins" aren't steel inside - it makes them nearly impossible to cut and you need to file or grind them)
  5. solder the remaining goldpins to the bottom of the board.
  6. Upload the firmware for the crank

The last step is to pack the whole crank with heat shrink sleeve.

This method of making the board isn't ideal, as the boards take up a lot of space in which you could fit other things. The best would be to solder all the components to the board directly, but I lack the skill to solder these small SMD myself. I would need to order it assembled, and I would probably make some mistakes and end up ordering them three times and waiting a year before they arrive.

If someone would be able to design the board, It would be great if it had some battery protection circutry and a sensor which would turn the ESP on if the crank started moving.


The HX711 sensor by default is set to 10Hz - it is much to slow for the power measurement. You need to lift pin 15 from the board and connect it to pin 16. This drives the pin HIGH and enables the 80Hz mode. This 80Hz, by the way, sets the rate of the whole arduino loop.


The ESP32 is programmed to go to sleep after 30s with no bluetooth device connected. To turn it back on you need to press the reset button. The sensors are also powered from a digital pin, which turns LOW in the sleep mode. If you want to test the sensors with the example code from libraries you need to drive the pin HIGH and wait a bit before the sensors turn on.

After assembly the sensors need to be calibrated by reading the value with no force and then with a weight applied (I used a 12kg or 16kg kettlebell hung on the pedal). These values need to be put in the powerCrank code.

It's best to tare the crank before each ride - it shouldn't be able to tare itself when someone is pedaling, but better safe than sorry and it is possible to tare it only once per turning on. If you notice some strange power levels you need to repeat this process:

  1. Put the crank straight down untill the light starts blinking.
  2. After a couple seconds the light will stay on - don't touch it then
  3. When the light turns off it sets the current force detected as a new 0.

If you want to just use the crank, without the console, the code is here on github. Everything else works the same.

Step 2: The Console

The case is cut from 3mm acrylic, the buttons are 3D printed and there are spacers for the LCD, cut from 5mm acrylic. It is glued with hot glue (it sticks quite well to the acrylic) and there is a 3D printed "bracket" to hold the PCB to the LCD. The pins for the LCD are soldered from the bottom side so it does not interfere with the ESP.

The ESP is soldered upside-down, so the USB port fits in the case

The separate button PCB is glued with hot glue, so the buttons are captured in their holes, but they still press the switches. The buttons are connected to the board with JST PH 2.0 connectors and the pin order is easy to deduce from the schematic

It is very important to mount the stepper driver in the correct orientation (the potentiometer near the ESP)

The whole portion for the SD card is disabled, as no one used it in the first version. The code needs to be updated with some UI settings like rider weight and difficulty setting.

The console is mounted using lasercut "arms" and zipties. The little teeth dig into the handlebars and hold the console.

Step 3: The Motor

The motor holds itself in the place of the adjustor knob with a 3D printed bracket. To its shaft is mounted a coupler - one side has a 5mm hole with set screws to hold the shaft, the other has a M6 thread with set screws to lock it. If you want, you can probably make it in a drill press from some 10mm round stock. It doesn't need to be extremely precise as the motor isn't mounted very tightly.

A piece of M6 threaded rod is screwed in the coupler and it pulls on a brass M6 nut. I machined it, but it can be as easily made from a piece of brass with a file. You can even weld some bits to a normal nut, so it wouldn't rotate. A 3D printed nut may also be a solution.

The thread needs to be finer than the stock screw. Its pitch is about 1.3mm, and for M6 it's 0.8mm. The motor doesn't have enough torque to turnon the stock screw.

The nut needs to be well lubricated, as the motor barely can turn the screw on the higher settings

Step 4: Configuration

To upload code to ESP32 from Arduino IDE you need to follow this tutorial:

The board is "WeMos LOLIN32", but the "Dev module" also works

I suggest using Visual Studio, but it can often break.

Before first use

The crank needs to be set up according to the "Crank" step

Using the "nRF Connect" app you need to check the MAC address of the crank ESP32 and set it in the BLE.h file.

In line 19 of indoorBike.ino you need to set, how many rotations of the screw are needed to set the resistance from completely loose to maximum. (The "maximum" is subjective on purpose, you adjust the difficulty with this setting.)

The smart trainer has "virtual gears" to set them up correctly, you need to calibrate it on lines 28 and 29. You need to pedal with a constant cadence on a given resistance setting, then read the power and set it in the file. Repeat this again with another setting.

The leftmost button switches from ERG mode (absolute resistance) to simulation mode (virtual gears). Simulation mode without a computer connection does nothing as there is no simulation data.

Line 36. sets the virtual gears - the number and ratios. You calculate them by dividing the number of teeth in the front gear by the number of teeth in the rear gear.

In line 12. you put the weight of the rider and the bike (In [newtons], mass times the gravitational acceleration!)

The whole physics part of this is probably too complicated and even I don't remember what it does exactly, but I calculate the required torque to pull the cyclist uphill or something like that (that's why the calibration).

These parameters are highly subjective, you need to set them up after a few rides for them to work correctly.

The debug COM port sends direct binary data received by bluetooth in quotes (' ') and simulation data.

The configurator

Because the configuration of the supposedly realistic physics turned out to be a huge hassle to make it feel realistic, I created a GUI configurator that should allow users to graphicaly define the function which converts from the grade of the hill to the absolute resistance level. It is not yet completely finished and I didn't have the opportunity to test it, but in the upcoming month I will be converting another bike, so I will polish it then.

On the "Gears" tab you can set the ratio of each gear by moving the sliders. You then need to copy the bit of code to replace the defined gears in the code.

On the "Grade" tab you are given a graph of a linear function (yes, it turns out the most hated subject in maths is actually useful) that takes the grade (vertical axis) and outputs absolute resistance steps (horizontal axis). I will go into the maths a bit later for those interested.

The user can define this function using the two points laying on it. On the right there is a place to change the current gear. The selected gear, as you might imagine, changes the way, how grade maps to resistance - on lower gears it is easier to pedal uphill. Moving the slider changes the 2nd coefficient, which influences how the selected gear changes the function. It is easiest to play with it a while to see how it behaves. You might also need to try a few different settings to find what works best for you.

It was written in Python 3 and should work with default libraries. To use it you need to uncomment the lines immediately after "uncomment these lines to use the configurator". As I said, it wasn't tested, so there might be some errors, but if anything comes up, please write a comment or open an issue, so I can correct it.

The maths (and physics)

The only way the controller can make it feel like you are going uphill is by turning the resistance screw. We need to convert the grade to the number of rotations. To make it easier to set up, the whole range from completely loose to not being able to turn the crank is divided into 40 steps, the same used in the ERG mode, but this time it uses real numbers instead of integers. This is done with a simple map function - you can look it up in the code. Now we are one step higher - instead of dealing with revolutions of the screw, we are dealing with imaginary steps.

Now how does it actually work when you go uphill on a bike (assuming a constant speed)? There obviously needs to be some force pushing you up, or else you would roll down. This force, as the first law of motion tells us, must be equal in magnitude but opposite in direction to the force pulling you down, for you to be in uniform motion. It comes from the friction between the wheel and the ground and if you draw the diagram of these forces, it needs to be equal the weight of the bike and the rider times the grade:


Now what makes the wheel apply this force? As we are dealing with gears and wheels, it is easier to think in terms of torque, which is simply the force times the radius:


As there are gears involved, you impart a torque on the crank, which pulls on the chain and turns the wheel. The torque needed to turn the wheel gets multiplied by the gear ratio:


and back from the torque formula we get the force required to turn the pedal


This is something that we can measure using the power meter in the crank. As dynamic friction is linearly related to the force and as this particular bike uses springs to impart this force, it is linear to the movement of the screw.

Power is force times the velocity (assuming the same direction of vectors)


and the linear velocity of the pedal is related to the angular velocity:


and so we can calculate the force required to turn the pedals on a set resistance level. As everything is linearly related, we can use proportions to do so.

This was essentially what the software needed to calculate during the calibration and using a roundabout way to get us a complicated composite, but a linear function relating grade to resistance. I wrote everything on paper calculated the final equation and all the constants became three coefficients.

This is technicaly a 3D function representing a plane (I think) that takes the grade and the gear ratio as the arguments, and these three coefficients are related to those needed to define a plane, but as the gears are discrete numbers, it was easier to make it a parameter instead of dealing with projections and such. The 1st and 3rd coefficients can be defined by a single line and (-1)* the 2nd coefiicient is the X coordinate of the point, where the line "rotates" around when changing gears.

In this visualisation the arguments are represented by the vertical line and the values by the horizontal one, and I know that this might be annoying, but it was more intuitive for me and it fit the GUI better. That is probably the reason why the economists draw their graphs this way.

Step 5: Finish!

Now you need some apps to ride with on your new trainer (which saved you around $900 :)). Here are my opinions on some of them.

  • RGT Cycling - in my opinion the best - it has a completely free option, but has a bit few tracks. Deals with the connection part the best, because your phone connects via bluetooth and a PC displays the track. Uses realistic video with an AR cyclist
  • Rouvy - a lot of tracks, paid subscription only, for some reason the PC app doesn't work with this, you need to use your phone. There may be problems when your laptop uses the same card for bluetooth and WiFi, it often lags and doesn't want to load
  • Zwift - an animated game, paid only, works quite well with the trainer, but the UI is quite primitive - the launcher uses Internet Explorer to display the menu.

If you enjoyed the build (or not), please tell me in the comments and if you have any questions you can ask here or submit an issue to github. I will gladly explain everything as it is quite complicated.

Be the First to Share


    • Photography Challenge

      Photography Challenge
    • New Year, New Skill Student Design Challenge

      New Year, New Skill Student Design Challenge
    • Fix It Speed Challenge

      Fix It Speed Challenge



    Question 7 weeks ago

    hi, kswiorek.
    Compliments, it seems you did just what I´m trying to do...
    I designed and built a home trainer, it is almost complete, at least the mechanical part of it.
    It has a magnetic controlled resistance, speed and cadence sensors, , and I tested it a lot with zwift.
    The problem is that at this moment I can only change the resistance through a potentiometer, while my goal is (obviously) to get the gradient from zwift then control the brake. Ialready have an esp32 board and I will try to adapt you console to use with my hardware.
    Do you think It will work?


    Answer 5 weeks ago

    I've just done some preliminary test modifying the ble.h (basically I only removed the part of communication with the powermeter)
    At the moment I'm focused in getting from zwift the grade, but zwift only see my power, seems that it don't recognize the indoor bike as a fitness machine , only as a power meter.
    Do you have any suggestion?


    Reply 5 weeks ago

    Try using the NRF Connect app on your phone to connect to the device. Then check if all the data there looks as it should, that you have all the flags set. I do not use Zwift, but I could help you get it connected to RGT, because I know it works with it


    Answer 7 weeks ago

    Yes, you can modify the code from GitHub. Zwift sends something called "simulation parameters" which are supposed to include the inclination, wind speed and a few others. This is defined in a Fitness Machine service pdf on the Bluetooth website, but if I remember correctly Zwift only changes the gradient. You can look at the BLE.h file and modify it to your needs.


    Reply 7 weeks ago

    Hi friend.
    Yes, I will try to modify the BLE.h file, maybe it will take a bit of time to understand how it works but I´m confident.
    Just in case, I will ask you for a help...
    Thank you a lot


    Question 9 months ago

    Hi, first of all thanks for the project, it's awesome to read, very well done and clean. I've built something similar during last year lockdown, basically is a dumb DIY trainer running on bearings. I've a set of Garmin vector 3 mounted on it, and reads even the cadence. I use Rouvy which is a beautiful and challenging way to do workouts, and my next step is to making it "interactive" by adding a resistance control on the rear (by using magnets or something similar). Basically my idea is to control the distance of the magnets from the two rolls, making more resistance. Here is my question: is it possible to use your software in the way i can talk with rouvy and, talking with my power meter/cadence sensor, making the resistance variable (by using an arduino control)? basically the goal would be pairing the rouvy output with my garmin power output. My pc has ANT+ and bluetooth dongles. Do you think i would be able to make a full DIY interactive trainer?


    Answer 9 months ago

    My software uses Bluetooth, specifically BLE, because ESP32 has it built-in. The console receives power data from the crank (also via BLE) and then it sends it to the computer. If your power meter uses Bluetooth, then you will be able to modify the code a bit to receive from a commercial sensor, and everything should work the same. If, however, the Garmin uses ANT, you will probably have to add another module that would be able to communicate with the sensor. If you managed to extract the power and cadence data from it, you could then feed it to my software, which would connect to the PC via Bluetooth.
    I would also suggest a friction based resistance. It will be much easier to control, because friction rises linearly with the applied force. Induced currents and magnetic fields might get complicated and it may be hard to get high enough resistance to pedal standing. Using the normal bicycle brakes would probably work quite well.


    Reply 9 months ago

    Hi, first of all thank you very much for the reply. Never thought about bike brakes, the idea is great :) i have to find a way to mount on the rolls, braking them "perpendicular" instead of the classic Hi, first of all thank you very much for the reply. Never thought about bike brakes, the idea is great :) i have to find a way to mount on the rolls, and in this position they would work "perpendicular" instead of the classic usage. That's a great hint, thanks! About the garmin vector 3 power pedals, they support both BLE and ANT+ protocols so it shouldn't be a problem getting them working, never tried to read the data from a DIY device, but Rouvy software pairs them and read both cadence and power correctly so shouldn't be a problem. I'm not a software expert though, i don't know how to write code, i think i will need to learn a bit for making software being able to read my power meters! :) good to hear it should work though.


    Reply 9 months ago

    Oh, and forgot to add, there is a great app for debugging BLE - "NRF Connect". It tells you all the raw data from the device.
    Here is also a website which will tell you the format of the data and what to do with it. Under "Cycling Power Service" you will find everything you need. If you have any questions about the code, ask and I will try to help.


    Reply 9 months ago

    Hi, thank you very much again for the links! in the weekend i will install the app on my PC and i will try to understand some stuff about it! i will let you know for sure. Thanks again, have a nice day!


    11 months ago

    Great Project!

    I did something similar: I just added a variable resistor with some cogwheels to the brake knob of my dumb trainer and connected an arduino. I measure the brake moment each time the wheel is spinning down using the known and constant inertia. So I could get around measuring the power at the pedals. After a few calibration runs the accurcy is now mostly within +/-10% which is sufficient to use it. To avoid fiddeling around with BLE protocols I connected a cheap ANT+ sensor, bridging the reed contacts with transistors by the arduino. Given the known brake positon, calibration curve and wheel speed the arduino transfers a virtual speed and cadence to the ANT+ sensor and an old bike computer (so I avoided most of the display part). My computer picks up the ANT+-signal, ROUVY or ZWIFT recalculate the power from the speed signal (asuming a trainer with known resistance) and here I go!
    Works pretty well, I now have 700km on the computer in one and a half month. But I need to turn the brake knob each time I want to change the power when slope changes. Feels more like automatic gear but still ok.

    Your project seems much more elaborated, so I should use this as a basis for a second version of my smart trainer. But I still have a lot of questions regarding the BLE part. Or should I better use the time for more training?


    Reply 11 months ago

    I will gladly help with the code for Bluetooth, I have spent more than 40 hours coding it, so it would be grat if I could spare someone the hassle.
    My mum has the best of both worlds - she uses the bike and I wrote most of the code :)


    Reply 11 months ago

    Thank you! I just have orderered an ESP32 and will start playing around as soon I find some time ...


    Question 11 months ago on Step 1

    Awesome project exactly what i was searching for thanks for sharing. One question i just want to use the power meter for zwift but it says the power meter sends its data to another esp the bike trainer and then communicates to zwift. If i juct do the Crank PM by itself with your code do i need to alter the coding to talk directly to zwift? Or will it work as is?


    Answer 11 months ago

    If you only want to use the power meter, then you need to upload a different program to the esp on the crank. The current one isn't compatible, both because it is easier for me with the console and so that the apps don't try to connect to the crank, while the console is on. I haven't uploaded this code to github yet, but I'll do it soon. You can use that also for measuring power in a normal bike.


    Reply 11 months ago

    Hey thanks you for your reply i really appreciate it!!.
    You sent a link to your power meter code.
    Is that code all set up to communicate directly to apps?
    Can i download this to my ESP crank arm design (or i should say YOUR design) and not need to modify the bluetooth com part to handshake with zwift ( that is the part im trying to decipher from your 3 files of code as you have the crank arm sending power and cadence to your fitness controller. Then you have a bluetooth portion for com with zwift and i cant seem to figure out in the code where your controller gets the info from the crank and then sends to Zwift).

    My goal is to make my own crank powermeter similar to yours that will juts talk to Zwift sending only power and cadence ( so i want to take that bluetooth part from the other code and add it to your crank code)
    I have a wheel on smart trainer i can compare the numbers too but i dont think the wheel on is very accurate.

    Then later on i want to use this guys dumb trainer controller design and have Zwift control my dumb trainer as well.
    This way i can have semi accurate date with 2 power sources to compare on my smart trainer and i can make a separate trainer for my wife on my old dumb classic trainer.

    Thanks again for your time have a great weekend and stay safe!
    Sorry for bugging you as well im just not a very good coder..i can figure out most of it but the communication back and forth to zwift is giving me grief :(


    Reply 11 months ago

    The code in this repository I sent you, will emulate a comercial power measuring pedal or crank. You will be able to connect it to Zwift as a "Power source" or to other devices as a power meter. You can follow the crank design from here or do something on your own, but the most important part of the code are the definitions in the begining - they tell Zwift that this device can send power data, so it is able to connect. If I have time tomorrow, I may be able to add some more comments to the code. You can also find the information on the Bluetooth website - and compare to the values in the code to figure out what they mean.
    I have a bit of a mess with the sources to this project, so if something doesn't work, please comment or add an issue to github, so I can correct it - I have a lot of old and new versions and I don't completely know what I'm using now, but the bluetooth part of this code should work.


    Reply 11 months ago

    Fantastic!! Thansk so much i will give it a try adn let you know how she goes :)

    Hi! I think is a very interesting project. But I have a few questions.
    1. Is it 2 pieces "connected" wirelessly? Crank + Console with motor?
    2. In this case... Can I connect 3th party power meter?
    3. For a cheaper installation. Using 3th party cadence and speed sensor. (with speed and weight you can get the power mathematicaly.

    The virtual gears are super cool thing!!