Hand gesture controlled car on an Arduino Uno
Tilt an MPU6050 on your hand and a 2WD car follows over nRF24L01 radios: forward, reverse, left, right, and it stops when the signal is lost. The whole build, an Arduino Uno and 5 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.
Teaching? Assign it to your class in one click.
Advanced Runs in your browser. Free, and no account needed.
The parts retail for about $75. On Mokxi they're free.
The note on the canvas
Word for word, as it sits beside the circuit in the editor.
Try it
1. Press Run. Both boards start, and the car's serial monitor says "link up".
2. Drag the MPU6050's pitch to +30: the hand tips forward and the car drives forward.
3. Drag the pitch to -30 and it reverses. Back to 0 and it stops.
4. Drag the roll to +30 or -30 and the car turns right or left on the spot.
5. Hold the hand's Uno in reset: half a second later the car stops by itself.
The code
The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.
// Hand gesture controlled car, the hand: an MPU6050 strapped to the back of
// your hand and an nRF24L01 radio, sending how the hand is tilted to the Uno
// on the car.
//
// MPU6050 SDA A4, SCL A5, VCC 5V, GND
// radio CE on 7, CSN on 8, SCK 13, MOSI 11, MISO 12, VCC on 3.3V
//
// The tilt comes from the accelerometer alone: with the hand still, the only
// acceleration it feels is gravity, and the angle of gravity in the board's
// axes is the tilt.
//
// pitch = atan2(-ax, sqrt(ay^2 + az^2)) positive with the fingers down
// roll = atan2(ay, az) positive tipped to the right
//
// About twenty times a second this board sends the two angles, in whole
// degrees, as one 4-byte packet. The car's radio answers every packet in
// hardware, so radio.write() says whether it got there, and once a second
// the serial monitor shows the tilt and how many packets were lost.
//
// The car decides what a tilt means (gesturerx). Both radios must agree on
// the channel, the data rate, the address and the payload size.
#include <Wire.h>
#include <Adafruit_MPU6050.h>
#include <Adafruit_Sensor.h>
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
Adafruit_MPU6050 mpu;
RF24 radio(7, 8); // CE on 7, CSN on 8
const byte address[6] = "CAR01";
// What goes over the air. The car declares the same struct.
struct Packet {
int16_t pitch;
int16_t roll;
};
const float DEGREES = 180.0 / M_PI;
unsigned long sent = 0, lost = 0;
unsigned long lastReport = 0;
void setup() {
Serial.begin(115200);
if (!mpu.begin()) {
Serial.println("no MPU6050 at 0x68: check SDA, SCL and VCC");
while (true) delay(10);
}
radio.begin();
radio.setPayloadSize(sizeof(Packet));
radio.setPALevel(RF24_PA_LOW);
radio.openWritingPipe(address);
radio.stopListening();
Serial.println("gesture hand: tilt the MPU6050 to drive");
}
void loop() {
sensors_event_t a, g, temp;
mpu.getEvent(&a, &g, &temp);
float ax = a.acceleration.x, ay = a.acceleration.y, az = a.acceleration.z;
Packet packet;
packet.pitch = (int16_t)lround(atan2(-ax, sqrt(ay * ay + az * az)) * DEGREES);
packet.roll = (int16_t)lround(atan2(ay, az) * DEGREES);
bool heard = radio.write(&packet, sizeof(packet));
sent++;
if (!heard) lost++;
if (millis() - lastReport >= 1000) {
lastReport = millis();
Serial.print("pitch ");
Serial.print(packet.pitch);
Serial.print(" roll ");
Serial.print(packet.roll);
Serial.print(" sent ");
Serial.print(sent);
Serial.print(" lost ");
Serial.println(lost);
}
delay(50);
}
// Hand gesture controlled car, the car: an nRF24L01 radio hears the hand's
// tilt from gesturetx, and a 2WD car on an L298N drives it.
//
// radio CE on A1, CSN on A0, SCK 13, MOSI 11, MISO 12, VCC on 3.3V
// L298N ENA pin 5, IN1 pin 6, IN2 pin 7 left motor
// ENB pin 10, IN3 pin 8, IN4 pin 9 right motor
// (both EN jumpers off), 12V from the 7.4 V pack, GND to the Uno
//
// The motor driver takes pins 5 to 10, so the radio's CE and CSN go on A1
// and A0, used as plain digital pins. SPI itself is always 11, 12 and 13 on
// an Uno.
//
// WHAT A TILT MEANS
//
// fingers down (pitch +) forward
// fingers up (pitch -) reverse
// tipped right (roll +) turn right
// tipped left (roll -) turn left
// flat, within DEAD_ZONE stop
//
// Pitch sets the throttle and roll the steering, each scaled from DEAD_ZONE
// (nothing) to FULL_TILT degrees (full speed), and the car mixes them like a
// tank: the left wheel gets throttle plus steering and the right wheel
// throttle minus steering. So a tilt sideways with the hand level spins the
// car on the spot, and forward and right together curve it to the right.
//
// FAILSAFE. If no packet has come for LINK_TIMEOUT ms (the hand switched
// off, out of range, its battery flat) the car stops and says so, and it
// drives again as soon as packets come back.
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
RF24 radio(A1, A0); // CE on A1, CSN on A0
const byte address[6] = "CAR01";
struct Packet {
int16_t pitch;
int16_t roll;
};
const int ENA = 5, IN1 = 6, IN2 = 7; // left motor
const int ENB = 10, IN3 = 8, IN4 = 9; // right motor
const int DEAD_ZONE = 15; // degrees of tilt that mean nothing
const int FULL_TILT = 45; // degrees of tilt for full speed
const unsigned long LINK_TIMEOUT = 500;
unsigned long lastPacket = 0;
bool linked = false;
char moving = ' ';
void side(int en, int a, int b, int s) {
digitalWrite(a, s > 0 ? HIGH : LOW);
digitalWrite(b, s < 0 ? HIGH : LOW);
if (s < 0) s = -s;
analogWrite(en, s == 0 ? 255 : (s > 255 ? 255 : s));
}
void drive(int left, int right) {
side(ENA, IN1, IN2, left);
side(ENB, IN3, IN4, right);
}
// A tilt in degrees as a speed from -255 to 255, nothing inside the dead zone.
int scaled(int degrees) {
int size = abs(degrees);
if (size <= DEAD_ZONE) return 0;
if (size > FULL_TILT) size = FULL_TILT;
int s = map(size, DEAD_ZONE, FULL_TILT, 80, 255);
return degrees < 0 ? -s : s;
}
// One letter for what the car is doing, printed when it changes.
void say(char what) {
if (what == moving) return;
moving = what;
switch (what) {
case 'F': Serial.println("forward"); break;
case 'B': Serial.println("reverse"); break;
case 'L': Serial.println("turn left"); break;
case 'R': Serial.println("turn right"); break;
default: Serial.println("stop"); break;
}
}
void setup() {
pinMode(IN1, OUTPUT);
pinMode(IN2, OUTPUT);
pinMode(IN3, OUTPUT);
pinMode(IN4, OUTPUT);
drive(0, 0);
Serial.begin(115200);
radio.begin();
radio.setPayloadSize(sizeof(Packet));
radio.setPALevel(RF24_PA_LOW);
radio.openReadingPipe(1, address);
radio.startListening();
Serial.println("gesture car: waiting for the hand");
}
void loop() {
if (radio.available()) {
Packet packet;
radio.read(&packet, sizeof(packet));
lastPacket = millis();
if (!linked) {
linked = true;
Serial.println("link up");
}
int throttle = scaled(packet.pitch);
int steer = scaled(packet.roll);
drive(constrain(throttle + steer, -255, 255), constrain(throttle - steer, -255, 255));
if (throttle > 0) say('F');
else if (throttle < 0) say('B');
else if (steer > 0) say('R');
else if (steer < 0) say('L');
else say('S');
}
if (linked && millis() - lastPacket > LINK_TIMEOUT) {
linked = false;
drive(0, 0);
moving = 'S';
Serial.println("signal lost: stopped");
}
}
Parts list
9 parts, plus the jumper wires. They retail for about $75; on Mokxi they're free.
How it is wired
32 connections, pin by pin, read from the circuit itself. Each line is one set of pins joined together, by a jumper wire or a breadboard strip.
- Ground: Arduino Uno R3 (1) pin GND; 2.4 GHz radio, nRF24L01+ (1) pin GND
- Arduino Uno R3 (1) pin 13; 2.4 GHz radio, nRF24L01+ (1) pin SCK
- Arduino Uno R3 (1) pin 12; 2.4 GHz radio, nRF24L01+ (1) pin MISO
- Arduino Uno R3 (1) pin 11; 2.4 GHz radio, nRF24L01+ (1) pin MOSI
- Arduino Uno R3 (1) pin 8; 2.4 GHz radio, nRF24L01+ (1) pin CSN
- Arduino Uno R3 (1) pin 7; 2.4 GHz radio, nRF24L01+ (1) pin CE
- Arduino Uno R3 (1) pin 3V3; 2.4 GHz radio, nRF24L01+ (1) pin VCC
- Ground: Arduino Uno R3 (1) pin GND
- Arduino Uno R3 (1) pin A4; Accelerometer and gyroscope, MPU6050 pin SDA
- Arduino Uno R3 (1) pin A5; Accelerometer and gyroscope, MPU6050 pin SCL
- 5 V: Accelerometer and gyroscope, MPU6050 pin VCC
- Ground: Accelerometer and gyroscope, MPU6050 pin GND
- Ground: Arduino Uno R3 (2) pin GND
- Arduino Uno R3 (2) pin 13; 2.4 GHz radio, nRF24L01+ (2) pin SCK
- Arduino Uno R3 (2) pin 12; 2.4 GHz radio, nRF24L01+ (2) pin MISO
- Arduino Uno R3 (2) pin 11; 2.4 GHz radio, nRF24L01+ (2) pin MOSI
- Arduino Uno R3 (2) pin 10; Motor driver module, L298N pin ENB
- Arduino Uno R3 (2) pin 9; Motor driver module, L298N pin IN4
- Arduino Uno R3 (2) pin 8; Motor driver module, L298N pin IN3
- Arduino Uno R3 (2) pin 7; Motor driver module, L298N pin IN2
- Arduino Uno R3 (2) pin 6; Motor driver module, L298N pin IN1
- Arduino Uno R3 (2) pin 5; Motor driver module, L298N pin ENA
- Arduino Uno R3 (2) pin 3V3; 2.4 GHz radio, nRF24L01+ (2) pin VCC
- Arduino Uno R3 (2) pin A0; 2.4 GHz radio, nRF24L01+ (2) pin CSN
- Arduino Uno R3 (2) pin A1; 2.4 GHz radio, nRF24L01+ (2) pin CE
- Ground: 2.4 GHz radio, nRF24L01+ (2) pin GND
- Motor driver module, L298N pin OUT1; Robot car chassis, 2WD pin L+
- Motor driver module, L298N pin OUT2; Robot car chassis, 2WD pin L-
- 7.4 V: Motor driver module, L298N pin 12V
- Ground: Motor driver module, L298N pin GND
- Motor driver module, L298N pin OUT3; Robot car chassis, 2WD pin R+
- Motor driver module, L298N pin OUT4; Robot car chassis, 2WD pin R-
Questions and answers
How does the car know how my hand is tilted?
The MPU6050 on the hand measures gravity, and the hand’s Uno turns that into pitch and roll angles. It sends them over the nRF24L01 about twenty times a second.
Why does the car not move when my hand is only slightly tilted?
Tilts under 15 degrees are a dead zone, so a hand held roughly flat keeps the car still. Speed rises with the tilt up to full speed at 45 degrees.
What happens if the hand loses power or goes out of range?
The car stops by itself if no packet has arrived for half a second, and drives again as soon as packets come back.
Why are the car radio’s CE and CSN on A1 and A0?
The L298N uses pins 5 to 10, and SPI always takes 11, 12 and 13 on an Uno. The analog pins work as ordinary digital pins for CE and CSN.
Change it and keep it
Open it in the editor, change the circuit or the code, and keep your version in a free account.