Module 07: Remote Control via Bluetooth

Level: 🟢 Beginner Board: Arduino Uno + HC-05 Prerequisites: Module 06 Estimated time: 60–90 minutes Goal: Pair a Bluetooth module, receive single-character commands from a phone, and drive the robot remotely in real time.


What You'll Learn

By the end of this module you will know how to wire the HC-05 Bluetooth module (including the voltage divider for the RX pin), write a command parser that maps single characters to motor functions, and understand why RC robots feel slightly behind your inputs and how to reduce that lag.


iPhone users: HC-05 uses Bluetooth Classic (SPP), which iOS cannot pair with directly. Use an HC-08 (BLE) module instead of HC-05, or use an Android device. If you are using Android, continue as written.

Up to now the robot executes a fixed program. This module gives it a live wireless link to your phone. You'll wire the HC-05, write a command parser, and drive the robot from a phone app. Phase 2 ends here. Your robot does everything a classic RC car does, except you built it from scratch.


7.1 How serial communication works

Bluetooth serial isn't magic. From the Arduino's point of view, the HC-05 looks exactly like the USB cable to your laptop; it's just another serial device. Data travels as a stream of bytes, one at a time, in order. There's no built-in concept of a "message boundary." You send the letter F from your phone and the Arduino receives byte value 70 (ASCII for F). What that byte means is entirely up to your code.

The HC-05 is a bridge, nothing more. On one side: Bluetooth radio. On the other: two wires (TX and RX). Everything else is your code.

