Gesture-controlled robot: tilt your hand, the car moves
- Build Logs
- 29 Jul, 2026
Tilt your hand forward and the robot drives forward. Tilt left, it turns left. It is the single most impressive thing you can build for under ₹1,600, and it demonstrates two genuinely useful skills: reading an accelerometer sensibly, and getting a radio link to work.
Both have a well-known failure mode, and both failures get blamed on the wrong thing. Gyroscope drift is not a broken sensor, and a dead nRF24L01 link is usually not a dead module.
What you will need
You are building two devices, so some parts are needed twice.
| Part | Qty | Approx. |
|---|---|---|
| Arduino Nano R3 (compatible) | 2 | ₹418 |
| nRF24L01 2.4GHz Transceiver (pack of 2) | 1 pack | ₹230 |
| MPU-6050 Accelerometer & Gyroscope | 1 | ₹173 |
| L298N Motor Driver Module | 1 | ₹139 |
| 100 RPM Dual Shaft BO Motor with Wheel (4 pcs) | 1 set | ₹420 |
Roughly ₹1,380. You will also want two 10µF capacitors — see below, they are not optional.
The capacitor that makes the radio work
Almost every "my nRF24L01 does not work" post has the same cause. The module draws short current bursts of over 100 mA when it transmits, and the 3.3V regulator on an Arduino Nano is a small thing intended for light loads. The burst drops the rail, the radio browns out mid-packet, and the link is either dead or works only when the two boards are touching.
Solder a 10µF electrolytic capacitor directly across VCC and GND on the module itself, as close to the pins as you can, negative stripe to GND. Do this on both modules before you write a line of code. It converts a module with a two-metre flaky range into one that works across a building.
The other classic mistake: the nRF24L01 is a 3.3V part. VCC goes to the Nano's 3.3V pin, never 5V. The data pins tolerate 5V logic, the supply pin does not.
Wiring, transmitter (the glove)
| From | To |
|---|---|
| MPU6050 VCC / GND | Nano 5V / GND |
| MPU6050 SDA / SCL | Nano A4 / A5 |
| nRF24L01 VCC | Nano 3.3V (with 10µF cap) |
| nRF24L01 GND | Nano GND |
| nRF24L01 CE / CSN | Nano D9 / D10 |
| nRF24L01 SCK / MOSI / MISO | Nano D13 / D11 / D12 |
The receiver is the same radio wiring on the second Nano, plus the L298N on D3, D4, D5, D6 with ENA/ENB on D2 and D7 — and yes, remove the ENA/ENB jumpers.
Why you should use the accelerometer, not the gyroscope
The MPU6050 gives you both, and for hand tilt you want the accelerometer.
A gyroscope measures rate of rotation. To get an angle you integrate it over time, and every tiny reading error accumulates. Within a minute your "level" hand reads as 20° tilted. That is drift, and it is inherent to integration — no amount of calibration removes it, which is why people conclude the chip is faulty.
An accelerometer measures acceleration, and at rest the only acceleration is gravity — which always points down. Work out where down is relative to the chip and you have an absolute tilt angle that never drifts. It is noisier, but noise is easy to filter and drift is not.
#include <Wire.h>
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
RF24 radio(9, 10);
const byte ADDRESS[6] = "RBD01";
struct Packet { int8_t x; int8_t y; }; // keep it tiny
Packet out;
const int MPU = 0x68;
void setup() {
Wire.begin();
Wire.beginTransmission(MPU);
Wire.write(0x6B); Wire.write(0); // wake it up
Wire.endTransmission(true);
radio.begin();
radio.openWritingPipe(ADDRESS);
radio.setPALevel(RF24_PA_LOW); // LOW while testing on a bench
radio.setDataRate(RF24_250KBPS); // slower = more range, less bandwidth
radio.stopListening();
}
void loop() {
Wire.beginTransmission(MPU);
Wire.write(0x3B); // accel data start
Wire.endTransmission(false);
Wire.requestFrom(MPU, 6, true);
int16_t ax = Wire.read() << 8 | Wire.read();
int16_t ay = Wire.read() << 8 | Wire.read();
int16_t az = Wire.read() << 8 | Wire.read();
// Tilt angles from gravity. Never drifts, because it is not integrated.
float pitch = atan2(ay, sqrt((float)ax * ax + (float)az * az)) * 180.0 / PI;
float roll = atan2(-ax, (float)az) * 180.0 / PI;
// Exponential smoothing kills the jitter without adding real lag.
static float sp = 0, sr = 0;
sp = sp * 0.8 + pitch * 0.2;
sr = sr * 0.8 + roll * 0.2;
out.x = constrain((int)sr, -90, 90);
out.y = constrain((int)sp, -90, 90);
radio.write(&out, sizeof(out));
delay(50); // 20 Hz is plenty; faster just floods the link
}
The receiver, and the dead-man timeout
#include <SPI.h>
#include <nRF24L01.h>
#include <RF24.h>
RF24 radio(9, 10);
const byte ADDRESS[6] = "RBD01";
struct Packet { int8_t x; int8_t y; };
Packet in;
const int DEADZONE = 15; // degrees of hand wobble to ignore
const int MAX_TILT = 45; // tilt beyond this = full speed
unsigned long lastPacket = 0;
const unsigned long LINK_TIMEOUT = 500;
void setup() {
radio.begin();
radio.openReadingPipe(0, ADDRESS);
radio.setPALevel(RF24_PA_LOW);
radio.setDataRate(RF24_250KBPS);
radio.startListening();
// ... motor pinModes ...
}
void loop() {
if (radio.available()) {
radio.read(&in, sizeof(in));
lastPacket = millis();
}
// SAFETY: if the glove goes out of range or its battery dies,
// stop. Without this the robot keeps its last command forever
// and drives into a wall at full speed.
if (millis() - lastPacket > LINK_TIMEOUT) { stopMotors(); return; }
int fwd = abs(in.y) < DEADZONE ? 0 : in.y;
int turn = abs(in.x) < DEADZONE ? 0 : in.x;
int speed = map(abs(fwd), DEADZONE, MAX_TILT, 60, 255);
speed = constrain(speed, 0, 255);
int left = speed, right = speed;
if (turn > 0) left = speed - map(abs(turn), DEADZONE, MAX_TILT, 0, speed);
else if (turn < 0) right = speed - map(abs(turn), DEADZONE, MAX_TILT, 0, speed);
if (fwd > 0) drive(left, right, true);
else if (fwd < 0) drive(left, right, false);
else if (turn != 0) spin(turn > 0);
else stopMotors();
}
The dead-man timeout is the most important thing in this sketch. Radio links drop. Without a timeout the robot holds its last received command indefinitely, so a glove that walks out of range leaves a robot driving flat out with nobody controlling it. Half a second of silence should stop it.
The deadzone matters almost as much. A human hand is never still. Without a 15° band of "this counts as level", the robot creeps constantly and feels broken.
Things that go wrong
No packets at all. In order of likelihood: missing capacitor, nRF24L01 on 5V, mismatched addresses, mismatched data rate between the two ends. Both radios must agree on setDataRate and setPALevel.
Works at 20 cm, dies at 2 m. Capacitor. Every time.
Robot creeps when the glove is level. Deadzone too small, or the MPU6050 was not level when you powered it. Add a zeroing step at startup: average 100 readings and subtract that offset.
Control feels laggy. Smoothing factor too aggressive. Raise 0.2 toward 0.4 for a snappier, noisier response.
MPU6050 reads all zeros. You did not wake it. The chip boots into sleep mode, which is what writing 0 to register 0x6B undoes.
Range is poor even with the capacitor. 2.4 GHz is the same band as WiFi. Try a different channel with radio.setChannel(108), which is above most WiFi traffic.
Where to take it next
Send data back the other way — the nRF24L01 is a transceiver, not a transmitter, so the robot can report battery voltage or an ultrasonic distance back to the glove and buzz when you are about to hit something. Combining this with the obstacle detection from our scanning robot build gives you a robot that overrides its operator, which is a genuinely interesting control problem.
Talk to us if your link will not come up — send a photo of the module and we will tell you whether the capacitor is on the right pins.