PTENES
MODULE 2.1 · PRACTICE

📱 Phone control: Wi-Fi and BLE

The ESP32 has Wi-Fi and Bluetooth built into the chip. In this module, you use both to drive the rover from your phone: a page with buttons, a lag-free browser joystick, and a BLE app. And first, the rule that separates a toy from a safe robot: if it loses the signal, the robot stops.

6
Topics
Intermediate
Level
Practice
Type
T1 rover
Hardware
0%
0 of 0 topics read · Section 1 of 6

Detailed content

1

📶 Wi-Fi: station and access point modes

The ESP32’s Wi-Fi works in two ways. In station mode (STA), it joins your home or school network, just like any phone, and gets an address from the router. In access point mode (AP), it creates its own network: your phone connects directly to the robot, without a router. For a robot that’s going outside, to the science fair, or onto the court, AP is the more predictable option.

🌱 New here?

O SSID is the network name that appears on your phone. The IP address is each device’s “house number” on the network (e.g., 192.168.4.1). A router assigns these addresses. In AP mode, the ESP32 does this itself and always uses 192.168.4.1.

STATION (STA) ACCESS POINT (AP)router phoneESP32 ESP32 phoneIP assigned by the router Fixed IP 192.168.4.1

How to read this map: On the left, everything goes through the router; if it’s far away or the network requires a login, you lose control of the robot. On the right, the ESP32 is the network, and your phone talks to it directly. The sketch below tries the first mode and falls back to the second if it can’t connect.

Goal: try to join your network for 10 seconds; if that fails, create the robot’s own network. The Serial Monitor shows which mode worked and the address to open in your browser.

wifi-sta-ou-ap.ino
// Wi-Fi: try station mode, fall back to access point (esp32 3.x package)
#include <WiFi.h>

const char* REDE_SSID  = "<seu_wifi>";      // replace with your network name
const char* REDE_SENHA = "<sua_senha>";
const char* AP_SSID    = "RoverINEMA";
const char* AP_SENHA   = "troque1234";      // at least 8 characters

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(REDE_SSID, REDE_SENHA);
  Serial.print("Conectando");
  unsigned long inicio = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - inicio < 10000) {
    delay(250);
    Serial.print(".");
  }
  Serial.println();
  if (WiFi.status() == WL_CONNECTED) {
    Serial.print("STATION mode. Open http://");
    Serial.println(WiFi.localIP());
  } else {
    WiFi.disconnect(true);
    WiFi.mode(WIFI_AP);
    WiFi.softAP(AP_SSID, AP_SENHA);
    Serial.print("ACCESS POINT mode. Connect to RoverINEMA and open http://");
    Serial.println(WiFi.softAPIP());
  }
}

void loop() {                                   // every 5 s: number of devices on the robot's network
  if (WiFi.getMode() == WIFI_AP) { Serial.print("Clients: "); Serial.println(WiFi.softAPgetStationNum()); }
  delay(5000);
}

How to check

  • ✓With the correct SSID and password: “STATION mode. Open http://192.168.x.y”.
  • ✓With an intentionally incorrect password: after 10 s, the “RoverINEMA” network appears on your phone, and the client count goes up to 1 when you connect.
  • ✗Can’t connect at all? The ESP32 can only see 2.4 GHz networks; networks that use only 5 GHz won’t show up.

Why learn this

All remote control in this module, and the telemetry in 2.5, starts with the ESP32 on the network. Choosing the right mode helps you avoid the classic science fair moment: the robot worked at home, but won’t connect to the school Wi-Fi because it requires a browser login.

Key concepts

STA
joins a network
AP
creates its own network
IP
network address
2.4 GHz
the band used by the ESP32
2

🌐 Web server on the ESP32: a page with buttons

The WebServer, which is included in the esp32 3.x package, turns the board into a small website. You register routes (“if someone requests /cmd?d=F, run this function”), and your phone’s browser becomes the remote control. You don’t need to install an app.

🌱 New here?

HTTP is how a browser communicates with a website: the browser sends a request (GET /cmd?d=F), and the server sends a response (text or a page). Each tap starts a new conversation that takes a few dozen milliseconds. That’s fine for buttons; for a joystick, we’ll see in topic 3 why it isn’t.

Goal: serve a page with five buttons. Each tap calls a route, the ESP32 writes the command to the Serial Monitor, and the GPIO 2 LED lights up while the robot “moves.” Test it without motors first; in topic 6, you’ll replace the LED with the motors.

botoes-web.ino
// Page with buttons served by the ESP32 (WebServer from the esp32 3.x package)
#include <WiFi.h>
#include <WebServer.h>