<svg viewBox="0 0 760 300" width="100%" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="aW7" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#c8b400" stroke-width="1.5" stroke-linecap="round"/></marker>
    <marker id="aP7" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#7030d0" stroke-width="1.5" stroke-linecap="round"/></marker>
    <marker id="aG7" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#1a6b4a" stroke-width="1.5" stroke-linecap="round"/></marker>
  </defs>

  <!-- PHONE -->
  <rect x="30" y="70" width="76" height="140" rx="10" fill="#111" stroke="#444" stroke-width="1.5"/>
  <rect x="38" y="82" width="60" height="90" rx="3" fill="#1f3f80"/>
  <text x="68" y="124" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#a0c0f0">BT</text>
  <text x="68" y="138" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#a0c0f0">App</text>
  <!-- phone button: F command -->
  <rect x="42" y="180" width="52" height="20" rx="3" fill="#c8420a"/>
  <text x="68" y="194" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="white">FWD 'F'</text>
  <!-- phone label -->
  <text x="68" y="226" text-anchor="middle" font-family="Manrope,sans-serif" font-size="10" fill="#555">phone</text>

  <!-- BT radio wave -->
  <path d="M112 130 Q124 118 112 106" fill="none" stroke="#7030d0" stroke-width="1.5"/>
  <path d="M116 134 Q134 118 116 102" fill="none" stroke="#7030d0" stroke-width="1" opacity=".6"/>
  <path d="M120 138 Q144 118 120 98" fill="none" stroke="#7030d0" stroke-width=".8" opacity=".3"/>
  <line x1="128" y1="118" x2="178" y2="118" stroke="#7030d0" stroke-width="1.5" stroke-dasharray="4 3" marker-end="url(#aP7)"/>
  <text x="153" y="110" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#7030d0">2.4GHz</text>
  <text x="153" y="135" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#555">byte: 0x46</text>
  <text x="153" y="148" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#555">(ASCII 'F')</text>

  <!-- HC-05 MODULE -->
  <rect x="180" y="88" width="90" height="90" rx="5" fill="#5020a0" stroke="#7030d0" stroke-width="1.5"/>
  <text x="225" y="126" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#e0c8ff">HC-05</text>
  <text x="225" y="144" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#c0a0ff">Bluetooth</text>
  <text x="225" y="157" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#c0a0ff">↕ UART</text>
  <!-- VCC / GND pins -->
  <circle cx="180" cy="100" r="4" fill="#c8420a"/>
  <text x="174" y="104" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="8" fill="#c8420a">VCC</text>
  <circle cx="180" cy="114" r="4" fill="#444"/>
  <text x="174" y="118" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="8" fill="#888">GND</text>
  <!-- TX/RX pins -->
  <circle cx="180" cy="148" r="4" fill="#c8b400"/>
  <text x="174" y="152" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="8" fill="#c8b400">TX</text>
  <circle cx="180" cy="162" r="4" fill="#c8b400"/>
  <text x="174" y="166" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="8" fill="#c8b400">RX</text>
  <!-- voltage divider note -->
  <rect x="230" y="168" width="130" height="34" rx="3" fill="#111"/>
  <text x="295" y="182" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8b400">Voltage divider!</text>
  <text x="295" y="196" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">HC-05 RX is 3.3V max</text>

  <!-- UART wires to Arduino -->
  <line x1="270" y1="148" x2="356" y2="148" stroke="#c8b400" stroke-width="1.5" marker-end="url(#aW7)"/>
  <line x1="356" y1="162" x2="270" y2="162" stroke="#c8b400" stroke-width="1.5" marker-end="url(#aW7)"/>
  <text x="313" y="142" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8b400">TX→RX</text>
  <text x="313" y="178" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8b400">RX←TX</text>

  <!-- ARDUINO -->
  <rect x="358" y="60" width="150" height="200" rx="5" fill="#1a6b4a" stroke="#0e4030" stroke-width="1.5"/>
  <text x="433" y="90" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#a8e8c8">ARDUINO</text>
  <!-- RX D2 (SoftwareSerial) -->
  <circle cx="358" cy="148" r="5" fill="#c8b400" stroke="#8a7a00" stroke-width="1"/>
  <text x="352" y="152" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="8" fill="#c8b400">D2 RX</text>
  <!-- TX D3 (SoftwareSerial) -->
  <circle cx="358" cy="162" r="5" fill="#c8b400" stroke="#8a7a00" stroke-width="1"/>
  <text x="352" y="166" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="8" fill="#c8b400">D3 TX</text>
  <!-- btSerial.read label -->
  <text x="433" y="132" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#a8e8c8">btSerial.available()</text>
  <text x="433" y="148" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#60b080">byte waiting?</text>
  <text x="433" y="170" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#a8e8c8">btSerial.read()</text>
  <text x="433" y="186" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#60b080">fetch one byte</text>
  <!-- arrow to parser -->
  <line x1="433" y1="196" x2="433" y2="218" stroke="#1a6b4a" stroke-width="1.5" marker-end="url(#aG7)"/>
  <text x="433" y="236" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#a8e8c8">switch(cmd)</text>
  <text x="433" y="250" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#60b080">parser decides</text>

  <!-- Motor driver -->
  <line x1="510" y1="160" x2="558" y2="160" stroke="#e07020" stroke-width="1.5" marker-end="url(#aW7)"/>
  <rect x="560" y="120" width="80" height="80" rx="4" fill="#1f3f80" stroke="#3060c0" stroke-width="1.5"/>
  <text x="600" y="156" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#a0c0f0">L298N</text>
  <text x="600" y="172" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#7090c0">→ motors</text>

  <!-- byte stream annotation -->
  <rect x="30" y="240" width="610" height="48" rx="3" fill="#111"/>
  <text x="40" y="258" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">byte stream example: </text>
  <rect x="178" y="246" width="22" height="22" rx="2" fill="#c8420a"/><text x="189" y="261" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="white">'F'</text>
  <rect x="206" y="246" width="22" height="22" rx="2" fill="#1a6b4a"/><text x="217" y="261" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="white">'F'</text>
  <rect x="234" y="246" width="22" height="22" rx="2" fill="#1a6b4a"/><text x="245" y="261" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="white">'F'</text>
  <rect x="262" y="246" width="22" height="22" rx="2" fill="#1f4d8c"/><text x="273" y="261" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="white">'L'</text>
  <rect x="290" y="246" width="22" height="22" rx="2" fill="#1f4d8c"/><text x="301" y="261" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="white">'L'</text>
  <rect x="318" y="246" width="22" height="22" rx="2" fill="#888"/><text x="329" y="261" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="white">'S'</text>
  <text x="360" y="261" font-family="Manrope,sans-serif" font-size="10" fill="#666">← each byte read once by Serial.read()</text>
  <text x="178" y="282" font-family="Manrope,sans-serif" font-size="10" fill="#555">no gaps between bytes — act on latest only, or buffer accumulates and lags</text>
</svg>

Fig 7.1: Full communication chain: phone app sends a character byte over Bluetooth radio to the HC-05, which passes it as UART serial to Arduino D2 (SoftwareSerial RX). Arduino reads it with btSerial.read(), passes the byte to a switch statement, and calls the appropriate motor function. The HC-05 TX wire connects to D2. The HC-05 RX wire needs a voltage divider because the module's RX pin is 3.3V logic while the Arduino D3 pin outputs 5V.

One thing trips up most people on their first Bluetooth build: the TX/RX naming is from the perspective of each device. The HC-05's TX (transmit) goes to the Arduino's RX (receive). The HC-05's RX (receive) goes to the Arduino's TX (transmit). Cross the wires. If you connect TX to TX, nothing works and there is no error, just silence.

HC-05 default state. The module ships with data mode at 9600 baud and AT command mode at 38400 baud. The sketch in this module uses 9600 baud (btSerial.begin(9600)), which matches the factory default. The default pairing PIN is 1234. If the module was previously configured by someone else, its baud rate may have changed and the sketch will appear to receive nothing. To check: hold the EN/KEY pin HIGH while powering on to enter AT mode, then open a serial terminal at 38400 baud and send AT+UART?. The response shows the current baud rate. To restore the default: send AT+UART=9600,0,0.

