PTENES
MÓDULO 2.1 · PRÁTICA

📱 Controle pelo celular: Wi-Fi e BLE

O ESP32 já tem Wi-Fi e Bluetooth no chip. Neste módulo você usa os dois para dirigir o rover pelo celular: uma página com botões, um joystick no navegador sem atraso e um app BLE. E, antes de tudo, a regra que separa brinquedo de robô seguro: perdeu o sinal, o robô para.

6
Tópicos
Intermediário
Nível
Prática
Tipo
Rover da T1
Hardware
0%
0 de 0 tópicos lidos · Seção 1 de 6

Conteúdo detalhado

1

📶 Wi-Fi: modo estação e ponto de acesso

O Wi-Fi do ESP32 funciona de dois jeitos. No modo estação (STA) ele entra na rede da sua casa ou escola, como um celular qualquer, e recebe um endereço do roteador. No modo ponto de acesso (AP) ele cria a própria rede: o celular se conecta direto no robô, sem roteador. Para o robô que vai para a rua, a feira de ciências ou a quadra, o AP é o caminho mais previsível.

🌱 Novo aqui?

O SSID é o nome da rede que aparece no celular. O endereço IP é o "número da casa" de cada aparelho na rede (ex.: 192.168.4.1). Um roteador distribui esses endereços. No modo AP, o próprio ESP32 faz esse papel e fica sempre em 192.168.4.1.

ESTAÇÃO (STA) PONTO DE ACESSO (AP)roteador celularESP32 ESP32 celularIP dado pelo roteador IP fixo 192.168.4.1

Como ler: à esquerda, tudo passa pelo roteador; se ele estiver longe ou a rede pedir login, o robô fica sem controle. À direita, o ESP32 é a própria rede e o celular fala com ele direto. O sketch abaixo tenta o primeiro e cai no segundo se não conseguir.

Objetivo: tentar entrar na sua rede por 10 segundos; se falhar, criar a rede própria do robô. O Serial Monitor mostra qual modo venceu e o endereço para abrir no navegador.

wifi-sta-ou-ap.ino
// Wi-Fi: tenta modo estação, cai para ponto de acesso (pacote esp32 3.x)
#include <WiFi.h>

const char* REDE_SSID  = "<seu_wifi>";      // troque pelo nome da sua rede
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() {                                   // a cada 5 s: quantos aparelhos estão na rede do robô
  if (WiFi.getMode() == WIFI_AP) { Serial.print("Clientes: "); Serial.println(WiFi.softAPgetStationNum()); }
  delay(5000);
}

Como verificar

  • ✓Com SSID e senha certos: "Modo ESTACAO. Abra http://192.168.x.y".
  • ✓Com a senha errada de propósito: depois de 10 s, a rede "RoverINEMA" aparece no celular e o contador de clientes sobe para 1 quando você entra.
  • ✗Nunca conecta? O ESP32 só enxerga redes de 2,4 GHz; redes só de 5 GHz não aparecem para ele.

Por que aprender

Todo controle remoto deste módulo, e a telemetria do 2.5, começa com o ESP32 na rede. Escolher o modo certo evita a cena clássica da feira: o robô funcionava em casa e não conecta no Wi-Fi da escola, que pede login no navegador.

Conceitos-chave

STA
entra numa rede
AP
cria a própria rede
IP
endereço na rede
2,4 GHz
a faixa que o ESP32 usa
2

🌐 Servidor web no ESP32: uma página com botões

A biblioteca WebServer, que já vem no pacote esp32 3.x, transforma a placa num pequeno site. Você registra rotas ("quem pedir /cmd?d=F, roda esta função") e o navegador do celular vira o controle remoto. Não precisa instalar app nenhum.

🌱 Novo aqui?

HTTP é a conversa do navegador com um site: o navegador faz um pedido (GET /cmd?d=F) e o servidor manda uma resposta (texto ou página). Cada clique abre uma conversa nova, que leva algumas dezenas de milissegundos. Para botões, basta; para um joystick, veremos no tópico 3 que não.

Objetivo: servir uma página com cinco botões. Cada toque chama uma rota, o ESP32 escreve o comando no Serial Monitor e acende o LED do GPIO 2 enquanto o robô "anda". Teste primeiro sem motores; no tópico 6 você troca o LED pelos motores.

botoes-web.ino
// Página com botões servida pelo ESP32 (WebServer do pacote 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 aceso = "em movimento"
  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("Conecte em RoverINEMA e abra http://");
  Serial.println(WiFi.softAPIP());
}