WebServer server(80);
const int PINO_LED = 2;

const char PAGINA[] = R"html(
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<style>body{font-family:sans-serif;text-align:center;background:#111;color:#eee}
button{width:90px;height:60px;margin:6px;font-size:18px;border-radius:10px}</style></head><body><h2>Rover INEMA</h2>
<button onclick="c('F')">Forward</button><br><button onclick="c('L')">Left</button><button onclick="c('S')">Stop</button>
<button onclick="c('R')">Right</button><br><button onclick="c('B')">Reverse</button><p id="st">ready</p>
<script>function c(d){fetch('/cmd?d='+d).then(r=>r.text()).then(t=>document.getElementById('st').textContent=t)}</script>
</body></html>)html";

void paginaInicial() { server.send(200, "text/html", PAGINA); }

void comando() {
  String d = server.arg("d");                   // F, B, L, R ou S
  digitalWrite(PINO_LED, d != "S");            // LED on = "moving"
  Serial.print("Command: "); Serial.println(d);
  server.send(200, "text/plain", "ok: " + d);
}

void setup() {
  Serial.begin(115200);
  pinMode(PINO_LED, OUTPUT);
  WiFi.softAP("RoverINEMA", "troque1234");
  server.on("/", paginaInicial);
  server.on("/cmd", comando);
  server.begin();
  Serial.print("Connect to RoverINEMA and open http://");
  Serial.println(WiFi.softAPIP());
}

void loop() {
  server.handleClient();                          // handles the requests that arrived
}

How to check

  • ✓On your phone, join the RoverINEMA network and open http://192.168.4.1: the five buttons appear.
  • ✓Tapping "Forward" prints "Command: F" in the Serial Monitor, turns on the LED, and the page shows "ok: F".
  • ✗Does the page open but the buttons don't respond? Your phone may have left the robot's network because it has "no internet"; tell it to stay connected.

✓ Do

  • ✓Call server.handleClient() on every loop iteration.
  • ✓Store the page in a constant and respond quickly.
  • ✓Use a large, always visible "Stop" button.

✗ Avoid

  • ✗delay() long in the loop: the page freezes until it finishes.
  • ✗Load images and libraries from the internet: there is no internet in AP mode.
  • ✗An "move" command without a corresponding "stop" command.

Why learn this

A page served by the robot itself works on any phone, with no app store needed. The same server can later display telemetry, adjust the PID gains from 2.3, and turn modes on and off without reflashing the ESP32.

Key concepts

Route
address → function
GET + parameter
/cmd?d=F
handleClient
handles requests in loop
fetch
request without reloading
3

⚡ WebSocket: lag-free control

With HTTP, each command opens and closes a conversation. A joystick sends its position ten times per second; opening ten conversations per second creates a queue, delay, and out-of-order commands. The WebSocket opens a single channel, which stays open, and messages pass through it in both directions with a delay of just a few milliseconds.

HTTP open·send·close open·send·closeopen·send·closeWebSocket channel opened once · each dot is a joystick positiontime →

How to read this map: on the top line, three commands take up the whole line, each paying the cost of opening and closing. On the bottom line, the same amount of time carries ten commands. When driving, what matters is the time between moving your finger and the wheel responding.

SituationUseWhy
Buttons, adjustments, turn mode onHTTPfew requests, easy to test
Joystick, steering wheel, continuous telemetryWebSocketsteady stream, low delay, two-way
No network, phone nearby onlyBLElow power use, no router (topic 4)

💡 Extra library

The esp32 package includes WiFi e WebServer, but not a WebSocket server. Install the WebSocketslibrary by Markus Sattler (links2004) from the Arduino IDE Library Manager, and use the latest version listed in the repository. It runs on port 81, alongside the web server on port 80.

The protocol you'll define is minimal: the browser sends the text x,y, with each number from -100 to 100 (e.g., 0,80 = forward at 80%). The ESP32 converts this into the speed of each wheel. The complete code, with the page, WebSocket, and motors, is in topic 6.

Why learn this

A robot controlled with a half-second delay hits the wall before it can obey. WebSocket is also the basis for the live dashboard in 2.5: the same channel that carries the command sends back the robot's speed and angle.

Key concepts

Latency
finger → wheel
Open channel
one connection, many messages
Two-way
command goes out, data comes back
"x,y" protocol
short text that's easy to debug
4

🔵 BLE: control it from your phone's app

O BLE (Bluetooth Low Energy) connects your phone to the robot without any network, using little battery power. Instead of pages, it organizes data into services e characteristics: the robot advertises "I have a command service," the app connects and writes a value to a characteristic. The esp32 3.x package already includes the BLE library; the lighter NimBLE-Arduino alternative is optional.

