iiOTESP32 Brasil
Modo de contingência: páginas públicas e downloads seguem disponíveis; login, comunidade e dados do servidor podem ficar indisponíveis.
Voltar para catálogo de tutoriais
Ilustração: placa ESP32 emitindo Wi-Fi para um celular com tela de configuração de rede, um roteador e outro celular mostrando a confirmação de conexão
Imagem ilustrativa gerada por IA
IntermediárioWebLeitura: 13 min

Como criar um portal Wi-Fi embarcado no ESP32

Portal que o técnico abre pelo celular para configurar o Wi-Fi sem cabo: varredura de redes, gravação na NVS e teste de conexão sem travar a resposta HTTP.

Tecnologias:#ESP32-S3#Arduino Core#WebServer#Preferences#NVS#HTML embarcado

Requisitos de hardware:

  • Placa ESP32 ou ESP32-S3 (referência: módulo N16R8)
  • Cabo USB com transmissão de dados
  • Um celular para abrir o portal
  • Rede Wi-Fi de 2,4 GHz

O problema que o portal resolve

Depois que o firmware está gravado, o dispositivo ainda não sabe em qual rede entrar. Recompilar com o SSID e a senha embutidos funciona na bancada e não funciona em campo: cada instalação teria um binário diferente, e o técnico precisaria de um notebook com o ambiente de compilação para trocar uma senha.

O portal embarcado inverte isso. A placa sobe uma rede própria, o técnico conecta o celular nela, abre uma página, escolhe a rede e digita a senha. As credenciais vão para a memória não volátil e sobrevivem a reinício e a nova gravação. Nenhum cabo, nenhum compilador.

Este tutorial documenta o portal do firmware AnalictY e é o complemento do tutorial de NAT: um configura o dispositivo, o outro roteia o tráfego dele.

Aqui o Arduino serve, e no NAT não

Vale explicar a escolha, porque parece incoerente à primeira vista.

O repetidor NAT precisa de ESP-IDF porque o NAPT depende de opções de compilação do lwIP que o Arduino Core não expõe. Já o portal de configuração não usa nada disso: ele precisa de Wi-Fi em modo ponto de acesso, um servidor HTTP e armazenamento persistente. Os três existem no Arduino Core, prontos e estáveis.

Usar ESP-IDF aqui só adicionaria complexidade sem ganho. A regra é escolher a camada pelo requisito, não por preferência.

WebServer síncrono, não assíncrono

Muitos tutoriais escolhem o ESPAsyncWebServer por reflexo. Este portal usa o WebServer que já vem no core:

code.cpp
cpp
1#include <WiFi.h>
2#include <WebServer.h>
3#include <Preferences.h>
4WebServer webServer(80);
5Preferences preferences;

O servidor síncrono atende uma requisição por vez, no laço principal. Para um portal que um técnico abre por dois minutos, isso é mais que suficiente — e evita uma dependência externa, que no ESP32 costuma trazer conflito de versão.

A regra prática: o assíncrono compensa quando há vários clientes simultâneos ou conexões longas, como WebSocket. Provisionamento não é esse caso.

Onde o HTML mora, e por que não é LittleFS

Outro reflexo comum é guardar a página em um sistema de arquivos. Este portal embute o HTML direto no binário, usando a macro F para manter o texto na flash em vez da RAM:

code.cpp
cpp
1page += F("<div class='api'>Celular no AP: http://192.168.4.1<br>");
2page += F("GET /api/status<br>GET /api/wifi/scan<br>");
3page += F("GET /api/config<br>POST /api/config</div>");

A vantagem é operacional: o firmware é um arquivo só. Não existe a etapa separada de subir a imagem do sistema de arquivos, nem o risco clássico de gravar o firmware novo e esquecer de atualizar a interface, deixando a página antiga servindo endpoints que já mudaram.

O custo é que alterar a interface exige recompilar. Para uma página de configuração que muda pouco, é uma troca vantajosa.

As rotas do portal

São nove rotas, cada uma com sua variante OPTIONS para responder ao CORS quando o portal é consumido por outra origem:

code.cpp
cpp
1GET / pagina do portal
2GET /api/status diagnostico em JSON
3GET /api/capabilities recursos suportados pela placa
4GET /api/wifi/scan redes encontradas na varredura
5GET /api/config configuracao atual
6POST /api/config grava SSID, senha e parametros
7GET /api/program programa do runtime
8POST /api/program grava o programa
9GET /api/runtime/status estado de execucao
10POST /api/reboot reinicia
11POST /api/reset-config apaga a configuracao da NVS

Separar leitura e escrita em métodos distintos da mesma rota mantém a API previsível: GET em `/api/config` devolve o que está gravado, e POST na mesma rota grava.

A varredura que não desiste na primeira tentativa

A varredura de Wi-Fi no ESP32 falha com frequência quando o rádio está ocupado, especialmente com o ponto de acesso ativo. Um scan que devolve erro faz o técnico concluir que a rede não existe, quando ela está ali.

Por isso a varredura tenta duas vezes, parando a anterior e limpando o resultado antes de repetir:

code.cpp
cpp
1int scanWifiNetworks() {
2 for (int attempt = 0; attempt < 2; attempt++) {
3 prepareWifiScanMode();
4 esp_wifi_scan_stop();
5 WiFi.scanDelete();
6 delay(attempt == 0 ? 120 : 450);
7 int count = WiFi.scanNetworks(false, true, false, 650);
8 if (count >= 0) {
9 return count;
10 }
11 WiFi.scanDelete();
12 WiFi.mode(apMode ? WIFI_AP_STA : WIFI_STA);
13 delay(350);
14 }
15 return -1;
16}

Os quatro argumentos importam. O primeiro false faz a varredura bloqueante, devolvendo o resultado pronto. O segundo true inclui redes ocultas. O terceiro false usa varredura ativa, que encontra mais redes que a passiva. E o 650 é o tempo em milissegundos por canal — abaixo disso, redes distantes somem do resultado.

A segunda tentativa espera mais que a primeira, 450 contra 120 milissegundos, dando tempo ao rádio de se reorganizar antes de repetir.

O detalhe que separa portal bom de portal ruim

Este é o ponto central do artigo.

A tentação é receber o formulário, conectar no Wi-Fi e só então responder ao navegador com o resultado. Não faça isso. Se a senha estiver errada, o ESP32 fica até vinte segundos tentando, o celular estoura o tempo de espera e a página morre sem resposta. O técnico conclui que não gravou, digita de novo e cai no mesmo lugar.

O correto é gravar, responder imediatamente e testar depois:

code.cpp
cpp
1void handleConfigPost() {
2 saveConfigFromRequest();
3 lastWifiStatus = WL_IDLE_STATUS;
4 lastWifiError = "Configuracao gravada. Teste Wi-Fi iniciado...";
5 wifiConnectTestPending = true;
6 String body;
7 body += F("<meta http-equiv='refresh' content='8;url=/'>");
8 body += F("<h1>Configuracao gravada</h1>");
9 webServer.send(200, "text/html", body);
10}

A gravação acontece, a resposta sai na hora, e a flag fica marcada. O teste de conexão roda no laço principal, fora do ciclo da requisição:

code.cpp
cpp
1if (wifiConnectTestPending) {
2 wifiConnectTestPending = false;
3 connectWiFiOnce(20000);
4}

E a página que o técnico recebe traz um meta refresh de 8 segundos apontando para a raiz. Ele não precisa fazer nada: a tela volta sozinha ao portal, agora mostrando se conectou ou por que falhou.

O ganho de percepção é enorme. Gravar sempre confirma na hora. O teste, que é demorado e pode falhar, acontece em separado.

Diagnóstico que diz o que fazer

Mensagem de erro genérica em provisionamento é inútil. O portal expõe o último estado em `/api/status`, com o motivo provável já traduzido:

code.cpp
cpp
1{
2 "wifi_connected": false,
3 "wifi_ssid": "MinhaRede-2.4G",
4 "last_wifi_status": "desconectado",
5 "last_wifi_error": "Falha: desconectado. Normalmente senha errada, WPA3-only, filtro MAC ou roteador recusando.",
6 "last_wifi_channel": 6,
7 "ap_active": true,
8 "ap_ip": "192.168.4.1"
9}