void loop() {
  server.handleClient();                          // atende os pedidos que chegaram
}

Como verificar

  • ✓No celular, entre na rede RoverINEMA e abra http://192.168.4.1: aparecem os cinco botões.
  • ✓Tocar em "Frente" escreve "Comando: F" no Serial Monitor, acende o LED e a página mostra "ok: F".
  • ✗A página abre mas os botões não respondem? O celular pode ter saído da rede do robô por "falta de internet"; mande ele manter a conexão.

✓ Faça

  • ✓Chamar server.handleClient() em todo giro do loop.
  • ✓Guardar a página numa constante e responder rápido.
  • ✓Usar um botão "Parar" grande e sempre visível.

✗ Evite

  • ✗delay() longo no loop: a página trava até ele acabar.
  • ✗Carregar imagens e bibliotecas da internet: no modo AP não há internet.
  • ✗Comando "andar" sem comando "parar" correspondente.

Por que aprender

Uma página servida pelo próprio robô funciona em qualquer celular, sem loja de apps. O mesmo servidor depois mostra telemetria, ajusta ganhos do PID do 2.3 e liga e desliga modos, sem regravar o ESP32.

Conceitos-chave

Rota
endereço → função
GET + parâmetro
/cmd?d=F
handleClient
atende no loop
fetch
pedido sem recarregar
3

⚡ WebSocket: controle sem atraso

Com HTTP, cada comando abre e fecha uma conversa. Um joystick manda a posição dez vezes por segundo; abrir dez conversas por segundo gera fila, atraso e comandos chegando fora de ordem. O WebSocket abre um canal só, que fica aberto, e as mensagens passam por ele nos dois sentidos com poucos milissegundos de atraso.

HTTP abre·envia·fecha abre·envia·fechaabre·envia·fechaWebSocket canal aberto uma vez · cada ponto é uma posição do joysticktempo →

Como ler: na linha de cima, três comandos ocupam a linha inteira, cada um pagando o custo de abrir e fechar. Na de baixo, o mesmo tempo carrega dez comandos. Para dirigir, o que importa é o tempo entre mexer o dedo e a roda responder.

SituaçãoUsePor quê
Botões, ajustes, ligar modoHTTPpoucos pedidos, simples de testar
Joystick, volante, telemetria contínuaWebSocketfluxo constante, baixo atraso, mão dupla
Sem rede, só o celular pertoBLEbaixo consumo, sem roteador (tópico 4)

💡 Biblioteca extra

O pacote esp32 traz WiFi e WebServer, mas não um servidor WebSocket. Instale pelo Gerenciador de Bibliotecas da Arduino IDE a biblioteca WebSockets, de Markus Sattler (links2004), e use a versão mais recente indicada no repositório. Ela roda na porta 81, ao lado do servidor web da porta 80.

O protocolo que você vai inventar é mínimo: o navegador manda o texto x,y, com cada número de -100 a 100 (ex.: 0,80 = frente a 80%). O ESP32 converte isso em velocidade de cada roda. O código completo, com página, WebSocket e motores, está no tópico 6.

Por que aprender

Robô dirigido com atraso de meio segundo bate na parede antes de obedecer. O WebSocket também é a base do painel ao vivo do 2.5: o mesmo canal que leva o comando traz de volta a velocidade e o ângulo do robô.

Conceitos-chave

Latência
dedo → roda
Canal aberto
uma conexão, muitas mensagens
Mão dupla
comando vai, dado volta
Protocolo "x,y"
texto curto e fácil de depurar
4

🔵 BLE: controlar pelo app do celular

O BLE (Bluetooth Low Energy) liga o celular ao robô sem rede nenhuma, gastando pouca bateria. Em vez de páginas, ele organiza os dados em serviços e características: o robô anuncia "tenho um serviço de comandos", o app se conecta e escreve um valor numa característica. O pacote esp32 3.x já traz a biblioteca BLE; a alternativa NimBLE-Arduino, mais leve, é opcional.

🌱 Novo aqui?

Pense numa característica como uma caixinha com nome (um UUID, código longo e único) onde o app pode escrever ou ler. Um serviço é a gaveta que agrupa caixinhas. Anunciar (advertising) é o robô gritando o próprio nome para os celulares em volta o encontrarem.

celularapp BLEescreve "F" ESP32 "RoverINEMA" serviço de comandos característica RXpode escrever motores

