Learning Goals 5 min
Cluster E ends with the project everyone wants to build: a phone-controlled robot car. You already have all the pieces — L298N + chassis (L03-10), HC-05 + level shifter (L03-23), command protocol (L03-25). Today you combine them, install a real joystick-style app on your phone, and drive a robot around your living room from a remote. By the end of this lesson you will:
- Wire and configure a fully untethered 2WD robot: chassis + L298N + HC-05 + UNO + battery, no USB cable required.
- Use a published Android "Arduino BT Car Controller"-style app — or Adafruit Bluefruit Connect on iOS / Android — to drive with a touch joystick / d-pad.
- Add real-time speed throttling via a slider, plus an emergency stop that overrides everything, plus a heartbeat that times out the motors if the phone disconnects.
Warm-Up 10 min
The full BOM (Bill of Materials) for today:
- 2WD chassis (L03-10 build).
- 2 × DC gearmotors.
- L298N motor driver.
- HC-05 Bluetooth module (renamed and PIN-set from L03-24).
- 1 kΩ + 2 kΩ resistors for the HC-05 RX level shift.
- 4 × AA pack for motors.
- Arduino UNO + USB cable (only for upload / fallback power).
- Android phone with "Serial Bluetooth Terminal" OR "Arduino BT Car Controller" OR Adafruit Bluefruit Connect installed.
App choices
| App | OS | Style |
|---|---|---|
| Serial Bluetooth Terminal | Android | Typed commands — works with our L03-25 protocol as-is. |
| Arduino Bluetooth Controller / RC Bluetooth | Android | Joystick / d-pad / button apps that send single characters (F B L R S) on press. |
| Bluefruit Connect | Android + iOS | Adafruit's app — Controller view has D-pad. BUT requires BLE module, not HC-05. Use only with BLE peripheral from L03-26/27. |
For HC-05: use Serial Bluetooth Terminal or an Arduino-Car app. For BLE peripherals, use Bluefruit Connect or nRF Connect. Today's build assumes HC-05.
New Concept · Connection-aware safety 20 min
The new ideas vs L03-25
- Live joystick mode: continuous "F"/"B"/"L"/"R" characters arrive every ~50–100 ms while you're pressing. When you release, the app stops sending — but our existing sketch keeps the last command active forever. Solution: implement a per-command timeout. If no command for > 300 ms, stop the motors.
- Connection drop = stop. If the phone goes out of range mid-drive, the chassis would coast forever and crash. Solution: the HC-05's STATE pin (or a heartbeat timeout) detects disconnect → stop.
- Speed slider: a SPEED command updates the duty for subsequent F/B/L/R, just like L03-25. Live throttling.
The watchdog pattern
unsigned long lastCmdAt = 0;
const unsigned long CMD_TIMEOUT_MS = 300;
void loop() {
// ... existing parse + dispatch ...
// Auto-stop if no command arrived recently
if (millis() - lastCmdAt > CMD_TIMEOUT_MS) {
stopAll();
}
}
void dispatch(...) {
lastCmdAt = millis(); // reset watchdog on every received command
// ... actual dispatch ...
}So the car's default state, in the absence of input, is "stopped". Hold the d-pad arrow → app sends F at 10 Hz → watchdog stays reset → motor runs. Release → app stops sending → 300 ms later → motor stops. Natural "dead-man's switch" behaviour.
Heartbeat for connection-loss detection
Many phone-side car-controller apps DO send a heartbeat character (often H or K) so the robot knows the connection is alive. We treat any character as a heartbeat — receiving F or even an unknown char counts. The CMD_TIMEOUT_MS handles both "no input from user" and "phone disconnected" with one mechanism.
Architecture diagram
The complete sketch has three concurrent jobs in one loop():
- Bluetooth parser: drain incoming bytes, dispatch.
- Watchdog: stop motors if no command for 300 ms.
- Heartbeat output: send "# alive" every 2 s so the phone screen confirms the link.
All three non-blocking, all sharing the same loop. The pattern we've been building toward since L02-35.
Worked Example · The full chassis sketch 30 min
Step 1 — assemble
The chassis from L03-10 with the HC-05 from L03-25, all wired as before. Battery pack at the back, Arduino in the middle, HC-05 forward (the antenna trace likes a clear line-of-sight).
Step 2 — the production sketch
// L03-28 · Bluetooth-controlled robot car (HC-05 + L298N + 2WD chassis)
// Single-char drive commands: F B L R S
// Keyword config: SPEED nnn (0..255), STOP (alias for S)
// Watchdog: if no command for CMD_TIMEOUT_MS, motors stop.
// Heartbeat: '# alive' every 2 s to the phone.
#include <SoftwareSerial.h>
#include "motor.h"
const int BT_RX = 2, BT_TX = 3;
SoftwareSerial bt(BT_RX, BT_TX);
const Motor motorL = {9, 8, 5};
const Motor motorR = {7, 6, 11};
int driveDuty = 200;
String line;
const unsigned long CMD_TIMEOUT_MS = 300;
const unsigned long HEARTBEAT_MS = 2000;
unsigned long lastCmdAt = 0;
unsigned long lastHeartbeat = 0;
String lastDir = "S";
void driveForward() { motorSpeed(motorL, driveDuty);
motorSpeed(motorR, driveDuty);
}
void driveBackward() { motorSpeed(motorL, -driveDuty);
motorSpeed(motorR, -driveDuty);
}
void spinLeft() { motorSpeed(motorL, -driveDuty);
motorSpeed(motorR, driveDuty);
}
void spinRight() { motorSpeed(motorL, driveDuty);
motorSpeed(motorR, -driveDuty);
}
void stopAll() { motorSpeed(motorL, 0);
motorSpeed(motorR, 0);
}
void applyDir(char d) {
switch (d) {
case 'F': case 'f': driveForward();
lastDir = "F";
break;
case 'B': case 'b': driveBackward();
lastDir = "B";
break;
case 'L': case 'l': spinLeft();
lastDir = "L";
break;
case 'R': case 'r': spinRight();
lastDir = "R";
break;
case 'S': case 's':
default: stopAll();
lastDir = "S";
break;
}
}
void dispatch(const String& cmd) {
if (cmd.length() == 0) return;
lastCmdAt = millis();
if (cmd.startsWith("SPEED ")) {
int n = cmd.substring(6).toInt();
if (n >= 0 && n <= 255) {
driveDuty = n;
bt.print("OK SPEED ");
bt.println(n);
} else bt.println("BAD SPEED");
return;
}
if (cmd.equalsIgnoreCase("STOP")) { stopAll();
lastDir = "S";
bt.println("OK STOP");
return;
}
applyDir(cmd[0]);
bt.print("OK ");
bt.println(lastDir);
}
void setup() {
Serial.begin(9600);
bt.begin(9600);
motorInit(motorL);
motorInit(motorR);
stopAll();
bt.println("# BT Car armed. F B L R S | SPEED 0..255 | STOP");
}
void loop() {
unsigned long now = millis();
// 1. Drain incoming bytes
while (bt.available()) {
char c = bt.read();
lastCmdAt = now; // any byte resets the watchdog
// Emergency-stop
if (c == 'S' || c == 's') {
stopAll();
lastDir = "S";
line = "";
bt.println("OK STOP (immediate)");
continue;
}
if (c == '\r') continue;
if (c == '\n') { dispatch(line);
line = "";
continue;
}
// Joystick mode — single-char direction with no terminator
if (line.length() == 0 && (c == 'F' || c == 'B' || c == 'L' || c == 'R' ||
c == 'f' || c == 'b' || c == 'l' || c == 'r')) {
applyDir(c);
continue;
}
line += c;
if (line.length() > 32) line = "";
}
// 2. Watchdog
if (now - lastCmdAt > CMD_TIMEOUT_MS && lastDir != "S") {
stopAll();
lastDir = "S";
bt.println("# watchdog -> STOP");
}
// 3. Heartbeat
if (now - lastHeartbeat >= HEARTBEAT_MS) {
lastHeartbeat = now;
bt.print("# alive speed=");
bt.print(driveDuty);
bt.print(" dir=");
bt.println(lastDir);
}
}Step 3 — bench test (wheels off the floor)
- Power up. Pair the phone. Open Serial Bluetooth Terminal at 9600 baud, line ending "None".
- Press "F". Both wheels spin forward.
- Wait 1 second without sending anything. Within 300 ms, the watchdog fires and wheels stop.
- Repeat with "B", "L", "R" — all should briefly spin then auto-stop.
- Type
SPEED 100\n. Then press "F". Slower spin.
Step 4 — install a controller app (Android)
On the Play Store, search "Arduino Bluetooth Controller" or "RC Bluetooth". Most send single chars on button press and resend at ~10 Hz while held. Configure: F, B, L, R, S as buttons. Connect to your HC-05. Press and hold "forward" → the chassis drives; release → it stops (watchdog).
Step 5 — drive on the floor
Set the chassis on a clear floor. USB cable disconnected. Power: the L298N's 5V regulator (with the 5V-enable jumper installed) feeds the UNO. The HC-05 takes its 5V from the same rail.
Drive around. Notice:
- Press-and-hold = drive; release = stop.
- If you walk out of range, the connection drops and the chassis stops within ~1 s (Bluetooth disconnect + watchdog).
- Type
SPEED 255for max speed bursts;SPEED 120for fine maneuvering.
Step 6 — emergency stop test
Drive forward at full speed. Tap "S" in your app. Wheels stop instantly — the 'S' bypasses the line buffer and fires immediately. Don't skip this test; you need to trust it before driving in tight spaces.
Basic 5 min
Goal: Add a horn — a buzzer on D12, sounded for 200 ms when the app sends H.
Challenge 1 5 min
Goal: Add "gentle turn" while driving. Sending FL arcs left while moving forward; FR arcs right. Implement as a two-character recognition (first char direction, second char turn).
Challenge 2 5 min
Goal: Add an HC-SR04 ultrasonic sensor (L02-21) facing forward. While driving forward, if the distance reading is < 15 cm, override and stop. Print "# OBSTACLE" to the phone.
Challenge 3 · Fix the watchdog 10 min
A classmate wrote a cut-down car sketch with a watchdog. It only knows F and S. Two things go wrong:
- Holding
Fon the app, the wheels only twitch. - After fixing that, closing the app leaves the car driving forever.
#include <SoftwareSerial.h>
#include "motor.h"
SoftwareSerial bt(2, 3);
const Motor motorL = {9, 8, 5};
const Motor motorR = {7, 6, 11};
const int DRIVE_DUTY = 200;
const unsigned long CMD_TIMEOUT_MS = 300;
unsigned long lastCmdAt = 0;
bool moving = false;
void stopAll() {
motorSpeed(motorL, 0);
motorSpeed(motorR, 0);
moving = false;
}
void setup() {
bt.begin(9600);
motorInit(motorL);
motorInit(motorR);
stopAll();
}
void loop() {
unsigned long now = millis();
lastCmdAt = now;
while (bt.available()) {
char c = bt.read();
if (c == 'F') {
motorSpeed(motorL, DRIVE_DUTY);
motorSpeed(motorR, DRIVE_DUTY);
moving = true;
}
if (c == 'S') {
stopAll();
}
}
if (now - lastCmdAt < CMD_TIMEOUT_MS && moving) {
stopAll();
bt.println("# watchdog -> STOP");
}
}Find both bugs. Explain which symptom each one causes. Fix them, upload, and test with the wheels off the floor.
It works if holding F drives smoothly, and the wheels stop within 300 ms of letting go or closing the app.
Recap 5 min
Cluster E ends with a real robot. The new ideas compared to L03-10's wired version were all about safety in the wireless world: a watchdog so "no command" means "stop", an emergency stop that bypasses buffering, a heartbeat for confirming the link, and connection-loss detection via the same watchdog. The bigger Cluster E lesson: Bluetooth Classic is great for Android phones streaming joystick commands; BLE is better for iOS, sensors, and battery-powered builds. We'll come back to this robot in L03-43 (Build) with PWM speed, BLE app, and obstacle avoidance. Next cluster: leave Bluetooth's 10 m range behind and step onto the internet with WiFi.
- Watchdog timer
- A piece of code that automatically stops or resets the system if it doesn't see an expected event within a deadline. For our car: if no Bluetooth byte for 300 ms, motors stop.
- Dead-man's switch
- Any control scheme where action requires continuous input. Press-and-hold drives the car; release stops it. Standard in tools, vehicles, industrial machines.
- Emergency stop
- A path that bypasses normal command processing and immediately halts actuators. Should work from any input state — half-typed buffer included.
- Heartbeat
- Periodic message from the device confirming "I'm alive and you're connected". Useful for the operator who can't see the device.
- Joystick mode
- Single-character commands arriving at high rate (~10/s) for as long as the user holds a button. Contrasts with line-buffered "type a verb + Enter" mode.
- Connection-loss detection
- The mechanism that notices the phone has disconnected. Cheapest path: same watchdog as the no-input one (no bytes for N ms = disconnect = stop).
- Bench test vs floor test
- Bench test = chassis on a desk, wheels free, you can grab it. Floor test = chassis on the floor, can crash. Always bench-test new code first.
- Ship-ready
- Another person can use the device without your instructions. Cluster E's capstone bar — same as Cluster A (turntable) and Cluster B (rover).
Extra Mission 5 min
Part 1 — Design a safe remote-controlled machine
Pick a machine that would be useful to drive from a phone: a toy crane, a camera dolly, a line-marking cart, a pet-treat launcher. On paper, design how it stays safe when something goes wrong.
Your design must include:
- The machine's name, its one job, and its command list.
- What it does when the user lets go of a button.
- What it does when the phone goes out of range, and how fast.
- An emergency stop, and the byte that triggers it.
- One extra safety sensor and the reading that makes it stop.
Part 2 — Make it
Add your design's safety rules to the car: the watchdog, the emergency stop, and your extra safety sensor (for example the HC-SR04 from Challenge 2). Test each one with the wheels off the floor first, then on a clear floor.
Bring back next class: your design, your uploaded sketch, and a short video showing each safety rule stopping the car.