7.2 Wiring the HC-05

The HC-05 has six pins. You only need four of them for basic operation. The fifth (STATE) is an output you can read to know if a device is connected. The sixth (EN/KEY) is used only during AT command mode for configuration; leave it unconnected during normal operation.

<svg viewBox="0 0 760 300" width="100%" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="aB7" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#1a1a18" stroke-width="1.5" stroke-linecap="round"/></marker>
  </defs>

  <!-- HC-05 module body -->
  <rect x="100" y="60" width="120" height="180" rx="5" fill="#5020a0" stroke="#7030d0" stroke-width="2"/>
  <text x="160" y="92" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="13" fill="#e0c8ff">HC-05</text>
  <!-- LED indicator -->
  <circle cx="160" cy="110" r="8" fill="#c8420a" opacity=".8"/>
  <text x="160" y="130" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#c0a0ff">status LED</text>
  <text x="160" y="142" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">fast blink = waiting</text>
  <text x="160" y="154" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">slow blink = paired</text>

  <!-- Pins left side -->
  <g>
    <!-- VCC -->
    <circle cx="100" cy="80" r="5" fill="#c8420a" stroke="#7a2000" stroke-width="1"/>
    <text x="94" y="84" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8420a">VCC</text>
    <!-- GND -->
    <circle cx="100" cy="100" r="5" fill="#444" stroke="#222" stroke-width="1"/>
    <text x="94" y="104" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">GND</text>
    <!-- TX -->
    <circle cx="100" cy="120" r="5" fill="#c8b400" stroke="#8a7a00" stroke-width="1"/>
    <text x="94" y="124" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">TX</text>
    <!-- RX -->
    <circle cx="100" cy="140" r="5" fill="#c8b400" stroke="#8a7a00" stroke-width="1"/>
    <text x="94" y="144" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">RX</text>
    <!-- STATE -->
    <circle cx="100" cy="160" r="5" fill="#888" stroke="#555" stroke-width="1"/>
    <text x="94" y="164" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">STATE</text>
    <!-- EN/KEY -->
    <circle cx="100" cy="180" r="5" fill="#555" stroke="#333" stroke-width="1"/>
    <text x="94" y="184" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="10" fill="#555">KEY</text>
    <text x="94" y="196" text-anchor="end" font-family="Manrope,sans-serif" font-size="9" fill="#444">leave open</text>
  </g>

  <!-- Wires to voltage divider / Arduino -->
  <!-- VCC → 5V Arduino -->
  <line x1="95" y1="80" x2="50" y2="80" stroke="#c8420a" stroke-width="1.5"/>
  <text x="44" y="84" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8420a">Arduino 5V</text>
  <!-- GND → GND -->
  <line x1="95" y1="100" x2="50" y2="100" stroke="#444" stroke-width="1.5"/>
  <text x="44" y="104" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">Arduino GND</text>
  <!-- TX → D2 (SoftwareSerial RX) directly -->
  <line x1="95" y1="120" x2="50" y2="120" stroke="#c8b400" stroke-width="1.5"/>
  <text x="44" y="124" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8b400">Arduino D2 (SoftwareSerial RX)</text>
  <text x="44" y="136" text-anchor="end" font-family="Manrope,sans-serif" font-size="9" fill="#6b6a65">direct — 3.3V is fine</text>

  <!-- RX needs voltage divider -->
  <!-- Voltage divider schematic -->
  <text x="380" y="44" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="12" letter-spacing="1" fill="#c8420a">VOLTAGE DIVIDER — HC-05 RX</text>
  <text x="380" y="62" text-anchor="middle" font-family="Manrope,sans-serif" font-size="11" fill="#6b6a65">Arduino D3 (SoftwareSerial TX) = 5V → must reduce to 3.3V for HC-05 RX</text>

  <!-- Divider circuit -->
  <!-- Top wire from Arduino TX -->
  <line x1="280" y1="100" x2="340" y2="100" stroke="#c8b400" stroke-width="1.5"/>
  <text x="268" y="104" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">Arduino TX (5V)</text>
  <!-- R1 = 1kΩ -->
  <rect x="340" y="88" width="50" height="24" rx="3" fill="#edeae0" stroke="#c4bfb0" stroke-width="1.5"/>
  <text x="365" y="104" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#3d3c38">1kΩ</text>
  <!-- Middle junction -->
  <line x1="390" y1="100" x2="440" y2="100" stroke="#c8b400" stroke-width="1.5"/>
  <circle cx="415" cy="100" r="4" fill="#c8b400"/>
  <!-- R2 = 2kΩ down to GND -->
  <line x1="415" y1="104" x2="415" y2="140" stroke="#c8b400" stroke-width="1.5"/>
  <rect x="390" y="140" width="50" height="24" rx="3" fill="#edeae0" stroke="#c4bfb0" stroke-width="1.5"/>
  <text x="415" y="156" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#3d3c38">2kΩ</text>
  <line x1="415" y1="164" x2="415" y2="190" stroke="#444" stroke-width="1.5"/>
  <line x1="400" y1="190" x2="430" y2="190" stroke="#444" stroke-width="1.5"/>
  <line x1="405" y1="196" x2="425" y2="196" stroke="#444" stroke-width="1.5"/>
  <line x1="410" y1="202" x2="420" y2="202" stroke="#444" stroke-width="1.5"/>
  <text x="415" y="218" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">GND</text>
  <!-- Output to HC-05 RX -->
  <line x1="440" y1="100" x2="500" y2="100" stroke="#c8b400" stroke-width="1.5" marker-end="url(#aB7)"/>
  <text x="520" y="96" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">HC-05 RX</text>
  <text x="520" y="112" font-family="Manrope,sans-serif" font-size="10" fill="#6b6a65">(3.3V)</text>
  <!-- Voltage annotation -->
  <rect x="310" y="228" width="280" height="58" rx="3" fill="#f5f2eb" stroke="#c4bfb0" stroke-width="1"/>
  <text x="320" y="247" font-family="IBM Plex Mono,monospace" font-size="10" fill="#3d3c38">Vout = Vin × R2 / (R1 + R2)</text>
  <text x="320" y="263" font-family="IBM Plex Mono,monospace" font-size="10" fill="#1a6b4a">     = 5V × 2000 / 3000 = 3.33V</text>
  <text x="320" y="279" font-family="Manrope,sans-serif" font-size="10" fill="#6b6a65">Nearest standard pair: 1kΩ + 2kΩ</text>