Repare no ap_active continuando verdadeiro depois da falha. É contrato do portal: o ponto de acesso não cai quando a conexão falha. Se caísse, o técnico ficaria sem caminho de volta e precisaria do cabo — exatamente o que o portal existe para evitar.

Quando conecta, o mesmo endpoint devolve o endereço obtido e a intensidade do sinal, o que ajuda a decidir o posicionamento da antena ainda em campo.

Tutorial: do zero ao Wi-Fi configurado pelo celular

Passo 1: preparar o ambiente

Instale o VS Code e a extensão PlatformIO IDE. Se nunca fez isso, o tutorial de primeiros passos com o PlatformIO cobre a instalação em detalhe.

Passo 2: configurar o platformio.ini

Este portal roda em Arduino Core. O arquivo mínimo para uma ESP32-S3 N16R8:

code.cpp
cpp
1[env:esp32-s3-n16r8]
2platform = espressif32
3board = esp32-s3-devkitc-1
4framework = arduino
5monitor_speed = 115200
6upload_speed = 921600
7board_build.arduino.memory_type = qio_opi
8build_flags =
9 -DBOARD_HAS_PSRAM
10lib_deps =
11 bblanchon/ArduinoJson@^7.2.1

O ArduinoJson monta as respostas dos endpoints. O WebServer e o Preferences já vêm no core, sem declaração.

Passo 3: gravar e abrir o monitor

Conecte a placa e grave. Na ESP32-S3 com duas portas USB, use a marcada UART:

code.cpp
cpp
1pio run -t upload
2pio device monitor -b 115200

No log de boot, procure a confirmação de que o ponto de acesso subiu e o servidor está escutando na porta 80.

Passo 4: conectar o celular

Abra a lista de redes Wi-Fi do celular. A rede da placa aparece com o nome definido no firmware. Conecte nela.

O celular vai avisar que essa rede não tem internet — é esperado, porque neste momento a placa ainda não está conectada a lugar nenhum. Escolha manter a conexão.

Passo 5: abrir o portal e escanear

No navegador do celular, abra:

code.cpp
cpp
1http://192.168.4.1

Toque em Scan. A varredura leva alguns segundos, e a lista traz cada rede com o sinal em dBm e o canal. Redes com sinal abaixo de -80 dBm tendem a conectar e cair; prefira as mais fortes.

Se a sua rede não aparecer nem na segunda tentativa, use o campo de SSID manual — redes ocultas exigem isso, e o campo existe para esse caso.

Passo 6: salvar e acompanhar

Escolha a rede, digite a senha e salve. A confirmação aparece na hora e a página volta sozinha ao portal em oito segundos.

Se conectou, o painel mostra o endereço que a placa recebeu na sua rede. Se falhou, mostra o motivo — e o ponto de acesso continua no ar para você corrigir sem cabo.

Lembre que o ESP32 não conecta em redes de 5 GHz. Se a sua tem sufixo 5G, use a de 2,4 GHz.

Passo 7: confirmar pela rede principal

Desconecte o celular do ponto de acesso e volte para a sua rede normal. Abra o endereço que o painel mostrou. Se o portal responder por ali, o provisionamento terminou: a placa está na sua rede e continua acessível.

Erros comuns e como resolver

A rede não aparece no scan. Confirme que é 2,4 GHz. Se for oculta, use o SSID manual. Se estiver longe, aproxime a placa e escaneie de novo — o tempo por canal favorece redes próximas.

Salvei e a página não respondeu. É o sintoma clássico de tentativa bloqueante. Confirme que o handler responde antes de testar a conexão, como mostrado acima.

Diz que gravou, mas não conecta. Veja o `last_wifi_error` em `/api/status`. Senha errada, WPA3-only e filtro de MAC no roteador dão o mesmo sintoma e mensagens diferentes.

O ponto de acesso some quando a conexão falha. Não deveria. Verifique se o código não está desligando o modo AP no tratamento de erro — sem ele o técnico perde o caminho de volta.

Perdi as credenciais e quero recomeçar. Chame o endpoint de reset da configuração, que apaga o que está na NVS e devolve a placa ao estado lógico de fábrica.

Ver mais tutoriais técnicosIIoT ESP32 Brasil — Documentação Prática