🌱 New here?

Think of a characteristic as a named little box (a UUID, a long, unique code) that the app can write to or read from. A service is the drawer that holds the boxes. Advertising is the robot shouting its name so nearby phones can find it.

phoneBLE appwrites "F" ESP32 "RoverINEMA" command service RX characteristiccan write motors

How to read this map: the dashed box is the service; inside it, the glowing characteristic is the only place where the app can write. The letter that arrives there becomes a motor command. The UUIDs in the code below follow the "UART over BLE" pattern that many generic apps already recognize.

Goal: receive letters over BLE (F, B, L, R, S), display them in the Serial Monitor, and turn on the LED. Use a free generic app such as nRF Connect or LightBlue to write to the characteristic.

ble-comandos.ino
// BLE commands using the BLE library built into the esp32 3.x package
#include <BLEDevice.h>
#include <BLEServer.h>

#define SERVICO_UUID "6E400001-B5A3-F393-E0A9-E50E24DCCA9E"
#define RX_UUID      "6E400002-B5A3-F393-E0A9-E50E24DCCA9E"
const int PINO_LED = 2;
volatile char ultimoCmd = 'S';

class ConexaoCb : public BLEServerCallbacks {
  void onConnect(BLEServer* s) { Serial.println("Phone connected"); }
  void onDisconnect(BLEServer* s) {
    ultimoCmd = 'S';                              // phone disconnected: stop
    Serial.println("Disconnected: STOPPED. Advertising again.");
    BLEDevice::startAdvertising();
  }
};

class RxCb : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic* c) {
    String v = c->getValue();                       // in 3.x, the value arrives as a String
    if (v.length() == 0) return;
    char cmd = toupper(v[0]);
    if (strchr("FBLRS", cmd)) { ultimoCmd = cmd; Serial.print("BLE command: "); Serial.println(cmd); }
  }
};

void setup() {
  Serial.begin(115200);
  pinMode(PINO_LED, OUTPUT);
  BLEDevice::init("RoverINEMA");
  BLEServer* srv = BLEDevice::createServer();
  srv->setCallbacks(new ConexaoCb());
  BLEService* servico = srv->createService(SERVICO_UUID);
  BLECharacteristic* rx = servico->createCharacteristic(RX_UUID,
      BLECharacteristic::PROPERTY_WRITE | BLECharacteristic::PROPERTY_WRITE_NR);
  rx->setCallbacks(new RxCb());
  servico->start();
  BLEDevice::getAdvertising()->addServiceUUID(SERVICO_UUID);
  BLEDevice::startAdvertising();
  Serial.println("BLE ready: look for RoverINEMA in the app");
}

void loop() {
  digitalWrite(PINO_LED, ultimoCmd != 'S');         // motors go here (topic 6)
  delay(20);
}

How to check

  • ✓In the app, “RoverINEMA” appears in the search. Connect: the Serial Monitor shows “Phone connected.”
  • ✓Write the text F to characteristic 6E400002: “BLE command: F” appears and the LED turns on. S turns it off.
  • ✓Turn off Bluetooth on your phone while the LED is on: “Disconnected: STOPPED” appears and the LED turns off.

💡 Wi-Fi and BLE in the same sketch

Each one fits in the default memory on its own. Together, the program usually exceeds the application space: under Tools → Partition Scheme, choose “Huge APP.” The ESP32 shares a single radio between the two, so range and latency get a little worse. For the rover, choose one of them.

Why learn this

BLE works where there’s no Wi-Fi, uses less power, and is the same standard used by game controllers, watches, and fitness sensors. Understanding services and characteristics also helps you read ready-made BLE sensors on the robot, wirelessly.

Key concepts

Service
data drawer
Characteristic
little box the app writes to
UUID
unique name
Advertising
robot announces itself
5

🛡️ Security: password and stopping if the connection is lost

With wireless control, two things can go wrong: someone who shouldn’t gets control, or the signal drops in the middle of a “forward” command. In the first case, a password solves it. In the second, the answer is the command watchdog: the robot only moves while it receives new commands. If nothing arrives for a short time (300 ms, for example), it stops on its own, even if the last command was “forward.”

🌱 New here?

Watchdog (“guard dog”) is a timer that needs to be “fed” from time to time. If no one feeds it, it acts. Here, each command that arrives feeds the dog; silence makes the robot stop. This is called designing for fail-safe operation: the state with no information is the stopped state.

commands signal lostmotor on 300 ms with no signal stoppedthe last command was “forward,” but silence wins