</svg>

Fig 7.2: HC-05 pinout and voltage divider for the RX pin. VCC, GND, and the HC-05 TX wire connect directly to Arduino 5V, GND, and D2 (SoftwareSerial RX); direct connection is fine since 3.3V output is safe for the Arduino input. Do NOT use D0/D1 as these conflict with USB uploads. The HC-05 RX wire cannot connect directly to Arduino D3 because the module's RX is 3.3V logic and 5V will damage it over time. A simple 1kΩ / 2kΩ divider drops the 5V signal to 3.33V. The LED blinks fast while waiting for a connection and slows to one blink per two seconds once paired.

Critical wiring note: TX goes to RX and RX goes to TX; always cross the pair. HC-05 TX → Arduino D2. Arduino D3 → voltage divider → HC-05 RX. Getting this backward produces total silence with no error. This sketch uses SoftwareSerial on D2/D3; do NOT use the hardware UART pins D0/D1, which conflict with USB uploads.

Secure the HC-05 module to the breadboard or chassis with a small strip of double-sided foam tape. The voltage divider resistors must stay firmly seated in the breadboard during driving; a loose connection causes intermittent serial errors that are difficult to diagnose.

Retention: Press both resistor legs fully into the breadboard holes and gently bend the exposed leads inward. If the 2 kΩ resistor vibrates loose while the robot is driving, the HC-05 RX pin sees full 5 V from Arduino D3 and the Bluetooth module can be damaged.

Advanced path: Voltage divider retention. Bend the resistor legs slightly at a 15° angle before inserting them into the breadboard; this increases the pull-out force. Group the two resistor wires and the HC-05 RX jumper wire tightly. If the 2kΩ resistor vibrates loose during driving, the HC-05 RX pin sees the full 5V signal from Arduino D3 and can be damaged over time.

7.3 Writing the command parser

A parser reads incoming bytes and decides what action to take. For simple RC control, single-character commands work perfectly: one byte, one action, no ambiguity. The phone app sends 'F' for forward, 'B' for backward, 'L' for left, 'R' for right, 'S' for stop. The Arduino reads one byte per loop pass and dispatches the matching motor function.

