Learning Goals 5 min
L03-23 showed you single-character commands (1 / 0 for an LED). L03-24 let you personalise the module. Today you design a richer command protocol — multi-letter commands with optional arguments — and wire it to motors so the phone can drive a chassis. By the end of this lesson you will:
- Pick a wire format for commands: single chars (
F,B,L,R,S) vs CSV strings (SET 180,75) — and explain when each is right. - Implement a line-buffered parser that accumulates characters until
\narrives, then dispatches on the first token. - Drive the L298N + two-wheel chassis from L03-10 by Bluetooth commands typed on a phone — the prep build for the BLE-controlled car (L03-28).
Warm-Up 10 min
Wire your existing pieces together:
- HC-05 from L03-23 — SoftwareSerial on D2 / D3, level-shifter on RX.
- L298N + two motors from L03-08 / L03-09 — IN1..IN4 + ENA/ENB on D4..D9 (avoid D2/D3).
- Common ground between battery, L298N, HC-05, UNO.
A 2WD chassis is ideal — even a cardboard mockup with two motors and wheels works for the demo.
Pre-class question
Your phone sends "FORWARD". What's the smallest sketch you could write to react to that string?
Reveal
Read characters one at a time into a String buffer. When you see \n (end-of-line), compare the buffer against FORWARD, BACKWARD, etc., and dispatch. Reset the buffer. The line-buffered parser pattern, which we'll write in §3.
Don't try to react to characters as they arrive — your sketch will see 'F', then 'O', then 'R'… and may dispatch on 'F' before reading the rest. Always wait for the terminator.
New Concept · Command protocols 25 min
Two common command styles
| Style | Example | Pros | Cons |
|---|---|---|---|
| Single-character | F, B, L, R, S | Tiny code; instant response; no parser | Hard to add arguments; cryptic to humans typing manually |
| Line-buffered keyword + args | FWD 150, TURN 90, STOP | Readable; takes parameters; extensible | Needs a parser; per-line dispatch latency (one full line per command) |
Rule of thumb: single-character for live joystick-style control where every ms counts (a phone joystick app sending 'F' or 'S' ten times a second). Line-buffered for "config" or "step" commands where you want arguments and clarity.
The line-buffered parser pattern
String line; // accumulator
void readLineFromBT() {
while (bt.available()) {
char c = bt.read();
if (c == '\r') continue; // ignore CR; we look for LF
if (c == '\n') {
dispatch(line);
line = "";
return;
}
line += c;
if (line.length() > 64) line = ""; // protect against runaway input
}
}dispatch(line) is your handler. It receives a single complete line (no terminator) and does whatever the command means. Two implementations, one per command style:
Single-character dispatch
void dispatch(const String& cmd) {
if (cmd.length() == 0) return;
switch (cmd[0]) {
case 'F': forward();
break;
case 'B': backward();
break;
case 'L': spinLeft();
break;
case 'R': spinRight();
break;
case 'S': stop();
break;
default: bt.print("Unknown: ");
bt.println(cmd);
}
}Easy to extend — add a new case. Easy to debug — print the byte you didn't recognise.
Keyword + args dispatch
void dispatch(const String& cmd) {
int sp = cmd.indexOf(' ');
String verb = (sp == -1) ? cmd : cmd.substring(0, sp);
String args = (sp == -1) ? "" : cmd.substring(sp + 1);
verb.toUpperCase(); // accept "fwd", "FWD", "Fwd" etc.
if (verb == "FWD") forwardArg(args.toInt()); // e.g. "FWD 150"
else if (verb == "REV") backwardArg(args.toInt());
else if (verb == "TURN") turnArg(args.toInt()); // "TURN 45" = 45 deg right
else if (verb == "STOP") stop();
else {
bt.print("Unknown verb: ");
bt.println(verb);
}
}The args string is the rest of the line after the first space. For multiple args, split by comma: FWD 150,300 meaning "forward at duty 150 for 300 ms".
Choose your terminator
Phone Bluetooth-serial apps usually send \r\n, \n, or nothing at all depending on the user's settings. Make your parser tolerant: ignore \r, dispatch on \n, and provide a timeout fallback so a half-line doesn't sit in the buffer forever:
unsigned long lastByteAt = 0;
const unsigned long LINE_TIMEOUT_MS = 200;
void loop() {
while (bt.available()) {
char c = bt.read();
lastByteAt = millis();
if (c == '\r') continue;
if (c == '\n') { dispatch(line);
line = "";
continue;
}
line += c;
}
// dispatch on stalled buffer (user's app didn't send a newline)
if (line.length() > 0 && (millis() - lastByteAt) > LINE_TIMEOUT_MS) {
dispatch(line);
line = "";
}
}So the protocol accepts both FWD 150\n and FWD 150 followed by a 200 ms pause. Friendly to human typing in random terminal apps.
Acknowledging commands
Echo back what you did so the phone's screen confirms each command:
void forwardArg(int duty) {
if (duty <= 0) duty = 200;
if (duty > 255) duty = 255;
setMotor(duty, duty);
bt.print("OK FWD ");
bt.println(duty);
}This is essential when there's no other feedback — the user can't see the robot if it's in another room.
Worked Example · Phone-driven chassis 25 min
Step 1 — wire it
| Component | UNO pin |
|---|---|
| HC-05 VCC, GND | 5V, GND |
| HC-05 TXD | D2 (SW Serial RX) |
| HC-05 RXD | D3 via 1 kΩ / 2 kΩ divider |
| L298N IN1, IN2, ENA | D9, D8, D5 |
| L298N IN3, IN4, ENB | D7, D6, D11 |
| L298N VS, GND | 4 × AA pack |
| L298N GND | Arduino GND |
| Motors | OUT1/2 (left) and OUT3/4 (right) |
Note: D11 doubles as SPI COPI — fine here since we're not using SPI today.
Step 2 — the sketch
// L03-25 · Bluetooth chassis driver
// Single-character commands: F B L R S (drive / spin / stop)
// Optional keyword commands: SPEED 200 (set drive duty)
#include <SoftwareSerial.h>
#include "motor.h"
SoftwareSerial bt(2, 3);
const Motor motorL = {9, 8, 5}; // IN1, IN2, ENA
const Motor motorR = {7, 6, 11}; // IN3, IN4, ENB
int driveDuty = 200;
String line;
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 dispatch(const String& cmd) {
if (cmd.length() == 0) return;
// Multi-letter keywords first
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;
}
// Single-character fallback
switch (cmd[0]) {
case 'F': case 'f': driveForward();
bt.println("OK FWD");
break;
case 'B': case 'b': driveBackward();
bt.println("OK REV");
break;
case 'L': case 'l': spinLeft();
bt.println("OK LEFT");
break;
case 'R': case 'r': spinRight();
bt.println("OK RIGHT");
break;
case 'S': case 's': stopAll();
bt.println("OK STOP");
break;
default:
bt.print("Unknown: ");
bt.println(cmd);
}
}
void setup() {
Serial.begin(9600);
bt.begin(9600);
motorInit(motorL);
motorInit(motorR);
stopAll();
bt.println("# Chassis armed. F/B/L/R/S, SPEED 0..255");
}
void loop() {
while (bt.available()) {
char c = bt.read();
if (c == '\r') continue;
if (c == '\n') { dispatch(line);
line = "";
continue;
}
line += c;
if (line.length() > 64) line = "";
}
}Step 3 — pair and drive
Hold the chassis up off the floor (wheels free), pair phone to HC-05, open the Bluetooth terminal app, connect. Set line ending to "Newline". Type:
F→ both wheels spin forward, phone shows "OK FWD".S→ stop.SPEED 120→ reduces drive duty; "OK SPEED 120".F→ slower forward this time.L/R→ in-place spin.
Step 4 — put it on the floor
Power the L298N from its 4 × AA pack (motor rail) and the UNO either via USB or from the L298N's 5 V regulator. Drive around. With SPEED 200, the chassis travels at the default L03-10 pace.
Step 5 — add the safety stop
Add this check at the top of loop() so a stray S at any time (even mid-line) instantly stops:
while (bt.available()) {
char c = bt.read();
// Emergency-stop: any 'S' or 's' kills the motors immediately.
if (c == 'S' || c == 's') {
line = "";
stopAll();
bt.println("OK STOP (immediate)");
continue;
}
if (c == '\r') continue;
if (c == '\n') { dispatch(line);
line = "";
continue;
}
line += c;
}The stop is now instant — the operator doesn't need to wait for a newline.
Step 6 — add a heartbeat
Send a status line every 2 s so the phone screen has constant feedback:
unsigned long lastBeat = 0;
// in loop():
if (millis() - lastBeat >= 2000) {
lastBeat = millis();
bt.print("# uptime ");
bt.print(millis() / 1000);
bt.print("s speed ");
bt.println(driveDuty);
}Useful both as "is the connection alive?" and as "what's the current speed setting?".
Basic 5 min
Goal: Add a TRIM L 10 / TRIM R 10 command that nudges one motor's duty up or down to compensate for tracking drift.
Challenge 1 5 min
Goal: Add a "canned move" command: SQUARE drives a small square (forward 1 s, turn 90°, repeat 4×) then stops.
Challenge 2 5 min
Goal: Add a ? command that prints a one-line help / status: current speed, current trims, uptime, last command.
Challenge 3 · The stop that ate SPEED 10 min
You added the Step 5 emergency stop. Now a strange thing happens on the phone:
> SPEED 120 OK STOP (immediate) Unknown: PEED 120
Before Step 5, SPEED 120 worked perfectly. Nothing else in the sketch changed.
- Explain, byte by byte, why the phone shows those two lines.
- Change the emergency-stop check so
SPEED 120works again. - Keep a stop that is instant, with no newline needed.
- Upload and test both commands from the phone.
It works if SPEED 120 replies OK SPEED 120, and your stop key halts the wheels mid-line.
Recap 5 min
Single-character commands for live joystick-style control, multi-letter keyword commands when you need arguments. Buffer characters into a String, dispatch on newline. Make stop instant — bypass the buffer for safety. Echo every command ("OK X") so the phone has feedback. Add a heartbeat so the user knows the link is alive. Tomorrow we leave Classic Bluetooth and meet BLE — the modern stack that finally works with iOS phones.
- Command protocol
- The set of rules for how a host talks to a device: command format, terminator, response format, error handling. Always design before coding.
- Single-character command
- One-byte command with no terminator.
F= forward,S= stop, etc. Low latency, no parser. Hard to extend with arguments. - Line-buffered command
- One command per line (terminated by
\n). Verb + optional args. Readable; takes parameters; needs a small parser. - Dispatch function
- The single point in the sketch that takes a complete parsed command and calls the matching action. Centralising it keeps the rest of the sketch clean.
- Terminator
- The byte(s) that mark end-of-command. Usually
\n(newline) or\r\n(CR+LF). Be tolerant of both. - Timeout dispatch
- Falling back to dispatching after N ms of silence if no newline arrived. Friendly to users typing without pressing Enter.
- Emergency stop
- A command that bypasses normal buffering and instantly stops actuators. For motorised builds, always include one and document it.
- Acknowledgement (ACK)
- A response echoed back so the controller knows the command was received. Essential when the operator can't see the device.
- Heartbeat
- A periodic status message from the device ("# uptime 12s"). Confirms the link is alive when no commands are flowing.
Extra Mission 5 min
Part 1 — Design a command language for a gadget
Pick a gadget you would like to control from a phone. Ideas: a night light, a desk fan, a toy crane, a scoreboard. On paper, design the Bluetooth command language for it.
Your design must include:
- The gadget's name and its one job.
- At least four commands, each with its arguments and the reply it sends back.
- An emergency or "all off" command, and the byte that triggers it.
- A
?help command and what it prints. - A check that no two commands start with the same letter as your stop byte.
Part 2 — Make it
Build the core of your gadget on the UNO with the HC-05. Use LEDs, a buzzer, a servo or the motor driver to stand in for the real parts. Write the line parser and dispatch() so every command in your design works from the phone.
Bring back next class: your command sheet, your uploaded sketch, and a screenshot of the phone terminal showing every command and its reply.