Pedal a Bike Through Virtual Reality for Under $100

Arduino Bikes Drones & Vehicles Technology
Pedal a Bike Through Virtual Reality for Under 0

Paul Yan’s “Arduino thing” brings virtual reality biking into the realm of affordability. Yan confesses, “I absolutely hate exercising and want to make that experience a little less painful and mind-numbing.” That, coupled with his idea that Arduino is “an alternate kind of game controller,” brought about the idea for a virtual reality biking experience. The device operates on two mechanisms: it measures the bike wheel’s revolutions with a tachometer that uses infrared light, and it is able to communicate that information to a smartphone over BLE. These mechanisms work together to measure your pedaling output and feed that into a virtual reality environment.

Yan set up his bike on a stationary indoor trainer, allowing him to pedal in place. The beauty of his device is that it’s widely compatible not only with any type of bike, but theoretically also with any device that has a looping or revolving surface, such as a treadmill. That’s because the optical tachometer is relatively noninvasive, requiring only a small piece of paper taped to the tire. Yan explains that “each time the wheel makes a complete rotation, the Arduino will detect when the piece of paper passes by and then sends a wireless message to the mobile phone,” thereby moving the virtual bike forward through its virtual environment.

YouTube player

Yan uses this $10 headset and a simple cartoon town developed with Unity which he renders for VR using Google Cardboard’s free SDK. He explains how he set up the virtual bike to move through the environment:

I used a 3rd party package called Simple Waypoint System to draw out a spline path. If you know what you’re doing, this package is not necessary but it did make life much easier. One of their examples was built to push a car along a path using the keyboard’s up button so I replaced the car with the camera rig, and had the incoming BLE ping call the same function as the up button.

All in all, Yan cites his cost as $30 for the Arduino and $10 for the headset, coming to a grand total of $40 for the project. However it’s important to note that the Unity environment is a $10 download, the 3rd party package which supports BLE for iOS and Android is another $10, and the optional Simple Waypoint System is $15. You’ll also need to build or obtain a stationary bike setup, a BLE breakout, and an IR sensor to wire to the Arduino. Plus, if you want the ability to steer the bike left and right, that will require extra components as well. While these extra considerations certainly bring the price over $40, it can still be built for under $100, which isn’t too shabby either.

Below is Yan’s schematic as well as the Arduino code.


The Arduino has two key components: a BLE breakout (Adafruit’s nRF8001) and a reflective IR sensor. The reflective sensor has two sides: one with an IR LED (“E”) and the other with an IR phototransistor (“S”). I soldered these onto a small perf board away from the Arduino with an extension cord made up of 18 gauge wire. The wire is thick enough to suspend the perf board in the air, yet flexible enough to adjust its position and aim like a goose neck lamp. The nRF8001 BLE breakout takes up pins 2, 9, 10, 11, 12, and 13, but your setup will probably differ.

#include "Adafruit_BLE_UART.h"

// nRF8001 pins: SCK:13, MISO:12, MOSI:11, REQ:10, ACI:X, RST:9, 3Vo:X
unsigned long time = 0l;
boolean connection = false;
uint8_t btm = 65;
uint8_t out = btm;
uint8_t cap = 90;
#define persec 30
#define sendat (1000/persec)

int irPin = 7;
int irSensorPin = 5;
int testLEDPin = 4;
int tripTime = 0;
int lastTrip = 0;
int tripBetween;
boolean detectState = false;
boolean lastDetectState = false;

void setup(void)

pinMode(irPin, OUTPUT);
pinMode(irSensorPin, INPUT);
pinMode(testLEDPin, OUTPUT);

uart.setDeviceName("YanBLE"); /* define BLE name: 7 characters max! */


void loop()
pollIR(); // IR sensor
uart.pollACI(); // BLE

void pollIR() {
digitalWrite(irPin, HIGH);

if (digitalRead(irSensorPin) == LOW) {
detectState = true;
if (detectState != lastDetectState) {
// run the first time reflection is detected

Serial.println("message sent via BLE");
if (connection == true) {
sendBlueMessage("1"); // dummy data passed here, this can be any value. We just need to ping the app

lastDetectState = true;
else {
// here we are seeing the same reflection over several frames
// turn test LED on to give visual indication of a positive reflection
digitalWrite(testLEDPin, HIGH);
else {
detectState = false;
lastDetectState = false;
digitalWrite(testLEDPin, LOW);

BLE-related functions below this point
void aciCallback(aci_evt_opcode_t event)
// this function is called whenever select ACI events happen
switch (event)
Serial.println(F("Advertising started"));
connection = true;
connection = false;

void rxCallback(uint8_t *buffer, uint8_t len)
// this function is called whenever data arrives on the RX channel

void sendBlueMessage(String message) {

uint8_t sendbuffer[20];
message.getBytes(sendbuffer, 20);
char sendbuffersize = min(20, message.length());

Serial.print(F("\n* Sending -> \"")); Serial.print((char *)sendbuffer); Serial.println("\"");

// write the data
uart.write(sendbuffer, sendbuffersize);

Discuss this article with the rest of the community on our Discord server!

Sophia is the managing editor of the Make: blog. When she’s not greasing editorial gears, she likes to run, ride, climb, and lift things, and make lo-tech goods like zines, desserts, and altered clothing. @sophiuhcamille

View more articles by Sophia Smith


Ready to dive into the realm of hands-on innovation? This collection serves as your passport to an exhilarating journey of cutting-edge tinkering and technological marvels, encompassing 15 indispensable books tailored for budding creators.