Select Page

Difficulty Level = 8 [What’s this?]

UPDATE: Check out the new robotics platform project!

I built a wireless robotics platform from a cheap R/C car, an Arduino with XBee shield, small microswitch sensors, and a Processing program running on a remote computer to control the vehicle. The vehicle is completely controlled by the code running on the remote computer which allows very rapid prototyping of the code to tell the vehicle what to do and how to react to the sensor events received from the vehicle. I’m hoping this is a good way to teach my 9-year old son about programming.

Wireless computer-controlled robotics platform built on cheap RC vehicle, Arduino microcontroller, and XBee radios

Before I get into details, here’s an overview of the features:

  • All logic controlling the vehicle is performed in a Processing program running on remote computer. The Arduino program listens for commands from the remote computer.
  • Bi-directional wireless communication over XBee radios with (theoretical) 1-mile range. I’ve accomplished 1/4 mile range with these radios.
  • Sensor events are transmitted from the vehicle to the controlling computer. This vehicle has 3 microswitches – two on front bumper and one at the rear.
  • Original circuitry of vehicle replaced with dual H-Bridge circuit to control drive motor and turn motor. Drive motor is controlled with variable speed.
  • Power: Vehicle motors powered by 4 AA batteries. Arduino with XBee shield powered by 9V battery mounted at front of vehicle.
  • Simple communications protocol: 2 byte commands from controller to vehicle, one byte sensor readings from vehicle to controller.

The Hardware

There’s nothing special about the configuration of the XBee radios. They are running the AT firmware (“transparent mode”) which allows them to simply exchange serial data. The Libelium XBee shield on top of the Arduino makes it easy to read/write serial data from Arduino code.

Arduino and XBee shield on top of the vehicle

Inside the vehicle is a simple circuit board with an SN754410 quadruple half-H driver to drive the motors. The drive motor and turn motor are connected. I had to rip out the original circuit board (but I saved it!).

Small circuit board with dual H-Bridge chip connected to drive motor and turn motor

The controller computer connects to an XBee radio via a Sparkfun FTDI USB breakout board. This is just a simple perf-board circuit connecting the USB interface to the XBee. It’s as simple as connecting the RX/TX pairs together, and giving it power.

Transmitter on controller computer. FTDI USB board and XBee radio.

Here’s a view of a microswitch collision sensor on the back of the vehicle. It’s mounted on a small piece of wood (yes, wood) glued to the chassis.

Rear collision sensor microswitch

The Code

This is a listing of the Arduino code. It is fairly simple and self-explanatory because the Arduino just listens for commands and sends sensor readings. A BEGIN_COMMAND delimiter indicates that the next two bytes are a command. The first byte of a command is a bit pattern telling the Arduino whether to go forward, backward, turn left, or turn right. There is one bit assigned to each of those 4 directions (4 bits are unused and reserved for future functions). The second byte represents speed. By using PWM on the H-bridge enable pin, the speed of the vehicle can be controlled.
If a sensor switch is closed, then a byte with the appropriate bit is sent back to the controlling computer. There is logic to debounce the switches. Studying this code is left as an exercise for the reader. Feel free to download the Arduino sketch.


// Pin assignments
#define LEFT_PIN 13         // red/black
#define TURN_ENABLE_PIN 12  // white
#define DRIVE_ENABLE_PIN 11 // white/black
#define RIGHT_PIN 10        // red
#define BACKWARD_PIN 9      // yellow/black
#define FORWARD_PIN 8       // yellow

#define LEFT_FRONT_BUMPER_PIN 7     // white
#define RIGHT_FRONT_BUMPER_PIN 6    // yellow
#define REAR_BUMPER_PIN 5           // red

// Input Commands
#define BEGIN_COMMAND 0x7F
#define FORWARD 0x1
#define BACKWARD 0x2
#define LEFT 0x4
#define RIGHT 0x8

// Sensor Data
#define LEFT_FRONT_BUMPER 0x1
#define RIGHT_FRONT_BUMPER 0x2
#define REAR_BUMPER 0x4
#define DEBOUNCE_THRESHOLD 50