How to read this map: Each cyan mark is an incoming command. While they keep arriving, the motor (blue line) stays on. When they stop, the amber ruler counts 300 ms and the motor drops to zero. Without the watchdog, the blue line would stay high until the battery ran out or the robot hit something.

✓ Safe wireless robot

  • ✓Password on the AP (8 characters or more) and change the example password.
  • ✓Command watchdog: stop after 200 to 500 ms of silence.
  • ✓Stop on disconnect (WebSocket or BLE).
  • ✓Maximum speed limited on the ESP32, not just on the page.

✗ Recipe for an accident

  • ✗Open network, no password, at a fair crowded with phones.
  • ✗“Forward” that stays active until a “stop” command arrives.
  • ✗Trust that the phone’s page will send the “stop” command.
  • ✗Test the first controller with the robot on the floor near the stairs.

Watchdog test in 4 steps

1

Wheels off the ground

Set the rover on a case or box, with the wheels free to spin.

2

Hold “forward”

With the joystick from topic 6, keep your finger pointing up: the wheels turn.

3

Cut the signal

Turn on your phone’s airplane mode without lifting your finger.

4

Check

The wheels stop in less than half a second and the Serial Monitor shows “watchdog: no command, STOPPED.” Only then put the robot on the floor.

Why learn this

Real robots, from vacuum cleaners to autonomous forklifts, stop when they lose control. You’ll use the same idea in 3.5, when the Raspberry Pi sends commands to the ESP32, and in Track 4, with the ROS 2 topic /cmd_vel.

Key concepts

Watchdog
silence = stop
Fail-safe
no data, no movement
Timeout
200 to 500 ms
WPA2 password
only authorized users can connect
6

🕹️ Browser joystick for the rover

Now put it all together: the ESP32 creates the network, serves a page with a round joystick, receives your finger’s position over WebSocket, and converts it into the speed of the two wheels. This conversion is called differential mixing: left wheel = y + x, right wheel = y − x. Move your finger up and both wheels turn at the same speed; move it to the right and the left wheel speeds up while the right slows down, so the robot turns.

xy x = 50, y = 40left = y + x = 90 right = y − x = −10left fast, right almost stopped: the rover makes a tight turn to the right

How to read this map: The blue dot is your finger, 50 to the right and 40 up. The two calculations on the right give each wheel’s speed as a percentage. Negative values make a wheel spin backward; the code caps values above 100 at 100.

Goal: Drive the Track 1 rover from your phone, using a joystick, WebSocket, stop on disconnect, and a 300 ms watchdog. TB6612FNG connections: PWMA 25, AIN1 26, AIN2 27, PWMB 14, BIN1 16, BIN2 17, STBY connected to 3V3 (the same pin map used throughout Track 2; change it if your rover uses different pins). Requires the WebSockets library (topic 3).

rover-joystick.ino
// Rover with browser joystick: WebServer + WebSocket + watchdog (esp32 package 3.x)
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h>   // “WebSockets” library (Markus Sattler)

const int PWMA = 25, AIN1 = 26, AIN2 = 27;        // left motor
const int PWMB = 14, BIN1 = 16, BIN2 = 17;        // right motor
const int VEL_MAX = 200;                          // PWM ceiling (0 to 255): safety limit
const unsigned long TIMEOUT_MS = 300;             // command watchdog

WebServer server(80);
WebSocketsServer ws(81);
unsigned long ultimoComando = 0;
bool andando = false;

const char PAGINA[] = R"html(
<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<style>body{font-family:sans-serif;text-align:center;background:#111;color:#eee}
#pad{width:260px;height:260px;margin:20px auto;border-radius:50%;background:#222;position:relative;touch-action:none}#dot{width:70px;height:70px;border-radius:50%;background:#3b82f6;position:absolute;left:95px;top:95px}</style>
</head><body><h2>Rover INEMA</h2><div id="pad"><div id="dot"></div></div><p id="st">connecting...</p>
<script>
var pad=document.getElementById('pad'),dot=document.getElementById('dot'),st=document.getElementById('st');
var ws=new WebSocket('ws://'+location.hostname+':81/'),x=0,y=0,ativo=false;
ws.onopen=function(){st.textContent='connected'};ws.onclose=function(){st.textContent='disconnected'};function mover(e){var r=pad.getBoundingClientRect(),dx=e.clientX-r.left-130,dy=e.clientY-r.top-130,d=Math.hypot(dx,dy);
if(d>95){dx=dx*95/d;dy=dy*95/d}dot.style.left=(95+dx)+'px';dot.style.top=(95+dy)+'px';
x=Math.round(dx/95*100);y=Math.round(-dy/95*100)}
function soltar(){ativo=false;x=0;y=0;dot.style.left='95px';dot.style.top='95px'}
pad.onpointerdown=function(e){ativo=true;pad.setPointerCapture(e.pointerId);mover(e)};
pad.onpointermove=function(e){if(ativo)mover(e)};pad.onpointerup=soltar;pad.onpointercancel=soltar;
setInterval(function(){if(ws.readyState===1)ws.send(x+','+y)},100);
</script></body></html>)html";

