Learning Goals 5 min
L03-28 was "the chassis works". v2 is "the chassis works well" — PWM-throttled speed, gentle turns, obstacle avoidance, fancier phone control. You'll combine Cluster B (motors), Cluster E (Bluetooth) and Cluster H (classes) into one polished build. By the end of this lesson you will:
- Refactor v1's flat sketch into a class-driven architecture:
Motorclass,Vehicleclass,BluetoothLinkclass, plus aRangeFinderfrom L02-21. - Add PWM speed control as a continuous variable from the phone's slider, plus gentle turn behaviour (one wheel slower while moving forward).
- Add an ultrasonic obstacle-stop: if anything is within 15 cm in front while driving forward, stop and report.
Warm-Up 10 min
The chassis from L03-28 plus an HC-SR04 ultrasonic sensor mounted on the front, pointing forward.
| Wire | UNO pin |
|---|---|
| HC-SR04 TRIG | A1 (analog pin used as digital) |
| HC-SR04 ECHO | A0 |
| L298N IN1 / IN2 / ENA (left) | D4 / D7 / D5 |
| L298N IN3 / IN4 / ENB (right) | D8 / D9 / D6 |
| HC-05 RX / TX | D2 / D3 via SoftwareSerial (with divider on TX→RX) |
| Battery / common ground | as L03-28 |
Mount the ultrasonic sensor at the front of the chassis, ~5–10 cm above the ground.
New Concept · Architecture in classes 20 min
Four classes
A0 and A1 — analog pins used as plain digital ones, because the digital side of the board has run out.| Class | Responsibility |
|---|---|
Motor | One physical motor + L298N IN1/IN2/EN. Methods: setSpeed(int duty), brake(). |
Vehicle | Two Motors + a drive duty + a trim. Methods: forward(), backward(), spinLeft(), spinRight(), arcLeft(), arcRight(), stop(), setSpeed(int duty). |
RangeFinder | HC-SR04 wrapper from L02-22/23. readCm() returns current distance, or -1 if no echo. |
BluetoothLink | Wraps SoftwareSerial + parser + watchdog. Method poll(); emits commands via a callback or polled queue. |
The .ino is now mostly setup() + loop() that talks to these four objects.
Command protocol (v2)
| Phone sends | Action |
|---|---|
F / B | Drive forward / backward at current speed |
L / R | Spin in place left / right |
S | Stop |
SPEED 200 | Set driving duty 0..255 |
ARC L / ARC R | Forward arc with inside wheel slower |
? | Status reply |
Same shape as L03-25; new keyword ARC for gentle turns.
Obstacle stop policy
Run RangeFinder.readCm() every 100 ms. If currently driving forward AND distance < 15 cm, force stop and emit !OBSTACLE message. Re-allow forward only when distance > 25 cm (hysteresis).
Worked Example · The v2 sketch 30 min
Step 1 — assume the four header files exist
You wrote Motor.h and Blinker.h in L03-41/42. RangeFinder.h and BluetoothLink.h are similar wrappers — write them or download from your personal library. Skeleton:
// RangeFinder.h
class RangeFinder {
public:
RangeFinder(int trig, int echo) : trig_(trig), echo_(echo) {}
void begin() { pinMode(trig_, OUTPUT);
pinMode(echo_, INPUT);
}
float readCm() {
digitalWrite(trig_, LOW);
delayMicroseconds(2);
digitalWrite(trig_, HIGH);
delayMicroseconds(10);
digitalWrite(trig_, LOW);
unsigned long w = pulseIn(echo_, HIGH, 25000);
if (w == 0) return -1;
return w / 58.0;
}
private:
int trig_, echo_;
};Step 2 — the main sketch
// L03-43 · Bluetooth Robot Car v2
#include <SoftwareSerial.h>
#include "Motor.h"
#include "RangeFinder.h"
Motor motorL(4, 7, 5);
Motor motorR(8, 9, 6);
RangeFinder front(A1, A0);
SoftwareSerial bt(2, 3);
int driveDuty = 200;
unsigned long lastCmdAt = 0;
unsigned long lastRangeAt = 0;
float lastDistCm = -1;
bool obstaclePresent = false;
String line, lastDir = "S";
void driveForward() { motorL.setSpeed( driveDuty);
motorR.setSpeed( driveDuty);
lastDir = "F";
}
void driveBackward() { motorL.setSpeed(-driveDuty);
motorR.setSpeed(-driveDuty);
lastDir = "B";
}
void spinLeft() { motorL.setSpeed(-driveDuty);
motorR.setSpeed( driveDuty);
lastDir = "L";
}
void spinRight() { motorL.setSpeed( driveDuty);
motorR.setSpeed(-driveDuty);
lastDir = "R";
}
void stopAll() { motorL.setSpeed(0);
motorR.setSpeed(0);
lastDir = "S";
}
void arcLeft() { motorL.setSpeed( driveDuty/2);
motorR.setSpeed( driveDuty);
lastDir = "AL";
}
void arcRight() { motorL.setSpeed( driveDuty);
motorR.setSpeed( driveDuty/2);
lastDir = "AR";
}
void applyDir(char d) {
switch (d) {
case 'F': driveForward();
break;
case 'B': driveBackward();
break;
case 'L': spinLeft();
break;
case 'R': spinRight();
break;
case 'S': default: stopAll();
}
}
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);
}
return;
}
if (cmd == "ARC L") { arcLeft();
bt.println("OK ARC L");
return;
}
if (cmd == "ARC R") { arcRight();
bt.println("OK ARC R");
return;
}
if (cmd == "?") {
bt.print("dir=");
bt.print(lastDir);
bt.print(" speed=");
bt.print(driveDuty);
bt.print(" dist=");
bt.println(lastDistCm);
return;
}
applyDir(cmd[0]);
bt.print("OK ");
bt.println(lastDir);
}
void readBluetooth() {
while (bt.available()) {
char c = bt.read();
lastCmdAt = millis();
if (c == 'S' || c == 's') { stopAll();
line = "";
bt.println("OK STOP");
continue;
}
if (c == '\r') continue;
if (c == '\n') { dispatch(line);
line = "";
continue;
}
if (line.length() == 0 && (c == 'F' || c == 'B' || c == 'L' || c == 'R')) { applyDir(c);
continue;
}
line += c;
if (line.length() > 32) line = "";
}
}
void readRange() {
if (millis() - lastRangeAt < 100) return;
lastRangeAt = millis();
float d = front.readCm();
lastDistCm = d;
bool wasObstacle = obstaclePresent;
if (d > 0 && d < 15) obstaclePresent = true;
else if (d > 25 || d < 0) obstaclePresent = false;
if (obstaclePresent && !wasObstacle) {
if (lastDir == "F" || lastDir == "AL" || lastDir == "AR") {
stopAll();
bt.println("!OBSTACLE - stop");
}
}
}
void setup() {
Serial.begin(9600);
bt.begin(9600);
front.begin();
stopAll();
bt.println("# Robot car v2 armed. F/B/L/R/S, SPEED 0..255, ARC L/R, ?");
}
void loop() {
readBluetooth();
readRange();
// Watchdog: stop if no command for 300 ms in joystick mode
if (millis() - lastCmdAt > 300 && lastDir != "S") {
stopAll();
bt.println("# watchdog -> STOP");
}
// Refuse forward while obstacle is too close
if (obstaclePresent && (lastDir == "F" || lastDir == "AL" || lastDir == "AR")) {
stopAll();
}
}Step 3 — bench test
Wheels off the floor. Pair phone. Test every command. With the chassis held in the air, wave your hand < 15 cm in front of the sensor → the watchdog plus obstacle stop fires.
Step 4 — floor test, fast lap
Set on floor with a clear lane. Drive forward with F. Place an obstacle 30 cm in front. The chassis approaches, the obstacle-detection fires at 15 cm, the chassis stops. Phone shows !OBSTACLE - stop. Move the obstacle away → it stays stopped (still lastDir = S from the auto-stop). Press F again → chassis resumes.
Step 5 — speed test
Try SPEED 100 (slow), SPEED 200 (default), SPEED 255 (max). Notice the difference; recall the dead band from L03-09 means speeds below ~50 won't start the motors from stop.
Step 6 — arcing test
ARC L while moving forward → chassis curves left. ARC R → curves right. Combined with obstacle stop, you can drive a smooth curved path around obstacles.
Basic 5 min
Goal: Add a horn — buzzer on D11 with the tone() wrapper. Command H plays a 200 ms beep.
Challenge 1 5 min
Goal: Auto-reverse on obstacle: instead of just stopping, the chassis briefly backs up 0.5 s after detecting an obstacle.
Challenge 2 5 min
Goal: Add a POLY n command that drives a regular polygon: POLY 4 = square, POLY 6 = hexagon. Use the trim + spin-time calibration to get tolerable angles.
Challenge 3 · Will it stop in time? 10 min
The sketch stops the car at 15 cm. But the car keeps moving while it waits for the next reading, and it coasts after the motors stop. Measure, calculate, then test.
- At
SPEED 255, time the car over 1 metre. Work out its speed in cm per second. readRange()runs every 100 ms. Calculate the worst-case distance the car moves between two readings.- Drive at full speed and send
Sas it passes a mark on the floor. Measure how far it coasts. - Calculate a safe stop distance: reaction distance + coasting distance + a 5 cm margin. Round up to a whole number.
- Change the 15 in
readRange()to your number. Keep the re-arm distance 10 cm above it.
It works if the car drives at full speed towards a box and stops at least 5 cm short of it, three runs out of three.
Recap 5 min
v2 = v1 + class architecture + PWM + obstacle stop. The class refactor means the .ino stays small even as features grow. Obstacle stop is a tiny safety layer that transforms the toy from "guided missile" to "sensible robot". Cluster I's pattern: take an earlier project, scale it with Cluster H ideas, ship a finished v2. Tomorrow: the plant monitor.
- v2 / iteration
- The polish pass on an earlier build. New features, cleaner architecture, real-world testing.
- Class architecture
- Project structure where each major concept (Motor, Vehicle, Sensor) is its own class. The
.inojust wires them together. - Joystick mode
- Single-character commands at ~10 Hz. Watchdog stops motors if input stops. From L03-28.
- Obstacle stop
- Hard rule: if a sensor reads too close in the direction of travel, stop. With hysteresis so the chassis doesn't oscillate.
- Hysteresis
- Different thresholds for entering and leaving a state. < 15 cm = obstacle; > 25 cm = clear. The 10 cm gap prevents flickering near the threshold.
- Arcing turn
- One wheel slower than the other while moving forward. Produces a smooth curve rather than an in-place pivot.
- Calibration
- Measuring real-world response (e.g. spin-time per degree) and storing as a constant. The bridge between "the code thinks it's 90°" and "the chassis actually turned 90°".
Extra Mission 5 min
Part 1 — Design a new driving mode
Your v2 car can drive, arc and stop for walls. Design one new mode that makes it more useful or more fun. Ideas: a parking mode that creeps up to a wall and stops at 10 cm, a patrol mode that drives back and forth, a race mode with a countdown start.
Your design must include:
- The mode's name and the Bluetooth command that starts it and stops it.
- A state diagram: every state, and what moves the car from one to the next.
- Which distances, speeds and times it uses, as numbers.
- How the watchdog and the obstacle stop still keep it safe.
Part 2 — Make it
Add your mode to bt-car-v2.ino as a non-blocking state machine, with no delay(). Test it with the wheels off the floor, then on a clear floor with a taped start and finish line.
Bring back next class: your design, your uploaded sketch with its class headers, and a 60-second video of your mode running. Also bring an ESP and a soil moisture probe for L03-44.