int sensorData = 0;
int lastSensorData = 0;
int lastLeftFrontBumperReading;
int lastRightFrontBumperReading;
int lastRearBumperReading;
int leftFrontBumperReadingTime;
int rightFrontBumperReadingTime;
int rearBumperReadingTime;
int command[2];

void setup() {
  Serial.begin(9600);

  pinMode(DRIVE_ENABLE_PIN, OUTPUT);
  pinMode(TURN_ENABLE_PIN, OUTPUT);
  pinMode(LEFT_PIN, OUTPUT);
  pinMode(RIGHT_PIN, OUTPUT);
  pinMode(BACKWARD_PIN, OUTPUT);
  pinMode(FORWARD_PIN, OUTPUT);

  pinMode(LEFT_FRONT_BUMPER_PIN, INPUT);
  digitalWrite(LEFT_FRONT_BUMPER_PIN, HIGH);
  pinMode(RIGHT_FRONT_BUMPER_PIN, INPUT);
  digitalWrite(RIGHT_FRONT_BUMPER_PIN, HIGH);
  pinMode(REAR_BUMPER_PIN, INPUT);
  digitalWrite(REAR_BUMPER_PIN, HIGH);

}

void loop() {

  // Read sensors and transmit
  readSensors();
  if (sensorData != lastSensorData) {
    Serial.write(sensorData);
    lastSensorData = sensorData;
  }


  // listen for wireless commands
  if (Serial.available() > 0) {
    if (readCommand() > 0) {
      executeCommand();
    }
  }
}


void readSensors() {
  int s;

  s = digitalRead(LEFT_FRONT_BUMPER_PIN);
  if (s != lastLeftFrontBumperReading) {
    leftFrontBumperReadingTime = millis();
  }
  if ((millis() - leftFrontBumperReadingTime) > DEBOUNCE_THRESHOLD) {
    if (s == LOW) {
      sensorData |= LEFT_FRONT_BUMPER;
    } else {
      sensorData &= ~LEFT_FRONT_BUMPER;
    }
  }
  lastLeftFrontBumperReading = s;

  s = digitalRead(RIGHT_FRONT_BUMPER_PIN);
  if (s != lastRightFrontBumperReading) {
    rightFrontBumperReadingTime = millis();
  }
  if ((millis() - rightFrontBumperReadingTime) > DEBOUNCE_THRESHOLD) {
    if (s == LOW) {
      sensorData |= RIGHT_FRONT_BUMPER;
    } else {
      sensorData &= ~RIGHT_FRONT_BUMPER;
    }
  }
  lastRightFrontBumperReading = s;

  s = digitalRead(REAR_BUMPER_PIN);
  if (s != lastRearBumperReading) {
    rearBumperReadingTime = millis();
  }
  if ((millis() - rearBumperReadingTime) > DEBOUNCE_THRESHOLD) {
    if (s == LOW) {
      sensorData |= REAR_BUMPER;
    } else {
      sensorData &= ~REAR_BUMPER;
    }
  }
  lastRearBumperReading = s;
}

int readCommand() {
  int b = Serial.read();
  if (b == BEGIN_COMMAND) {
    command[0] = readByte();
    command[1] = readByte();
    return 1;
  } else {
    return 0;
  }
}

// blocking read
int readByte() {
  while (true) {
    if (Serial.available() > 0) {
      return Serial.read();
    }
  }
}

void executeCommand() {
  int c = command[0];
  int speed = command[1];

  digitalWrite(DRIVE_ENABLE_PIN, LOW);
  if (c & FORWARD) {
    digitalWrite(BACKWARD_PIN, LOW);
    digitalWrite(FORWARD_PIN, HIGH);
  }
  if (c & BACKWARD) {
    digitalWrite(FORWARD_PIN, LOW);
    digitalWrite(BACKWARD_PIN, HIGH);
  }
  if (c & (FORWARD | BACKWARD)) {
    analogWrite(DRIVE_ENABLE_PIN, speed);
  }

  digitalWrite(TURN_ENABLE_PIN, LOW);
  if (c & LEFT) {
    digitalWrite(RIGHT_PIN, LOW);
    digitalWrite(LEFT_PIN, HIGH);
  }
  if (c & RIGHT) {
    digitalWrite(LEFT_PIN, LOW);
    digitalWrite(RIGHT_PIN, HIGH);
  }
  if (c & (LEFT | RIGHT)) {
    digitalWrite(TURN_ENABLE_PIN, HIGH);
  }
}