<svg viewBox="0 0 760 260" width="100%" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <marker id="aSw" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#1a1a18" stroke-width="1.5" stroke-linecap="round"/></marker>
    <marker id="aGsw" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M2 2L8 5L2 8" fill="none" stroke="#1a6b4a" stroke-width="1.5" stroke-linecap="round"/></marker>
  </defs>

  <!-- Input byte -->
  <rect x="30" y="108" width="80" height="44" rx="4" fill="#5020a0" stroke="#7030d0" stroke-width="1.5"/>
  <text x="70" y="128" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#e0c8ff">byte</text>
  <text x="70" y="144" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#e0c8ff">cmd</text>

  <!-- Arrow to switch -->
  <line x1="112" y1="130" x2="156" y2="130" stroke="#1a1a18" stroke-width="1.5" marker-end="url(#aSw)"/>

  <!-- Switch diamond -->
  <polygon points="200,90 260,130 200,170 140,130" fill="#f5eed8" stroke="#c8aa60" stroke-width="1.5"/>
  <text x="200" y="126" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#8a6800">switch</text>
  <text x="200" y="140" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#8a6800">(cmd)</text>

  <!-- Branch: F -->
  <line x1="200" y1="90" x2="200" y2="52" stroke="#1a1a18" stroke-width="1" marker-end="url(#aSw)"/>
  <rect x="160" y="28" width="80" height="24" rx="3" fill="#d8f0e5" stroke="#1a6b4a" stroke-width="1"/>
  <text x="200" y="44" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#1a6b4a">'F' → forward</text>

  <!-- Branch: B -->
  <line x1="200" y1="170" x2="200" y2="208" stroke="#1a1a18" stroke-width="1" marker-end="url(#aSw)"/>
  <rect x="160" y="208" width="80" height="24" rx="3" fill="#fde8e8" stroke="#c8420a" stroke-width="1"/>
  <text x="200" y="224" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8420a">'B' → backward</text>

  <!-- Branch: L -->
  <line x1="260" y1="130" x2="330" y2="80" stroke="#1a1a18" stroke-width="1" marker-end="url(#aSw)"/>
  <rect x="330" y="66" width="80" height="24" rx="3" fill="#daeaf5" stroke="#1f4d8c" stroke-width="1"/>
  <text x="370" y="82" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#1f4d8c">'L' → turnLeft</text>

  <!-- Branch: R -->
  <line x1="260" y1="130" x2="330" y2="130" stroke="#1a1a18" stroke-width="1" marker-end="url(#aSw)"/>
  <rect x="330" y="116" width="80" height="24" rx="3" fill="#daeaf5" stroke="#1f4d8c" stroke-width="1"/>
  <text x="370" y="132" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#1f4d8c">'R' → turnRight</text>

  <!-- Branch: S -->
  <line x1="260" y1="130" x2="330" y2="180" stroke="#1a1a18" stroke-width="1" marker-end="url(#aSw)"/>
  <rect x="330" y="166" width="80" height="24" rx="3" fill="#f5f2eb" stroke="#c4bfb0" stroke-width="1.5"/>
  <text x="370" y="182" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#6b6a65">'S' → stopMotors</text>

  <!-- default -->
  <line x1="140" y1="130" x2="60" y2="200" stroke="#c4bfb0" stroke-width="1" stroke-dasharray="4 3" marker-end="url(#aSw)"/>
  <text x="44" y="215" text-anchor="end" font-family="IBM Plex Mono,monospace" font-size="9" fill="#888">default:</text>
  <text x="44" y="229" text-anchor="end" font-family="Manrope,sans-serif" font-size="9" fill="#888">ignore</text>

  <!-- loop annotation -->
  <rect x="460" y="50" width="280" height="170" rx="4" fill="#1a1a18"/>
  <text x="600" y="74" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#888">loop() structure</text>
  <line x1="470" y1="82" x2="730" y2="82" stroke="#333" stroke-width="1"/>
  <text x="474" y="102" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">if (btSerial.available()) {</text>
  <text x="486" y="120" font-family="IBM Plex Mono,monospace" font-size="10" fill="#a8e8c8">  char cmd = btSerial.read();</text>
  <text x="486" y="138" font-family="IBM Plex Mono,monospace" font-size="10" fill="#a8e8c8">  handleCommand(cmd);</text>
  <text x="474" y="156" font-family="IBM Plex Mono,monospace" font-size="10" fill="#c8b400">}</text>
  <line x1="470" y1="164" x2="730" y2="164" stroke="#333" stroke-width="1"/>
  <text x="474" y="182" font-family="Manrope,sans-serif" font-size="10" fill="#555">check first — only read</text>
  <text x="474" y="198" font-family="Manrope,sans-serif" font-size="10" fill="#555">if data is actually there</text>
  <text x="474" y="214" font-family="Manrope,sans-serif" font-size="10" fill="#c8420a">btSerial.read() on empty</text>
  <text x="474" y="228" font-family="Manrope,sans-serif" font-size="10" fill="#c8420a">buffer returns -1</text>
</svg>

Fig 7.3: Command parser logic. Each pass through loop() checks Serial.available() first. If a byte is waiting, read it and pass it to a switch statement. Each case calls the matching motor function. The default case silently ignores unknown bytes, which prevents garbled data or noise from crashing the robot.

// Full RC command parser sketch
// Pin constants and motor functions from Modules 4–6 go above this
// SoftwareSerial on D2/D3 frees the hardware UART for USB uploads —
// no need to disconnect wires before uploading a new sketch.

