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
| 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.
Try It Yourself 15 min
Goal: Add a horn — buzzer on D11 with the tone() wrapper. Command H plays a 200 ms beep.
Hint
Same as L03-28 Easy task. Don't forget pinMode(11, OUTPUT).
Goal: Auto-reverse on obstacle: instead of just stopping, the chassis briefly backs up 0.5 s after detecting an obstacle.
Hint
Don't use delay(500) — block-free. Add a state machine: OBSTACLE_REVERSING with an end-time. While in that state, ignore F commands. When end-time passes, transition to IDLE.
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.
Hint
For each "side": drive forward 1 s; spin right for the time it takes to turn (360/n) degrees. Calibrate the spin-time-per-degree empirically — drive a known turn (say 90°), time it, store the ratio. Different surfaces (carpet vs hardwood) need different calibration.
Mini-Challenge · Demo + measure 10 min
- Tape an obstacle course on the floor — start line, finish line, a couple of obstacles.
- Time yourself driving from start to finish with the phone.
- Record a 60-second video showing: forward → obstacle stop → reverse-arc-around → finish.
- Note in your notebook: total course time, number of times obstacle stop fired, anything that surprised you about v2 vs v1.
If you finish early, swap chassis with a classmate and drive each other's. Comparing build quality and code feel is the fastest way to spot polishing opportunities.
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°".
Homework 5 min
- Finish v2. Video. Bring to next class.
- Save as
bt-car-v2.ino+ the supporting class headers. - Read ahead to ARD-L03-44 (WiFi Smart-Plant Monitor). Bring a soil moisture probe + an ESP + an LDR. We'll wire a plant data logger with email alerts.
Bring back next class:
- Working v2 car + video.
- ESP + soil moisture probe.