Now let’s look at the Processing code that really controls the vehicle from afar. I used Procesing because it’s easy for beginners, has an IDE that is familiar to Arduino coders, and hides a lot of complexity. And I’ve been coding in Java since last century — Processing is just Java simplified, so it’s second-nature for me.

This Processing sketch doesn’t actually have a visual interface at all. It just communicates with the transmitter via the USB serial interface. Before I show the entire Processing sketch, I just want to show you the part of the code you write in the draw() method that controls the vehicle. The draw() method is the same concept as the loop() method in Arduino. This is how you tell the vehicle what to do:

void draw() {

   ...

  speed = 9;  // set speed value.  Valid values are 0-9.
  forward(1000);  // go forward for 1000ms (one second)
  stop();
  right();  // turn wheels
  backward(500);  // backward for half a second
  left();
  speed = 5;
  backward(500);
  stop();
  forward(2000);
  //  you get the idea...

  straight();  // wheels straight
  end();  // end the program.  Without this, it repeats.
}

What about sensor events that we receive from the vehicle? We handle those by writing methods for each event. These methods will be called as soon as the sensor reading is received. The currently running program (like above) will be terminated immediately and the appropriate method below will run. It looks like we’re only checking for collision events at the beginning of the draw() method, but when an event happens, the program instructions stop and we come back to the beginning of the draw() immediately so we can handle the event. Here’s the full draw() method and the event handling methods:

void draw() {

  if (rightFrontBumper) {
    println(timestamp() + " ----RIGHT FRONT IMPACT!----");
    rightFrontBumper();
  }
  if (leftFrontBumper) {
    println(timestamp() + " ----LEFT FRONT IMPACT!-----");
    leftFrontBumper();
  }
  if (rearBumper) {
    println(timestamp() + "-------REAR IMPACT!--------");
    rearBumper();
  }

  speed = 9;  // set speed value.  Valid values are 0-9.
  forward(1000);  // go forward for 1000ms (one second)
  stop();
  right();  // turn wheels
  backward(500);
  left();
  speed = 5;
  backward(500);
  stop();
  forward(2000);
  //  you get the idea...

  straight();  // wheels straight
  end();  // end the program.  Without this, it repeats.
}

void leftFrontBumper() {
  leftFrontBumper = false;
  running = true;
  left();
  speed = 9;
  backward(800);
  straight();
  end();
}

void rightFrontBumper() {
  rightFrontBumper = false;
  running = true;
  right();
  speed = 9;
  backward(800);
  straight();
  end();
}

void rearBumper() {
  rearBumper = false;
  running = true;
  right();
  speed = 9;
  forward(1000);
  straight();
  end();
}

You can start your program again by pressing the ‘s’ key. Also, after a program is done running, you can control the vehicle with the arrow keys. For example, if you want to position the vehicle for another run.

The whole idea here is that this provides a platform for programming the vehicle to do whatever you wish, and dealing with collisions however you want. There’s a lot of potential here for more sensors and more commands to the vehicle (How about a robotic arm? Infrared sensors?).

There’s a lot more code in the Processing sketch that handles the communication, etc. Here is an entire listing with simple code for the program to run, and simple event handling methods for collisions. I won’t explain every bit of the code, but if you like code, you’ll be able to understand it. Feel free to download the entire Processing sketch that implements the remote vehicle controller.

import processing.serial.*;

Serial port = null;
String portName;
int lastInput = 0;
int[] command = new int[2];
int[] lastCommand = new int[2];
int speed = 9;
int lastSpeed = 0;
int speedStep = (255 - 130) / 8;
boolean running = true;
boolean leftFrontBumper = false;
boolean rightFrontBumper = false;
boolean rearBumper = false;

