Contenido detallado
📶 Wi-Fi: modo estación y punto de acceso
El Wi-Fi del ESP32 funciona de dos maneras. En modo estación (STA), se conecta a la red de tu casa o escuela, como cualquier celular, y recibe una dirección del router. En modo punto de acceso (AP), crea su propia red: el celular se conecta directamente al robot, sin router. Para un robot que va a la calle, a una feria de ciencias o a la cancha, el AP es la opción más predecible.
🌱 ¿Eres nuevo aquí?
O SSID es el nombre de la red que aparece en el celular. La dirección IP es el «número de casa» de cada dispositivo en la red (ej.: 192.168.4.1). Un router distribuye esas direcciones. En modo AP, el propio ESP32 cumple esa función y siempre está en 192.168.4.1.
Cómo leer: a la izquierda, todo pasa por el router; si está lejos o la red pide iniciar sesión, el robot se queda sin control. A la derecha, el ESP32 es la propia red y el celular se comunica directamente con él. El sketch de abajo intenta la primera opción y pasa a la segunda si no lo consigue.
Objetivo: intentar conectarse a tu red durante 10 segundos; si falla, crear la red propia del robot. El Monitor Serial muestra qué modo funcionó y la dirección que debes abrir en el navegador.
// Wi-Fi: intenta el modo estación y, si falla, pasa al punto de acceso (paquete esp32 3.x)
#include <WiFi.h>
const char* REDE_SSID = "<seu_wifi>"; // cámbialo por el nombre de tu red
const char* REDE_SENHA = "<sua_senha>";
const char* AP_SSID = "RoverINEMA";
const char* AP_SENHA = "troque1234"; // mínimo de 8 caracteres
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("Modo ESTACAO. Abra http://");
Serial.println(WiFi.localIP());
} else {
WiFi.disconnect(true);
WiFi.mode(WIFI_AP);
WiFi.softAP(AP_SSID, AP_SENHA);
Serial.print("Modo PONTO DE ACESSO. Conecte em RoverINEMA e abra http://");
Serial.println(WiFi.softAPIP());
}
}
void loop() { // cada 5 s: cuántos dispositivos están en la red del robot
if (WiFi.getMode() == WIFI_AP) { Serial.print("Clientes: "); Serial.println(WiFi.softAPgetStationNum()); }
delay(5000);
}
Cómo verificar
- ✓Con el SSID y la contraseña correctos: «Modo ESTACAO. Abra http://192.168.x.y».
- ✓Si escribes mal la contraseña a propósito: después de 10 s, la red «RoverINEMA» aparece en el celular y el contador de clientes sube a 1 cuando te conectas.
- ✗¿Nunca se conecta? El ESP32 solo detecta redes de 2,4 GHz; las redes de 5 GHz únicamente no aparecen.
Por qué aprender
Todo control remoto de este módulo, y la telemetría del 2.5, empieza con el ESP32 conectado a la red. Elegir el modo correcto evita la escena clásica de la feria: el robot funcionaba en casa y no se conecta al Wi-Fi de la escuela, que pide iniciar sesión en el navegador.
Conceptos clave
🌐 Servidor web en el ESP32: una página con botones
La biblioteca WebServer, que ya viene en el paquete esp32 3.x, convierte la placa en un pequeño sitio web. Registras rutas («si alguien solicita /cmd?d=F, ejecuta esta función») y el navegador del celular se convierte en el control remoto. No hace falta instalar ninguna aplicación.
🌱 ¿Eres nuevo aquí?
HTTP es la conversación entre el navegador y un sitio web: el navegador hace una solicitud (GET /cmd?d=F) y el servidor envía una respuesta (texto o página). Cada clic abre una conversación nueva, que tarda algunas decenas de milisegundos. Para los botones basta; para un joystick, veremos en el tema 3 que no.
Objetivo: servir una página con cinco botones. Cada toque llama a una ruta, el ESP32 escribe el comando en el Serial Monitor y enciende el LED del GPIO 2 mientras el robot «avanza». Primero haz la prueba sin motores; en el tema 6 cambiarás el LED por los motores.
// Página con botones servida por el ESP32 (WebServer del paquete esp32 3.x)
#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')">Frente</button><br><button onclick="c('L')">Esq.</button><button onclick="c('S')">Parar</button>
<button onclick="c('R')">Dir.</button><br><button onclick="c('B')">Ré</button><p id="st">pronto</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 encendido = "en movimiento"
Serial.print("Comando: "); 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("Conéctate a RoverINEMA y abre http://");
Serial.println(WiFi.softAPIP());
}
void loop() {
server.handleClient(); // atiende las solicitudes recibidas
}
Cómo verificar
- ✓En el celular, conéctate a la red RoverINEMA y abre http://192.168.4.1: verás los cinco botones.
- ✓Al tocar «Frente», aparece «Comando: F» en el Serial Monitor, se enciende el LED y la página muestra «ok: F».
- ✗¿La página se abre, pero los botones no responden? Es posible que el celular haya salido de la red del robot por «falta de internet»; indícale que mantenga la conexión.
✓ Hazlo
- ✓Llamar server.handleClient() en cada vuelta del loop.
- ✓Guardar la página en una constante y responder rápido.
- ✓Usar un botón «Parar» grande y siempre visible.
✗ Evita
- ✗delay() largo en el loop: la página se bloquea hasta que termine.
- ✗Cargar imágenes y bibliotecas de internet: en modo AP no hay internet.
- ✗Comando «avanzar» sin el comando «parar» correspondiente.
Por qué aprender
Una página servida por el propio robot funciona en cualquier celular, sin tienda de aplicaciones. Después, el mismo servidor mostrará telemetría, ajustará las ganancias del PID del 2.3 y activará y desactivará modos, sin volver a grabar el ESP32.
Conceptos clave
⚡ WebSocket: control sin demora
Con HTTP, cada comando abre y cierra una conversación. Un joystick envía la posición diez veces por segundo; abrir diez conversaciones por segundo genera una cola, demoras y comandos que llegan fuera de orden. WebSocket WebSocket abre un solo canal, que permanece abierto, y los mensajes circulan en ambos sentidos con unos pocos milisegundos de demora.
Cómo leer: en la línea de arriba, tres comandos ocupan toda la línea y cada uno paga el costo de abrir y cerrar. En la de abajo, el mismo tiempo transporta diez comandos. Para conducir, lo que importa es el tiempo entre mover el dedo y que la rueda responda.
| Situación | Usa | Por qué |
|---|---|---|
| Botones, ajustes, activar un modo | HTTP | pocas solicitudes, fáciles de probar |
| Joystick, volante, telemetría continua | WebSocket | flujo constante, poca demora, comunicación en ambos sentidos |
| Sin red, solo con el celular cerca | BLE | bajo consumo, sin router (tema 4) |
💡 Biblioteca adicional
El paquete esp32 incluye WiFi e WebServer, pero no un servidor WebSocket. Instala desde el Administrador de bibliotecas de Arduino IDE la biblioteca WebSockets, de Markus Sattler (links2004), y usa la versión más reciente indicada en el repositorio. Funciona en el puerto 81, junto al servidor web del puerto 80.
El protocolo que vas a definir es mínimo: el navegador envía el texto x,y, con cada número entre -100 y 100 (por ejemplo: 0,80 = avance al 80%). El ESP32 lo convierte en la velocidad de cada rueda. El código completo, con la página, WebSocket y motores, está en el tema 6.
Por qué aprender
Un robot controlado con medio segundo de demora choca contra la pared antes de obedecer. WebSocket también es la base del panel en vivo del 2.5: el mismo canal que transmite el comando devuelve la velocidad y el ángulo del robot.
Conceptos clave
🔵 BLE: controlar desde la aplicación del celular
O BLE (Bluetooth Low Energy) conecta el celular con el robot sin necesidad de una red y consume poca batería. En lugar de páginas, organiza los datos en servicios e características : el robot anuncia «tengo un servicio de comandos», la app se conecta y escribe un valor en una característica. El paquete esp32 3.x ya incluye la biblioteca BLE; la alternativa NimBLE-Arduino, más ligera, es opcional.
🌱 ¿Eres nuevo aquí?
Piensa en una característica como una cajita con nombre (un UUID, código largo y único) donde la app puede escribir o leer. Un servicio es el cajón que agrupa cajitas. Anunciar (advertising) es cuando el robot anuncia su propio nombre para que los celulares cercanos lo encuentren.
Cómo leer: el recuadro punteado es el servicio; dentro de él, la característica resaltada es el único lugar donde la app escribe. La letra que llega allí se convierte en un comando para el motor. Los UUID del código de abajo siguen el estándar «UART sobre BLE», que muchas apps genéricas ya reconocen.
Objetivo: recibir letras por BLE (F, B, L, R, S), mostrarlas en el Serial Monitor y encender el LED. Usa una app genérica gratuita como nRF Connect o LightBlue para escribir en la característica.
// Comandos por BLE con la biblioteca BLE incluida en el paquete esp32 3.x
#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("Celular conectado"); }
void onDisconnect(BLEServer* s) {
ultimoCmd = 'S'; // perdió el celular: se detiene
Serial.println("Se desconectó: DETENIDO. Anunciando de nuevo.");
BLEDevice::startAdvertising();
}
};
class RxCb : public BLECharacteristicCallbacks {
void onWrite(BLECharacteristic* c) {
String v = c->getValue(); // en la 3.x el valor llega como String
if (v.length() == 0) return;
char cmd = toupper(v[0]);
if (strchr("FBLRS", cmd)) { ultimoCmd = cmd; Serial.print("Comando BLE: "); 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 listo: busca RoverINEMA en la app");
}
void loop() {
digitalWrite(PINO_LED, ultimoCmd != 'S'); // aquí se agregan los motores (tema 6)
delay(20);
}
Cómo verificar
- ✓En la app, «RoverINEMA» aparece en la búsqueda. Conéctate: el Serial Monitor muestra «Celular conectado».
- ✓Escribe el texto F en la característica 6E400002: aparece «Comando BLE: F» y el LED se enciende. S lo apaga.
- ✓Apaga el Bluetooth del celular mientras el LED está encendido: aparece «Se desconectó: DETENIDO» y el LED se apaga.
💡 Wi-Fi y BLE en el mismo sketch
Cada uno por separado cabe en la memoria predeterminada. Juntos, el programa suele superar el espacio de aplicación: en Herramientas → Partition Scheme, elige «Huge APP». El ESP32 comparte una sola radio entre ambos, así que el alcance y la latencia empeoran un poco. Para el rover, elige uno de los dos.
Por qué aprender
BLE funciona donde no hay Wi-Fi, consume menos y es el mismo estándar de los controles de videojuegos, relojes y sensores de gimnasio. Entender los servicios y las características también te ayuda a leer sensores BLE listos para usar en el robot, sin cables.
Conceptos clave
🛡️ Seguridad: contraseña y detención si se pierde la conexión
Con una conexión inalámbrica, pueden ocurrir dos cosas: alguien que no debería toma el control, o la señal se corta en medio de un comando de «avanzar». En el primer caso, una contraseña lo resuelve. En el segundo, la respuesta es el watchdog de comandos: el robot solo se mueve mientras recibe comandos nuevos. Si pasa poco tiempo (300 ms, por ejemplo) sin que llegue nada, se detiene solo, aunque el último comando haya sido «avanzar».
🌱 ¿Eres nuevo aquí?
Watchdog («perro guardián») es un temporizador al que hay que «alimentar» de vez en cuando. Si nadie lo alimenta, actúa. Aquí, cada comando que llega alimenta al perro; el silencio hace que el robot se detenga. A esto se le llama diseñar para una falla segura: si no hay información, el estado es detenido.
Cómo leer: cada trazo cian representa la llegada de un comando. Mientras llegan, el motor (línea azul) sigue encendido. Cuando dejan de llegar, la escala ámbar cuenta 300 ms y el motor baja a cero. Sin el watchdog, la línea azul se quedaría arriba hasta que se agotara la batería o el robot chocara.
✓ Robot inalámbrico seguro
- ✓Contraseña en el AP (8 caracteres o más) y cambia la contraseña de ejemplo.
- ✓Watchdog de comandos: detenerse tras 200 a 500 ms de silencio.
- ✓Detenerse al desconectarse (WebSocket o BLE).
- ✓Velocidad máxima limitada en el ESP32, no solo en la página.
✗ Receta para un accidente
- ✗Red abierta, sin contraseña, en una feria llena de celulares.
- ✗«Avanzar» hasta que llegue un comando de «detenerse».
- ✗Confiar en que la página del celular enviará el comando de «detenerse».
- ✗Probar el primer control con el robot en el suelo, cerca de la escalera.
Prueba del watchdog en 4 pasos
Ruedas en el aire
Apoya el rover sobre un estuche o una caja, con las ruedas libres.
Mantén presionado «avanzar»
Con el joystick del tema 6, mantén el dedo hacia arriba: las ruedas giran.
Corta la señal
Activa el modo avión del celular sin levantar el dedo.
Verifica
Las ruedas se detienen en menos de medio segundo y el Serial Monitor muestra «watchdog: sin comando, DETENIDO». Solo entonces pon el robot en el suelo.
Por qué aprender
Los robots reales, desde las aspiradoras hasta los montacargas autónomos, se detienen cuando pierden el control. Vas a usar la misma idea en el 3.5, cuando Raspberry Pi envíe comandos al ESP32, y en la Ruta 4, con el tema /cmd_vel de ROS 2.
Conceptos clave
🕹️ Joystick en el navegador para el rover
Ahora, todo junto: el ESP32 crea la red, muestra una página con un joystick redondo, recibe la posición del dedo por WebSocket y la convierte en la velocidad de las dos ruedas. La conversión es la mezcla diferencial: rueda izquierda = y + x, rueda derecha = y − x. Si llevas el dedo hacia arriba, ambas giran igual; si lo llevas hacia la derecha, la izquierda acelera y la derecha frena, y el robot gira.
Cómo leer: el punto azul es tu dedo, a 50 hacia la derecha y 40 hacia arriba. Los dos cálculos de la derecha determinan la velocidad de cada rueda, en porcentaje. Los valores negativos hacen que la rueda gire hacia atrás; por encima de 100, el código limita el valor a 100.
Objetivo: conducir el rover de la Trilha 1 desde el celular, con joystick, WebSocket, parada al desconectarse y watchdog de 300 ms. Conexión al TB6612FNG: PWMA 25, AIN1 26, AIN2 27, PWMB 14, BIN1 16, BIN2 17, STBY conectado a 3V3 (el mismo mapa de toda la Trilha 2; cámbialo si tu rover usa otros pines). Necesitas la biblioteca WebSockets (tema 3).
// Rover con joystick en el navegador: WebServer + WebSocket + watchdog (paquete esp32 3.x)
#include <WiFi.h>
#include <WebServer.h>
#include <WebSocketsServer.h> // biblioteca "WebSockets" (Markus Sattler)
const int PWMA = 25, AIN1 = 26, AIN2 = 27; // motor izquierdo
const int PWMB = 14, BIN1 = 16, BIN2 = 17; // motor derecho
const int VEL_MAX = 200; // límite de PWM (0 a 255): límite de seguridad
const unsigned long TIMEOUT_MS = 300; // watchdog de comando
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">conectando...</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='conectado'};ws.onclose=function(){st.textContent='desconectado'};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 recibe el 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); // mezcla diferencial
motor(PWMB, BIN1, BIN2, (y - x) * 255 / 100);
ultimoComando = millis(); // alimenta el watchdog
andando = (x != 0 || y != 0);
}
} else if (tipo == WStype_DISCONNECTED) {
parar();
Serial.printf("Cliente %u salió: PARADO\n", num);
} else if (tipo == WStype_CONNECTED) {
Serial.printf("Cliente %u se conectó\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 a 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("Conéctate a RoverINEMA y abre http://");
Serial.println(WiFi.softAPIP());
}
void loop() {
server.handleClient();
ws.loop();
if (andando && millis() - ultimoComando > TIMEOUT_MS) {
parar();
Serial.println("watchdog: sin comando, PARADO");
}
}
Cómo verificar
- ✓Con las ruedas en el aire, abre http://192.168.4.1:; la página dice "conectado" y el Serial Monitor, "Cliente 0 conectou".
- ✓Dedo hacia arriba: las dos ruedas avanzan. Dedo hacia la derecha: las ruedas giran en sentidos opuestos. Suelta: se detienen.
- ✓Cerrar la pestaña con el dedo hacia arriba: "Cliente 0 salió: PARADO". Modo avión: "watchdog: sin comando, PARADO".
- ✗¿Una rueda gira al revés? Invierte los dos cables de ese motor en el driver, no el código.
🌱 ¿Eres nuevo aquí?
El texto entre R"html( e )html" es una cadena sin procesar de C++: todo lo que contiene, incluidas las comillas y los saltos de línea, se convierte en texto sin necesidad de barras invertidas. Es la forma más limpia de guardar una página HTML completa dentro del sketch.
Por qué aprender
Con el rover manejable desde el celular, puedes probar cada módulo siguiente con el robot en el suelo y las manos libres: lo llevas hasta la pista, lo colocas y lo sueltas. También es la base del modo "manual" que querrás en cualquier robot autónomo, para sacarlo de una esquina cuando falle la autonomía.
Conceptos clave
✅ Resumen del módulo
Para profundizar
- Wi-Fi API (arduino-esp32) — modos STA y AP, eventos de conexión
- BLE API (arduino-esp32) — la biblioteca BLE integrada
- Guía de migración 2.x → 3.0 — qué cambió en Wi-Fi y BLE
- Biblioteca WebSockets (Links2004) — ejemplos y versión actual
- NimBLE-Arduino — alternativa BLE más ligera (opcional)
Siguiente módulo
2.2 · IMU y encoders: el robot empieza a saber cuánto giró, cuánto avanzó y dónde está.