Como ler: a caixa tracejada é o serviço; dentro dele, a característica com brilho é o único lugar onde o app escreve. A letra que chega ali vira comando de motor. Os UUIDs do código abaixo seguem o padrão "UART sobre BLE" que muitos apps genéricos já reconhecem.

Objetivo: receber letras pelo BLE (F, B, L, R, S), mostrar no Serial Monitor e acender o LED. Use um app genérico gratuito como nRF Connect ou LightBlue para escrever na característica.

ble-comandos.ino
// Comandos por BLE com a biblioteca BLE embutida no pacote 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';                              // perdeu o celular: para
    Serial.println("Desconectou: PARADO. Anunciando de novo.");
    BLEDevice::startAdvertising();
  }
};

class RxCb : public BLECharacteristicCallbacks {
  void onWrite(BLECharacteristic* c) {
    String v = c->getValue();                       // na 3.x o valor chega 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 pronto: procure RoverINEMA no app");
}

void loop() {
  digitalWrite(PINO_LED, ultimoCmd != 'S');         // aqui entram os motores (tópico 6)
  delay(20);
}

Como verificar

  • ✓No app, "RoverINEMA" aparece na busca. Conecte: o Serial Monitor mostra "Celular conectado".
  • ✓Escreva o texto F na característica 6E400002: aparece "Comando BLE: F" e o LED acende. S apaga.
  • ✓Desligue o Bluetooth do celular com o LED aceso: aparece "Desconectou: PARADO" e o LED apaga.

💡 Wi-Fi e BLE no mesmo sketch

Cada um sozinho cabe na memória padrão. Juntos, o programa costuma passar do espaço de aplicação: em Ferramentas → Partition Scheme, escolha "Huge APP". O ESP32 divide um rádio só entre os dois, então o alcance e o atraso pioram um pouco. Para o rover, escolha um dos dois.

Por que aprender

BLE funciona onde não há Wi-Fi, gasta menos e é o mesmo padrão de controles de jogo, relógios e sensores de academia. Entender serviço e característica também ajuda a ler sensores BLE prontos no robô, sem fio.

Conceitos-chave

Serviço
gaveta de dados
Característica
caixinha que o app escreve
UUID
nome único
Advertising
robô se anuncia
5

🛡️ Segurança: senha e parada se perder a conexão

Sem fio, duas coisas dão errado: alguém que não devia entra no controle, e o sinal cai no meio de um comando "frente". No primeiro caso, uma senha resolve. No segundo, a resposta é o watchdog de comando: o robô só se move enquanto recebe comandos novos. Se passar um tempo curto (300 ms, por exemplo) sem nada chegar, ele para sozinho, mesmo que o último comando fosse "frente".

🌱 Novo aqui?

Watchdog ("cão de guarda") é um temporizador que precisa ser "alimentado" de tempos em tempos. Se ninguém alimentar, ele age. Aqui, cada comando que chega alimenta o cão; o silêncio faz o robô parar. Isso se chama projetar para falha segura: o estado sem informação é o estado parado.

comandos sinal caiumotor ligado 300 ms sem nada paradoo último comando era "frente", mas o silêncio vence

Como ler: cada traço ciano é um comando chegando. Enquanto eles chegam, o motor (linha azul) segue ligado. Quando param, a régua âmbar conta 300 ms e o motor cai para zero. Sem o watchdog, a linha azul ficaria no alto até a bateria acabar ou o robô bater.

✓ Robô sem fio seguro

  • ✓Senha no AP (8 caracteres ou mais) e troca da senha de exemplo.
  • ✓Watchdog de comando: parar após 200 a 500 ms de silêncio.
  • ✓Parar ao desconectar (WebSocket ou BLE).
  • ✓Velocidade máxima limitada no ESP32, não só na página.

✗ Receita de acidente

