GTA5 Exercise Bike

Here I turned an old exercise bike into a video game controller using an Arduino Leonardo. The Leonardo can emulate a USB keyboard, so in theory one could attach some sensors to a bike and pedal one’s way through Grand Theft Auto V, which, aside from being violent and sexist, is probably the best driving simulation out there. My thought was to get this working for fun, then adapt it for a more school-friendly game like Mario Kart and put it in the cafeteria for kids to try. Perhaps it could work with google earth and one could explore the planet by bike? Maybe the PE department will get interested. One way or another I think it will inspire some kids. The project was funded by Riverdale’s Frankel Fellows program, which awards funding for passion projects. Specs and code are below.

bike

The Arduino-powered GTA5 exercise bike

 

 

gta_mainboard

Main breadboard with Arduino Leonardo

 

gta_reed

A reed sensor next to the chainwheel is triggered every time the magnet passes it

 

rotation

The handlebars are connected to a rotation sensor for steering

 

Materials

1 x exercise bike, or perhaps a regular bike on rollers or a stand
1 x reed switch
1 x Arduino Leonardo (the Uno et al won’t do it – you need onboard usb emulation)
1 x rotation sensor
1 x small rare earth magnet
2 x breadboards and a bunch of jumpers
1 x arcade style button (preferably more to add extra functions)
some 10k ohm resistors to pull down the button(s) and the reed switch
either a bike handlebar/stem assembly or a simpler construct out of wood
1 x usb cable
GTA5 or any other PC or Mac game controlled by a keyboard

Method

After removing the plastic shell from the exercise bike, I attached a small rare-earth magnet to the chainwheel (the gear attached to the pedals) and placed a breadboard with a reed switch on the frame so the magnet passed within a few mm of the sensor each time the pedals make a revolution. This will allow the Arduino to figure out how fast the rider is pedaling. Steering is a little trickier – the exercise bike handlebars don’t turn. Luckily this model features a 1.25″ square tube steering stem that 1″ square tube fits into. I chopped off the front of a kids bike and welded it to a piece of 1″ square tube so the kids handlebars and front fork would sleeve onto the exercise bike stem. Using some flexible hose I attached a rotation sensor to the front fork. The problem was that the sensor had a range of 300 degrees, but the handlebars could spin without limit, which would break the sensor. The solution was to drill a hole in the square tube stem and run a steel rod back through the forks, limiting their movement.

The steering setup could have been a lot simpler – this could have been done with zipties and some wood rather than a bicycle and welding – but I happened to have everything on hand.

I attached a piece of plywood with four arcade style buttons and connected one to the main breadboard with a pulldown resistor. This button will be the reverse/brake key. The other three are there for future functions, perhaps for other games.

The Arduino has to receive input from the two sensors and the button and send keystrokes using usb keyboard emulation to the pc. I found some code for an Arduino bike speedometer by Amanda Ghassaei that worked well. The rotation sensor returns an analog value, so getting the steering angle was pretty straightforward.

Generating the keystrokes was trickier than I thought. The Arduino keyboard library is easy to use – I could generate keystrokes by pedaling that showed up in the notepad app – but GTA wasn’t receiving any input. It turned out that one has to hold down the movement keys (W, A, S, and D) in GTA to make the car move – an instantaneous keystroke using keyboard.print() has no effect. Luckily setting a 100 ms delay() between keyboard.press() and keyboard.release() is enough for GTA to get the message.

However, delay() stops everything, meaning that there’s no way to send keystrokes for steering during this time. So extending the delay() past 100 ms doesn’t seem like a good idea, even if the player is accelerating at top speed (pedaling very fast). The same goes for steering – if you hold down the A key in the game the car steers sharply to the left. If you give the A key a few quick taps, the car veers slightly to the left.

I opted to create a drive() function that’s called repeatedly by loop(). drive() begins by releasing (unpressing) all keys. If the user is pedaling slowly, it sends a W keypress once every 50 times that drive() is called. As the player pedals faster, the W keypress is sent more often. The same technique is used for steering – the greater the steering angle, the more often a keypress is sent. The result is that both acceleration/braking and steering keypresses are sent in an interleaved fashion. The steering is a bit sloppy, but the overall experience is really fun. Everyone who’s tried it, from ages 15 to 74 (my mother) got completely swept up in trying to escape from the police by pedaling like mad. It’s easy to forget you’re working out.

If anyone has a better way to approach this that reduces the sloppiness of the steering, I’d love to hear about it.

Here’s the Fritzing diagram and file:

Here’s the code:

//bike speedometer code part borrowed from Amanda Ghassaei 2012
//http://www.instructables.com/id/Arduino-Bike-Speedometer/
//calculations
//tire radius ~ 13.5 inches
//circumference = pi*2*r =~85 inches
//max speed of 35mph =~ 616inches/second
//max rps =~7.25

#include <Keyboard.h>
#define reed A0 //pin connected to reed switch
#define steerPin A1

int lockPin = 2; // this is a jumper to ground - a safety switch. The Arduino won't emulate a keyboard unless this jumper is connected. This is important because you can't download code into the Arduino if it's acting like a keyboard
int ledPin = 13;
int reversePin = 7;
int steerAngle = 225; // straight
int counter = 0;