void motor(int pwm, int in1, int in2, int v) {   // v de -255 a 255
  v = constrain(v, -VEL_MAX, VEL_MAX);
  digitalWrite(in1, v > 0);
  digitalWrite(in2, v < 0);
  ledcWrite(pwm, abs(v));                             // API 3.x: ledcWrite takes the PIN
}

void parar() { motor(PWMA, AIN1, AIN2, 0); motor(PWMB, BIN1, BIN2, 0); andando = false; }

void aoReceber(uint8_t num, WStype_t tipo, uint8_t* dados, size_t tam) {
  if (tipo == WStype_TEXT) {
    char buf[24];
    size_t n = tam < sizeof(buf) - 1 ? tam : sizeof(buf) - 1;
    memcpy(buf, dados, n); buf[n] = 0;
    int x = 0, y = 0;
    if (sscanf(buf, "%d,%d", &x, &y) == 2) {
      motor(PWMA, AIN1, AIN2, (y + x) * 255 / 100);  // differential mixing
      motor(PWMB, BIN1, BIN2, (y - x) * 255 / 100);
      ultimoComando = millis();                      // feeds the watchdog
      andando = (x != 0 || y != 0);
    }
  } else if (tipo == WStype_DISCONNECTED) {
    parar();
    Serial.printf("Client %u disconnected: STOPPED\n", num);
  } else if (tipo == WStype_CONNECTED) {
    Serial.printf("Client %u connected\n", num);
  }
}

void setup() {
  Serial.begin(115200);
  int saidas[] = {AIN1, AIN2, BIN1, BIN2};
  for (int p : saidas) pinMode(p, OUTPUT);
  ledcAttach(PWMA, 20000, 8);                         // 20 kHz, 8 bits (0 to 255)
  ledcAttach(PWMB, 20000, 8);
  parar();
  WiFi.softAP("RoverINEMA", "troque1234");
  server.on("/", []() { server.send(200, "text/html", PAGINA); });
  server.begin();
  ws.begin();
  ws.onEvent(aoReceber);
  Serial.print("Connect to RoverINEMA and open http://");
  Serial.println(WiFi.softAPIP());
}

void loop() {
  server.handleClient();
  ws.loop();
  if (andando && millis() - ultimoComando > TIMEOUT_MS) {
    parar();
    Serial.println("watchdog: no command, STOPPED");
  }
}

How to check

  • ✓With the wheels off the ground, open http://192.168.4.1/: the page says "connected" and the Serial Monitor says "Client 0 connected".
  • ✓Move your finger up: both wheels go forward. Move your finger right: the wheels turn in opposite directions. Release: they stop.
  • ✓Close the tab while moving your finger up: "Client 0 disconnected: STOPPED". Turn on airplane mode: "watchdog: no command, STOPPED".
  • ✗Is one wheel turning backward? Swap the two wires for that motor on the driver, not in the code.

🌱 New here?

The text between R"html( e )html" is a raw string in C++: everything inside it, including quotation marks and line breaks, becomes text without needing backslashes. It’s the cleanest way to store an entire HTML page inside the sketch.

Why learn this

With the rover controlled by your phone, you can test each upcoming module with the robot on the floor and your hands free: take it to the track, position it, and let go. It’s also the foundation of the "manual" mode you’ll want in any autonomous robot, so you can get it out of a corner when autonomy goes wrong.

Key concepts

Differential mixing
y + x e y − x
Pointer events
finger and mouse work the same
VEL_MAX
speed limit on the robot
10 messages/s
feeds the watchdog

✅ Module summary

✓
Station or access point: join your home network or create one for the robot; AP mode is the most predictable when you’re away from home.
✓
WebServer for buttons: simple routes, no app to install.
✓
WebSocket for continuous control: one open channel, commands with little delay.
✓
BLE without a network: a service, a characteristic, and a generic app.
✓
No signal = stop: command watchdog, stop on disconnect, and maximum speed enforced by the robot itself.

To go further

Next module

2.2 · IMU and encoders: the robot can now tell how far it has turned, how far it has traveled, and where it is.