#include <SoftwareSerial.h>
SoftwareSerial btSerial(2, 3); // RX=D2, TX=D3  (HC-05 TX → D2, HC-05 RX ← D3 via divider)

// Dead-man timer: stops robot if Bluetooth connection drops
unsigned long lastCommandTime = 0;

void setup() {
  // Motor driver pins — same assignments as Module 4
  pinMode(ENA, OUTPUT);  pinMode(ENB, OUTPUT);
  pinMode(IN1, OUTPUT);  pinMode(IN2, OUTPUT);
  pinMode(IN3, OUTPUT);  pinMode(IN4, OUTPUT);
  stopMotors();                // safe state before Bluetooth connects
  Serial.begin(9600);         // USB serial — for debug output while tethered
  btSerial.begin(9600);       // SoftwareSerial on D2/D3 — must match HC-05 baud rate
}

void handleCommand(char cmd) {
  lastCommandTime = millis(); // reset watchdog — any received byte proves the BT link is alive
  switch (cmd) {
    case 'F':
      setDirectionForward();
      analogWrite(ENA, CRUISE);
      analogWrite(ENB, CRUISE - RTRIM);
      Serial.println("FWD"); // USB debug — always available
      break;
    case 'B':
      setDirectionBackward();
      analogWrite(ENA, CRUISE);
      analogWrite(ENB, CRUISE - RTRIM);
      Serial.println("BWD");
      break;
    case 'L':
      turnLeft(160);
      Serial.println("LEFT");
      break;
    case 'R':
      turnRight(160);
      Serial.println("RIGHT");
      break;
    case 'S':
      stopMotors();
      Serial.println("STOP");
      break;
    default:
      break;  // unknown byte — ignore silently
  }
}

void loop() {
  // Dead-man timer: stop if no command received in last 500ms
  if (millis() - lastCommandTime > 500 && lastCommandTime > 0) {
    stopMotors();
  }
  if (btSerial.available() > 0) {
    char cmd = (char) btSerial.read();
    handleCommand(cmd);
  }
}

Advanced path: Dead-man timer. The watchdog variable lastCommandTime records the timestamp of every received command. If 500ms pass with no new command, loop() calls stopMotors(). This prevents runaway: if your phone drops the Bluetooth connection mid-drive, the robot stops on its own within half a second rather than driving into a wall. Reset lastCommandTime = 0 at startup so the robot does not stop before the first command ever arrives.

Notice the structure: loop() does one thing: check for a byte and dispatch it. All the decision-making lives in handleCommand(). That separation matters. In Module 11 you'll add autonomous collision avoidance that can override RC commands. A clean handler function means you can intercept the call, apply the override logic, and call the real motor functions without touching the parser at all.

7.4 Latency and why RC robots feel sluggish

A well-wired HC-05 robot will feel slightly behind your inputs. You press forward and the robot starts moving 50–100ms later. That's not a code problem or a wiring problem; it's several unavoidable delays stacked on each other.

<svg viewBox="0 0 760 200" width="100%" xmlns="http://www.w3.org/2000/svg">

  <!-- Timeline bar -->
  <line x1="40" y1="100" x2="720" y2="100" stroke="#c4bfb0" stroke-width="1.5"/>

  <!-- Segments -->
  <!-- Touch to app: 10ms -->
  <rect x="40" y="80" width="50" height="40" rx="2" fill="#5020a0" stroke="#7030d0" stroke-width="1"/>
  <text x="65" y="75" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#7030d0">10ms</text>
  <text x="65" y="133" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">touch→app</text>

  <!-- BT transmit: 20ms -->
  <rect x="92" y="80" width="90" height="40" rx="2" fill="#5020a0" stroke="#9050f0" stroke-width="1"/>
  <text x="137" y="75" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#9050f0">20–40ms</text>
  <text x="137" y="133" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">BT radio</text>

  <!-- UART: 1ms -->
  <rect x="184" y="80" width="30" height="40" rx="2" fill="#c8b400" stroke="#8a7a00" stroke-width="1"/>
  <text x="199" y="75" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#8a7a00">1ms</text>
  <text x="199" y="133" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">UART</text>

  <!-- Arduino loop wait: 0-2ms -->
  <rect x="216" y="80" width="40" height="40" rx="2" fill="#1a6b4a" stroke="#0e4030" stroke-width="1"/>
  <text x="236" y="75" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#1a6b4a">0–2ms</text>
  <text x="236" y="133" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">loop wait</text>

  <!-- Motor ramp: 0-150ms -->
  <rect x="258" y="80" width="120" height="40" rx="2" fill="#c8420a" stroke="#7a2000" stroke-width="1"/>
  <text x="318" y="75" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="9" fill="#c8420a">0–150ms</text>
  <text x="318" y="133" text-anchor="middle" font-family="Manrope,sans-serif" font-size="9" fill="#888">motor ramp</text>

  <!-- Robot moves -->
  <rect x="380" y="80" width="80" height="40" rx="2" fill="#edeae0" stroke="#c4bfb0" stroke-width="1"/>
  <text x="420" y="104" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#3d3c38">moving</text>

  <!-- Total annotation -->
  <line x1="40" y1="160" x2="460" y2="160" stroke="#c8420a" stroke-width="1.5"/>
  <line x1="40" y1="155" x2="40" y2="165" stroke="#c8420a" stroke-width="1.5"/>
  <line x1="460" y1="155" x2="460" y2="165" stroke="#c8420a" stroke-width="1.5"/>
  <text x="250" y="178" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="11" fill="#c8420a">total: ~30–200ms typical</text>

  <!-- Fix box -->
  <rect x="500" y="60" width="240" height="110" rx="4" fill="#1a1a18"/>
  <text x="620" y="82" text-anchor="middle" font-family="IBM Plex Mono,monospace" font-size="10" fill="#888">reduce perceived lag</text>
  <line x1="510" y1="90" x2="730" y2="90" stroke="#333" stroke-width="1"/>
  <text x="514" y="108" font-family="Manrope,sans-serif" font-size="10" fill="#c8c4bc">1. Remove ramp from RC mode</text>
  <text x="514" y="124" font-family="Manrope,sans-serif" font-size="10" fill="#c8c4bc">2. Use SoftwareSerial to free</text>
  <text x="514" y="138" font-family="Manrope,sans-serif" font-size="10" fill="#c8c4bc">   D0/D1 for faster USB debug</text>
  <text x="514" y="154" font-family="Manrope,sans-serif" font-size="10" fill="#c8c4bc">3. BT classic ~20ms is floor</text>