int DIR_FORWARD = 0x1;
int DIR_BACKWARD = 0x2;
int DIR_LEFT = 0x4;
int DIR_RIGHT = 0x8;
int BEGIN_COMMAND = 0x7F;

// Sensor Data
int LEFT_FRONT_BUMPER = 0x1;
int RIGHT_FRONT_BUMPER = 0x2;
int REAR_BUMPER = 0x4;

SimpleDateFormat df = new SimpleDateFormat("hh:mm:ss.SSS");

void setup() {
  size(1, 1);

  portName = Serial.list()[0];
  try {
    port = new Serial(this, portName, 9600);
  } catch (Exception e) {
    e.printStackTrace();
  }
}

String timestamp() {
  return df.format(new Date());
}

void draw() {

  if (rightFrontBumper) {
    println(timestamp() + " ----RIGHT FRONT IMPACT!----");
    rightFrontBumper();
  }
  if (leftFrontBumper) {
    println(timestamp() + " ----LEFT FRONT IMPACT!-----");
    leftFrontBumper();
  }
  if (rearBumper) {
    println(timestamp() + " -------REAR IMPACT!--------");
    rearBumper();
  }

  speed = 9;
  left();
  forward(1200);
  right();
  forward(800);
  stop();

  left();
  backward(1200);
  stop();
  right();

  forward(2500);
  stop();

  left();
  backward(4000);

  end();
}

void leftFrontBumper() {
  leftFrontBumper = false;
  running = true;

  left();
  speed = 9;
  backward(800);
  straight();
  end();
}

void rightFrontBumper() {
  rightFrontBumper = false;
  running = true;

  right();
  speed = 9;
  backward(800);
  straight();
  end();
}
void rearBumper() {
  rearBumper = false;
  running = true;

  speed = 9;
  forward(1000);
  straight();
  end();
}






void end() {
  if (!running) return;
  stop();
  straight();
  println(timestamp() + " -----------END------------");
  println("Press 's' to restart program or use arrow keys to control vehicle.");
  running = false;
}

void interruptibleDelay(int millis) {
  int start = millis();
  int d;
  while (running) {
    int timeLeftToWait = millis-(millis()-start);
    d = min(10, timeLeftToWait);
    if (d <= 0) return;
    delay(d);
  }
}


void forward(int millis) {
  if (!running) return;
  forward();
  interruptibleDelay(millis);
  stop();
}

void forward() {
  if (!running) return;
  doForward();
}

void doForward() {
  command[0] = command[0] & ~DIR_BACKWARD;
  command[0] = command[0] | DIR_FORWARD;
  sendCommand();
}

void backward(int millis) {
  if (!running) return;
  doBackward();
  interruptibleDelay(millis);
  stop();
}

void backward() {
  if (!running) return;
  doBackward();
}

void doBackward() {
  command[0] = command[0] & ~DIR_FORWARD;
  command[0] = command[0] | DIR_BACKWARD;
  sendCommand();
}

void left() {
  if (!running) return;
  doLeft();
}

void doLeft() {
  command[0] = command[0] & ~DIR_RIGHT;
  command[0] = command[0] | DIR_LEFT;
  sendCommand();
}

void right() {
  if (!running) return;
  doRight();
}

void doRight() {
  command[0] = command[0] & ~DIR_LEFT;
  command[0] = command[0] | DIR_RIGHT;
  sendCommand();
}

void straight() {
  if (!running) return;
  doStraight();
}

void doStraight() {
  command[0] = command[0] & ~DIR_RIGHT;
  command[0] = command[0] & ~DIR_LEFT;
  sendCommand();
}

void stop() {
  if (!running) return;
  doStop();
}
void doStop() {
  command[0] = command[0] & ~DIR_BACKWARD;
  command[0] = command[0] & ~DIR_FORWARD;
  sendCommand();
}



void serialEvent(Serial p) {
  int input = p.read();
  lastInput = input;
  processInput(input);
} 