//storage variables
float radius = 4;// tire radius (in inches)- CHANGE THIS FOR YOUR OWN BIKE
int reedVal;
long timer = 0;// time between one full rotation (in ms)
float mph = 0.00;
float circumference;
int maxReedCounter = 100;//min time (in ms) of one rotation (for debouncing)
int reedCounter;
int keypressFreq;

void setup(){
  pinMode(reed, INPUT);
  reedCounter = maxReedCounter;
  circumference = 2*3.14*radius;

  // TIMER SETUP- the timer interrupt allows precise timed measurements of the reed switch
  //for more info about configuration of arduino timers see http://arduino.cc/playground/Code/Timer1
  cli();//stop interrupts
  //set timer1 interrupt at 1kHz
  TCCR1A = 0;// set entire TCCR1A register to 0
  TCCR1B = 0;// same for TCCR1B
  TCNT1  = 0;
  // set timer count for 1khz increments
  OCR1A = 1999;// = (1/1000) / ((1/(16*10^6))*8) - 1
  // turn on CTC mode
  TCCR1B |= (1 << WGM12);
  // Set CS11 bit for 8 prescaler
  TCCR1B |= (1 << CS11);   
  // enable timer compare interrupt
  TIMSK1 |= (1 << OCIE1A);
  sei();//allow interrupts
  //END TIMER SETUP

  pinMode(lockPin, INPUT);
  pinMode(steerPin, INPUT);
  pinMode(ledPin, OUTPUT);
  pinMode(reversePin, INPUT);
  digitalWrite(lockPin, HIGH);
  Keyboard.begin();  
}

ISR(TIMER1_COMPA_vect) {//Interrupt at freq of 1kHz to measure reed switch
  reedVal = digitalRead(reed);//get val of A0
  if (reedVal){//if reed switch is closed
    if (reedCounter == 0){//min time between pulses has passed
      mph = (56.8*float(circumference))/float(timer);//calculate miles per hour
      timer = 0;//reset timer
      reedCounter = maxReedCounter;//reset reedCounter
    }
    else{
      if (reedCounter > 0){//don't let reedCounter go negative
        reedCounter -= 1;//decrement reedCounter
      }
    }
  }
  else{//if reed switch is open
    if (reedCounter > 0){//don't let reedCounter go negative
      reedCounter -= 1;//decrement reedCounter
    }
  }
  if (timer > 2000){
    mph = 0;//if no new pulses from reed switch- tire is still, set mph to 0
  }
  else{
    timer += 1;//increment timer
  } 
}

void drive(){
  //mph range 0-2.5
   if (digitalRead(reversePin)) { // reverse (brakes) is always full speed
      Keyboard.press(115); // s 
      Keyboard.release(119);
      delay(100);
   }    
   else { // forward or stopped, clear keys
      Keyboard.release(115);
      Keyboard.release(119);
    }
    if (mph >= 2.3) { // fast
       Keyboard.press(119);
       delay(100);  
    }

    if ((mph >= 2) && (mph < 2.3)) { // pretty fast
      if (counter%2==0) {
        Keyboard.press(119);
        delay(100);
      }
    }
    if ((mph >= 1) && (mph < 2)) { // med
      if (counter%20==0) {
        Keyboard.press(119);
        delay(100);
      }
    }
    if ((mph >= .5) && (mph < 1)) { // slow
      if (counter%30==0) {
        Keyboard.press(119);
        delay(100);
      }
    }

    if ((mph > 0) && (mph < .5)) { // really slow
      if (counter%50==0) {
        Keyboard.press(119);
        delay(100);
      }
    }

    steerAngle = analogRead(steerPin);
    /* 
      range 48 - 423
      straight 195-245
    */

    if ((steerAngle >= 195) && (steerAngle <= 245)) { // straight
      Keyboard.release(97);
      Keyboard.release(100);
    } 

    // if not straight, calc freq of keypress
    if (steerAngle > 245) { // left
      Keyboard.release(100);
      keypressFreq = map(steerAngle, 246, 395, 25, 1);
      if (steerAngle > 395) {keypressFreq = 1;}
      if (keypressFreq > 1) { Keyboard.release(97);}
       if (counter%keypressFreq==0) {
          Keyboard.press(97); // d
       }
    }

    if (steerAngle < 195) { // right
      Keyboard.release(97);
      keypressFreq = map(steerAngle, 194, 60, 25, 1);
      if (steerAngle < 60) {keypressFreq = 1;}
      if (keypressFreq > 1) { Keyboard.release(100);}
        if (counter%keypressFreq==0) {
          Keyboard.press(100); // a
       }
    }  
  }

void loop(){
  if (!digitalRead(2)) { // pull this jumper to disable keyboard emulation so you can program the board
    if (counter > 1000) { counter = 0; }
    counter++;
    drive();    
  }

  else {
    Keyboard.releaseAll(); // jumper is pulled, Arduino is not acting like a keyboard 
  }
}

Share & Print

0 0 100 0

2 Comments

  1. Sean Dagony-Clark 10 months ago

    Love this. But yeah, GTA is a wee bit violent. Would this work with something like Mario Kart or a race game?

    • jmerrow@riverdale.edu Author 10 months ago

      It can be adapted to any PC or Mac game that’s controlled by a keyboard…

Leave a Reply