</svg>

Fig 7.4: Latency stack from button press to robot motion. Bluetooth Classic (which the HC-05 uses) adds 20–40ms unavoidably. The motor ramp function from Module 6 adds another 0–150ms. For RC mode, remove the ramp; direct writes feel much more responsive. Save ramping for autonomous movement where jerk does not matter to the user.

The ramp functions from Module 6 make autonomous movement smooth. They make RC movement feel dead. Disable them in RC mode. Use a boolean flag at the top of your sketch (something like bool rcMode = true) and skip the ramp calls when it is set. You will use this same flag in Module 11 to switch between manual and autonomous behavior.

bool rcMode = true;  // true = RC, false = autonomous

void setSpeed(int leftSpeed, int rightSpeed) {
  if (rcMode) {
    analogWrite(ENA, leftSpeed);
    analogWrite(ENB, rightSpeed - RTRIM);
  } else {
    // Autonomous mode: rampUp only handles straight-line speed.
    // Arc turns in autonomous mode call driveArc() directly — not setSpeed().
    rampUp(leftSpeed, 150);  // smooth start for straight segments
  }
}

In Module 11, flipping rcMode to false switches the robot from direct RC response to smooth autonomous movement; the same handleCommand() parser stays in place.

Advanced path: packet framing and checksum. Single-character commands are fine for a classroom robot. Production RC systems use framed packets: a start byte, a payload of one or more data bytes, and a checksum byte at the end. The receiver waits for the start byte, reads the payload, then computes its own checksum of the received bytes and compares it to the sent checksum. A mismatch means a corrupted packet; discard it and wait for the next one. A simple checksum: XOR all payload bytes together. The sender appends the result. The receiver XORs the same bytes and checks for a match. One extra byte of overhead catches most single-bit errors. This matters when the BT link is noisy, range is near the limit (~10m), or you have multiple Bluetooth devices operating nearby and packets occasionally corrupt.

7.5 Phone app options

You need an Android or iOS app that can pair with the HC-05 and send single-character commands over Bluetooth serial. Several free options work out of the box.

App Platform Notes
Serial Bluetooth Terminal Android Best general-purpose option. Can send single characters on button press. Configure buttons to send F, B, L, R, S without newline.
Arduino Bluetooth Controller Android Pre-built D-pad layout. Sends single characters by default. Check which characters each button sends and match them in your switch statement.
Bluetooth Terminal HP iOS iOS limits Bluetooth Classic access. This app uses BLE bridging; check that your HC-05 firmware version supports it, or use an HC-08 (BLE native) for iOS.
Custom MIT App Inventor app Android Build your own layout. MIT App Inventor has a Bluetooth component that sends bytes directly. Takes 30 minutes to build a basic D-pad. Worth it for custom command sets.

