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.