  • ✗Rede aberta, sem senha, numa feira cheia de celulares.
  • ✗"Frente" que vale até chegar um "parar".
  • ✗Confiar que a página do celular vai mandar o "parar".
  • ✗Testar o primeiro controle com o robô no chão perto da escada.

Teste do watchdog em 4 passos

1

Rodas no ar

Apoie o rover num estojo ou caixa, com as rodas livres.

2

Segure "frente"

Com o joystick do tópico 6, mantenha o dedo para cima: as rodas giram.

3

Corte o sinal

Ative o modo avião do celular sem soltar o dedo.

4

Confira

As rodas param em menos de meio segundo e o Serial Monitor mostra "watchdog: sem comando, PARADO". Só então leve o robô ao chão.

Por que aprender

Robôs de verdade, de aspirador a empilhadeira autônoma, param quando perdem o controle. Você vai usar a mesma ideia no 3.5, quando o Raspberry Pi mandar comandos ao ESP32, e na Trilha 4, com o tópico /cmd_vel do ROS 2.

Conceitos-chave

Watchdog
silêncio = parar
Falha segura
sem dado, sem movimento
Timeout
200 a 500 ms
Senha WPA2
só quem deve entra
6

🕹️ Joystick no navegador para o rover

Agora tudo junto: o ESP32 cria a rede, serve uma página com um joystick redondo, recebe a posição do dedo pelo WebSocket e converte em velocidade das duas rodas. A conversão é a mistura diferencial: roda esquerda = y + x, roda direita = y − x. Dedo para cima, as duas giram iguais; dedo para a direita, a esquerda acelera e a direita freia, e o robô vira.

xy x = 50, y = 40esquerda = y + x = 90 direita = y − x = −10esquerda forte, direita quase parada: o rover faz uma curva fechada para a direita

Como ler: o ponto azul é o dedo, a 50 para a direita e 40 para cima. As duas contas à direita viram a velocidade de cada roda, em porcentagem. Valores negativos giram a roda para trás; acima de 100 o código corta em 100.

Objetivo: dirigir o rover da Trilha 1 pelo celular, com joystick, WebSocket, parada ao desconectar e watchdog de 300 ms. Ligação no TB6612FNG: PWMA 25, AIN1 26, AIN2 27, PWMB 14, BIN1 16, BIN2 17, STBY ligado no 3V3 (o mesmo mapa de toda a Trilha 2; troque se o seu rover usa outros pinos). Precisa da biblioteca WebSockets (tópico 3).

rover-joystick.ino
// Rover com joystick no navegador: WebServer + WebSocket + watchdog (pacote 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 esquerdo
const int PWMB = 14, BIN1 = 16, BIN2 = 17;        // motor direito
const int VEL_MAX = 200;                          // teto de PWM (0 a 255): limite de segurança
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 recebe o PINO
}

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);  // mistura diferencial
      motor(PWMB, BIN1, BIN2, (y - x) * 255 / 100);
      ultimoComando = millis();                      // alimenta o watchdog
      andando = (x != 0 || y != 0);
    }
  } else if (tipo == WStype_DISCONNECTED) {
    parar();
    Serial.printf("Cliente %u saiu: PARADO\n", num);
  } else if (tipo == WStype_CONNECTED) {
    Serial.printf("Cliente %u conectou\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("Conecte em RoverINEMA e abra http://");
  Serial.println(WiFi.softAPIP());
}

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

Como verificar

  • ✓Com as rodas no ar, abra http://192.168.4.1: a página diz "conectado" e o Serial Monitor, "Cliente 0 conectou".
  • ✓Dedo para cima: as duas rodas para a frente. Dedo para a direita: as rodas giram em sentidos opostos. Soltar: param.
  • ✓Fechar a aba com o dedo para cima: "Cliente 0 saiu: PARADO". Modo avião: "watchdog: sem comando, PARADO".
  • ✗Uma roda gira ao contrário? Inverta os dois fios daquele motor no driver, não o código.

🌱 Novo aqui?

O texto entre R"html( e )html" é uma string crua do C++: tudo dentro dela, aspas e quebras de linha incluídas, vira texto sem precisar de barra invertida. É o jeito mais limpo de guardar uma página HTML inteira dentro do sketch.

Por que aprender

Com o rover dirigível pelo celular, você testa cada módulo seguinte com o robô no chão e com as mãos livres: leva até a pista, posiciona, solta. É também a base do modo "manual" que você vai querer em qualquer robô autônomo, para tirá-lo de um canto quando a autonomia errar.

Conceitos-chave

Mistura diferencial
y + x e y − x
Pointer events
dedo e mouse iguais
VEL_MAX
teto no robô
10 envios/s
alimenta o watchdog

✅ Resumo do módulo

✓
Estação ou ponto de acesso: entre na rede da casa ou crie a do robô; o AP é o mais previsível fora de casa.
✓
WebServer para botões: rotas simples, nenhum app para instalar.
✓
WebSocket para controle contínuo: um canal aberto, comandos com pouco atraso.
✓
BLE sem rede: serviço, característica e um app genérico.
✓
Silêncio = parar: watchdog de comando, parada ao desconectar e velocidade máxima no próprio robô.

Para ir além

Próximo módulo

2.2 · IMU e encoders: o robô passa a saber quanto girou, quanto andou e onde está.