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?".
Try It Yourself 15 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.
Hint
int trimL = 0, trimR = 0;
// In driveForward(): pass driveDuty + trimL / trimR
motorSpeed(motorL, driveDuty + trimL);
motorSpeed(motorR, driveDuty + trimR);
// In dispatch:
if (cmd.startsWith("TRIM L ")) trimL = cmd.substring(7).toInt();
if (cmd.startsWith("TRIM R ")) trimR = cmd.substring(7).toInt();Phone the trim live to straighten the chassis without re-uploading.
Goal: Add a "canned move" command: SQUARE drives a small square (forward 1 s, turn 90°, repeat 4×) then stops.
Hint
Implement as a state machine in loop() so it's non-blocking — same pattern as the smart-bin lid (L02-26) and the rotating display (L03-14). The dispatch handler just sets mode = SQUARE_LEG_1; stateEntered = millis(); and the main loop drives the chassis based on the current mode.
Goal: Add a ? command that prints a one-line help / status: current speed, current trims, uptime, last command.
Hint
String lastCmd = "(none)";
// In dispatch, top of function: lastCmd = cmd;
case '?':
bt.print("HELP: F B L R S | SPEED n | TRIM L/R n");
bt.print(" | uptime "); bt.print(millis()/1000); bt.print("s");
bt.print(" speed "); bt.print(driveDuty);
bt.print(" trim L "); bt.print(trimL); bt.print(" R "); bt.print(trimR);
bt.print(" last "); bt.println(lastCmd);
break;Self-documenting devices are kind to your future self. Every Bluetooth project should have a ?.
Mini-Challenge · Write your protocol spec 10 min
Document the chassis's Bluetooth API as a v2 of yesterday's notebook page. Include:
- Pair: name, PIN, baud (from L03-24).
- Commands: each one + arguments + responses + side effects.
- Safety: emergency stop (any
Sat any time). - Heartbeat: lines starting with
#are status, not responses.
Example spec
Chassis · BT API v2 Pair: Aliya-Bot / PIN 4567 / baud 9600 Drive (single char, no newline needed for S): F forward (resp: OK FWD) B backward (resp: OK REV) L spin left (resp: OK LEFT) R spin right (resp: OK RIGHT) S emergency stop (instant, mid-line ok) (resp: OK STOP) Config (keyword + newline): SPEED nnn 0..255 drive duty (resp: OK SPEED nnn) TRIM L nnn left motor trim (resp: OK TRIM ..) TRIM R nnn right motor trim (resp: OK TRIM ..) ? status + help (resp: long line) Heartbeat: '# uptime Ns speed nnn' every 2 s.
Tape it next to the chassis. When you come back in 3 months and want to drive it again, this is the manual.
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.
Homework 5 min
- Save the chassis sketch as
bt-chassis.ino. It's the prep build for the Bluetooth car project (L03-28). - Update your engineering notebook's chassis page with the BT API v2 spec.
- Video a 30-second remote drive (you holding the phone, chassis on the floor, executing commands).
- Read ahead to ARD-L03-26 (ArduinoBLE Basics). Bring a Nano 33 BLE or Nano 33 BLE Sense if you have one — otherwise we'll use yours conceptually + look at code. (Cluster E's "modern BLE" lessons strictly need the BLE-capable hardware; if you only have a UNO, follow along and revisit when the board arrives.)
Bring back next class:
- Saved sketch + API page.
- Driving video.
- (Optional) Nano 33 BLE.