Instructor note: iOS Bluetooth Classic support is restricted by Apple. The HC-05 uses Bluetooth Classic (SPP profile), which iOS does not expose to third-party apps. For iOS users, either switch to an HC-08 BLE module (same wiring, different AT commands, different baud rate) or have them pair from an Android device for this module and test on their own phone in Module 11 using the BLE path. The first pairing session always takes longer than expected. Budget extra time. The HC-05 default pairing PIN is 1234. The device will show up as "HC-05" in the Bluetooth device list. With SoftwareSerial on D2/D3, students can upload sketches without disconnecting HC-05 wires, which is a significant workflow improvement over the D0/D1 path.

Lab 07: Drive the robot from your phone

Wire the HC-05, pair it with your phone, upload the RC sketch, and drive the robot across the room using the phone app. This is the Phase 2 milestone where every skill from the last four modules comes together.

What you need: Completed robot from Module 06 · HC-05 module · 1kΩ and 2kΩ resistors · jumper wires · Android phone with Serial Bluetooth Terminal or equivalent app

  1. Disconnect power. Wire the HC-05: VCC to Arduino 5V, GND to GND, HC-05 TX to Arduino D2, Arduino D3 to 1kΩ resistor, 1kΩ other end to both the HC-05 RX pin and a 2kΩ resistor, 2kΩ other end to GND.

    Retention: Press both resistor legs fully into the breadboard holes and gently bend the exposed leads inward. If the 2 kΩ resistor vibrates loose while the robot is driving, the HC-05 RX pin sees full 5 V from Arduino D3 and the Bluetooth module can be damaged.

  2. Upload the complete RC sketch with handleCommand() and the loop() that calls it. Set btSerial.begin(9600) in setup(). The HC-05 wires stay connected during upload; SoftwareSerial on D2/D3 does not conflict with USB.

  3. Power on the robot. The HC-05 LED should blink rapidly; it is waiting for a connection.

  4. On your phone, go to Bluetooth settings and pair with "HC-05". PIN is 1234.

  5. Open your Bluetooth terminal app. Connect to HC-05. The LED on the module should now blink slowly, once every two seconds.

  6. Configure the app buttons to send: F (forward), B (backward), L (left), R (right), S (stop).

    Common failure: Most terminal apps have a "button press" mode that sends a single character with no newline; use that mode. If you are using a general-purpose terminal in line mode, disable the automatic newline (line ending: No line ending). The Arduino parser matches exact single characters; a trailing \n hits the default case and is silently ignored, making it look like the robot is not responding.

  7. Place the robot on the floor. Send F. Robot should drive forward. Send S. Robot stops. Test all directions.

  8. Open the Arduino Serial Monitor (9600 baud). The debug output (FWD, BWD, LEFT, RIGHT, STOP) comes through the USB connection on the hardware UART, which is now free to use simultaneously with Bluetooth.

Module milestone: Phase 2 complete. The robot responds to all five commands (F, B, L, R, S) from the phone app, drives across the room under full wireless control, and stops on command. RC mode is operational. Ready for Phase 3: Sensing and Autonomy.


Finished Phase 2? This is the moment to show someone. A robot you control from your phone is one of the most satisfying things you can build, and one of the easiest to show off. Film a 15-second clip of it responding to your commands, share it with a friend, post it wherever you're comfortable: Twitter/X, Instagram, YouTube, or the Make-It community with #MakeItRobot. Note what you'd tune first (latency? turn radius? stopping?). That instinct is exactly what Phase 3 builds on.


Self-Check: Module 07

Key Terms Glossary

Term Definition
UART Universal Asynchronous Receiver-Transmitter. The serial protocol used between the HC-05 and Arduino. Two wires: TX (transmit) and RX (receive). Always cross them between devices.
baud rate The speed of serial communication in bits per second. Both ends of the link must use the same rate. HC-05 default is 9600. Must match btSerial.begin() in your sketch.
Serial.available() Returns the number of bytes waiting in the serial receive buffer. Check this before calling Serial.read(); reading an empty buffer returns -1, which your switch statement will dispatch as an unknown command.
voltage divider Two resistors in series used to reduce voltage. The output taps between the two resistors. Used here to drop Arduino's 5V TX signal to 3.3V for the HC-05 RX pin.
Bluetooth Classic (SPP) The Bluetooth profile used by the HC-05. Serial Port Profile presents a Bluetooth link as a virtual serial cable. Supported natively on Android, restricted on iOS.
command parser Code that reads incoming bytes and maps them to actions. A switch statement on the received character is the simplest form. Separate it from motor logic so you can add override behavior later.
latency The delay between sending a command and seeing the robot respond. For HC-05 BT, the floor is ~20–40ms from the radio alone. Motor ramps and loop overhead add to this.
SoftwareSerial An Arduino library that emulates serial on any two digital pins instead of the hardware D0/D1 pins. Useful for keeping D0/D1 free for USB uploads while the HC-05 remains wired.

Previous: ← Module 06: Speed Control with PWM · Next: Module 08: Ultrasonic Distance Sensing →

Related Blog Posts