void processInput(int input) {
  if ((input & LEFT_FRONT_BUMPER) != 0) {
    stop();
    leftFrontBumper = true;
    running = false;
  }

  if ((input & RIGHT_FRONT_BUMPER) != 0) {
    stop();
    rightFrontBumper = true;
    running = false;
  }

  if ((input & REAR_BUMPER) != 0) {
    stop();
    rearBumper = true;
    running = false;
  }
}


void sendCommand() {
  if (!isNewCommand()) {
    return;
  }
  if (port != null) {
    port.write(BEGIN_COMMAND);
    port.write(command[0]);
    command[1] = 255 - ((9 - speed) * speedStep);
    port.write(command[1]);
    if ((command[0] & DIR_FORWARD) > 0) {
      print(timestamp() + " FORWARD\t");
    }
    if ((command[0] & DIR_BACKWARD) > 0) {
      print(timestamp() + " BACKWARD\t");
    }
    if (((command[0] & DIR_FORWARD) == 0) && ((command[0] & DIR_BACKWARD) == 0)) {
      print(timestamp() + " STOP\t");
    }
    if ((command[0] & DIR_LEFT) > 0) {
      print("LEFT\t");
    }
    if ((command[0] & DIR_RIGHT) > 0) {
      print("RIGHT\t");
    }
    if (((command[0] & DIR_LEFT) == 0) && ((command[0] & DIR_RIGHT) == 0)) {
      print("STRAIGHT\t");
    }
    println("SPEED=" + speed);
    

    lastCommand[0] = command[0];
    lastCommand[1] = command[1];
    lastSpeed = speed;
  }
}

boolean isNewCommand() {
  return ((command[0] != lastCommand[0]) || (command[1] != lastCommand[1]) || (speed != lastSpeed));
}

void keyPressed() {
  if (key == CODED) {
    if (keyCode == UP) {
      doForward();
    }
    if (keyCode == DOWN) {
      doBackward();
    }
    if (keyCode == LEFT) {
      doLeft();
    }
    if (keyCode == RIGHT) {
      doRight();
    }
  }
  if ((key >= '1') && (key <= '9')) {
    speed = 9 - ('9' - key);
    println("set speed = " + speed);
  }
  if (key == 's') {
    running = true;
  }
}

void keyReleased() {
  if (key == CODED) {
    if (keyCode == UP) {
      doStop();
    }
    if (keyCode == DOWN) {
      doStop();
    }
    if (keyCode == LEFT) {
      doStraight();
    }
    if (keyCode == RIGHT) {
      doStraight();
    }
  }
}

Demo

Here's a demo where I tried to program the vehicle to negotiate an obstacle course. This is the instructions and the sensor event handling code:

void draw() {

  if (rightFrontBumper) {
    println(timestamp() + " ----RIGHT FRONT IMPACT!----");
    rightFrontBumper();
  }
  if (leftFrontBumper) {
    println(timestamp() + " ----LEFT FRONT IMPACT!-----");
    leftFrontBumper();
  }
  if (rearBumper) {
    println(timestamp() + " -------REAR IMPACT!--------");
    rearBumper();
  }

  speed = 9;
  left();
  forward(1200);
  right();
  forward(800);
  stop();

  left();
  backward(1200);
  stop();
  right();

  forward(2500);
  stop();

  left();
  backward(4000);

  end();
}

void leftFrontBumper() {
  leftFrontBumper = false;
  running = true;

  left();
  speed = 9;
  backward(800);
  straight();
  end();
}

void rightFrontBumper() {
  rightFrontBumper = false;
  running = true;

  right();
  speed = 9;
  backward(800);
  straight();
  end();
}

void rearBumper() {
  rearBumper = false;
  running = true;

  speed = 9;
  forward(1000);
  straight();
  end();
}

Let's see how well it worked. I tried to navigate through the cones without hitting them. The wheels skidded a bit when changing direction on the smooth floor. At the end, I purposefully ran into something backwards to demonstrate a sensor event. See if you can match up the vehicle's actions with the code above.

Finally, here's a screenshot of the Processing console that shows everything that happened. Notice the rear impact event at the end.

Hope you enjoyed reading about this project. It was a lot of work and I hope that it can be the basis for future robotics experimentation.