A Bluetooth or Wi-Fi robot is controlled by text commands arriving over a serial link, and badly written parsing is why such robots lag, jump or ignore half the commands. This guide shows how to read commands without blocking, how the Arduino timeout works, and a short command format that is easy to parse.
Status: written from the Arduino Serial reference. The sketches are untested outlines and no latency measurements of our own are included.
The simple way and its catch
The easy method is Serial.readStringUntil('\n'), which collects characters until a newline. The Arduino reference says it ends early if it times out, and returns what it has read. The timeout set with Serial.setTimeout() defaults to 1000 ms and also governs parseInt(), parseFloat(), readString() and readBytesUntil(). So if a phone app sends a command without the terminator, your robot can stall for up to a second inside that call, with the motors still running the last command. That is the classic cause of a laggy Bluetooth car.
Safer: build the line yourself, without blocking
Read one character per pass of loop() only when Serial.available() says one is waiting, and add it to a buffer. When you see the end-of-line character, process the buffer. Nothing waits, so the control code keeps running.
// Untested outline.
const byte MAX_LEN = 24;
char line[MAX_LEN];
byte len = 0;
void readSerial() {
while (Serial.available() > 0) {
char c = Serial.read();
if (c == '\n') {
line[len] = 0;
handleCommand(line);
len = 0;
} else if (c != '\r' && len < MAX_LEN - 1) {
line[len++] = c;
}
}
}
void loop() {
readSerial();
// control code runs every pass
}Using a fixed char array rather than the String class avoids heap fragmentation on small boards, and the length check stops a long garbage line overflowing the buffer.
Choose a command format that is easy to parse
| Style | Example | Notes |
|---|---|---|
| Single letter | F, B, L, R, S | Easiest; good for a first robot, but no speed value |
| Letter and number | F200 | Direction plus speed; parse the number after the letter |
| Named command and values | DRIVE 200 -150 | Left and right speed; clear to read and extend |
| Framed with a checksum | $DRIVE,200,-150*3F | For noisy links; more work to write |
Whatever you choose, document it in a comment so you and a phone-app builder agree on it.
Parsing “DRIVE left right”
// Untested outline.
void handleCommand(char *s) {
if (strncmp(s, "DRIVE ", 6) == 0) {
int left, right;
if (sscanf(s + 6, "%d %d", &left, &right) == 2) {
left = constrain(left, -255, 255);
right = constrain(right, -255, 255);
setMotors(left, right);
lastCommand = millis();
}
} else if (strcmp(s, "STOP") == 0) {
setMotors(0, 0);
}
}Check that sscanf returned 2 before using the values, constrain them to the range your motor driver accepts, and ignore anything that does not match. parseInt() returns 0 if it reads no digits before the timeout, so a missing number silently becomes a zero command; checking the result yourself avoids that.
Add a failsafe
If the link drops, the last command stays active. Record the time of each valid command and, in loop(), stop the motors if none has arrived for a set time, say 500 ms. The value is a starting point; choose it from how often your app sends. For a wired example see the HC-05 module page, and for a full car build see the Bluetooth robot car guide.
// Untested outline.
unsigned long lastCommand = 0;
const unsigned long FAILSAFE_MS = 500;
void loop() {
readSerial();
if (millis() - lastCommand > FAILSAFE_MS) setMotors(0, 0);
}Common mistakes
- The phone app sends
\r\nand the sketch only looks for\n, leaving a stray\rin the command. - Baud rate mismatch between the module and
Serial.begin(); garbled text is the sign. - Using the same hardware serial port for debugging and for the Bluetooth module on an Uno, which clashes. See the Uno pinout guide for the pins.
- No failsafe, so a lost connection leaves the robot driving.
- Trusting values from the phone without constraining them.
A test plan
- Open the Serial Monitor and type commands by hand with newline line endings.
- Send a command without a newline and confirm the robot does not freeze.
- Send garbage and a very long line and confirm nothing crashes.
- Switch off the sender and confirm the motors stop after the failsafe time.
- Test with wheels off the ground first, and record what you changed.
Sources: Arduino language reference for Serial.setTimeout(), Serial.readStringUntil() and Serial.parseInt(). Last reviewed November 2026. Written, checked and maintained by Jignesh Panchal.