Provisionador Wi-Fi Para Projetos IoT Com ESP32

by PNAAT_FIT in Circuits > Microcontrollers

31 Views, 0 Favorites, 0 Comments

Provisionador Wi-Fi Para Projetos IoT Com ESP32

capa2.png

Configure dispositivos IoT sem teclado ou display usando Captive Portal, mDNS e armazenamento persistente de credenciais.


Link para o repositório do projeto: https://github.com/pnaat/lab-maker-assincrono/tree/main/Projetos/12-Provisionador-Wifi


Este projeto faz parte dos exemplos assíncronos disponibilizados pelo curso de IoT oferecido pelo PNAAT.


O PNAAT é o Programa Nacional de Aprendizado Acelerado em Tecnologia, uma iniciativa gratuita de capacitação em tecnologia nas áreas de computação, desenvolvimento embarcado (IoT) e inteligência artificial.

Este programa conecta você a capacitações técnicas, laboratórios de ponta e a indústrias nacionais e internacionais. Transforme suas ideias em soluções para desafios reais e acelere sua carreira em tecnologia!

Supplies

Hardware:

  1. Esp32-S3
  2. Celular ou Computador
  3. Rede Wi-Fi

Software

  1. VSCode
  2. ESP-IDF

Demonstração

GifBlink.gif
GifTelas.gif

Modo Configuração

O ESP32 cria a rede:

ESP32-Config

Ao conectar-se nela, o usuário é direcionado para uma página web onde informa:

  1. Nome da rede Wi-Fi (SSID)
  2. Senha

Modo Operacional

Após conectar ao roteador:

  1. Obtém IP via DHCP
  2. Disponibiliza acesso pelo IP local
  3. Disponibiliza acesso por:

http://esp32.local

  1. Permite ligar e desligar o LED embarcado
  2. Permite reconfigurar a rede a qualquer momento


Fluxograma do funcionamento

ESP32 Inicializa

↓

Cria rede ESP32-Config

↓

Usuário conecta na rede

↓

Captive Portal é exibido

↓

SSID e senha são informados

↓

Credenciais são salvas na NVS

↓

ESP32 conecta ao roteador

↓

Servidor Web permanece ativo

↓

LED e status podem ser controlados pelo navegador



Montagem Do Hardware

Para este projeto, basta conectar seu ESP32-S3 ao notebook/computador via cabo USB.

Estrutura Geral Do Programa

1 - Inicialização do Sistema

Configuração da pilha de rede, modo APSTA, armazenamento NVS (Non-Volatile Storage) e dos periféricos.

Linhas 34 a 139

/*
Configura o pino do LED utilizando a API gpio_config_t (recomendada no ESP-IDF moderno).
*/
static void init_led_gpio(void) {
gpio_config_t io_conf = {
.pin_bit_mask = (1ULL << LED_GPIO),
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE
};
gpio_config(&io_conf);
gpio_set_level(LED_GPIO, led_state);
}

/*
* @brief Inicializa o serviço mDNS para resolver domínios na rede local sem precisar saber o IP estático.
* Permite que clientes na mesma rede acessem o dispositivo via: http://esp32.local
*/
static void start_mdns_service(void) {
ESP_ERROR_CHECK(mdns_init());
ESP_ERROR_CHECK(mdns_hostname_set("esp32"));
ESP_ERROR_CHECK(mdns_instance_name_set("ESP32 Control Webserver"));
ESP_LOGI(TAG, "mDNS iniciado! Acesse na sua rede local por: http://esp32.local");
}

// ============================================================================
// --- Gerenciamento de Memória Não Volátil (NVS) ---
// ============================================================================

/*
* @brief Salva as credenciais da rede Wi-Fi na partição NVS para persistência pós-reboot.
*/
static esp_err_t save_wifi_credentials(const char *ssid, const char *password) {
nvs_handle_t nvs;
esp_err_t err = nvs_open("wifi_store", NVS_READWRITE, &nvs);
if (err != ESP_OK) return err;

err = nvs_set_str(nvs, "ssid", ssid);
if (err == ESP_OK) {
err = nvs_set_str(nvs, "password", password);
}
if (err == ESP_OK) {
err = nvs_commit(nvs); // Confirma a gravação na flash
}
nvs_close(nvs);
return err;
}

/*
* @brief Carrega as credenciais salvas na NVS, caso existam.
*/
static esp_err_t load_wifi_credentials(char *ssid, size_t ssid_len, char *password, size_t pass_len) {
nvs_handle_t nvs;
esp_err_t err = nvs_open("wifi_store", NVS_READONLY, &nvs);
if (err != ESP_OK) return err;

err = nvs_get_str(nvs, "ssid", ssid, &ssid_len);
if (err == ESP_OK) {
err = nvs_get_str(nvs, "password", password, &pass_len);
}
nvs_close(nvs);
return err;
}

// ============================================================================
// --- Event Loop & Configuração do Wi-Fi ---
// ============================================================================

/*
* @brief Callback de eventos de sistema (WIFI_EVENT e IP_EVENT).
* Trata o estado da conexão Station (STA) em segundo plano de forma assíncrona.
*/
static void wifi_event_handler(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) {
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
esp_wifi_connect();
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
is_connected = false;
ESP_LOGW(TAG, "Conexao com a rede local perdida. Tentando reconectar...");
esp_wifi_connect();
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
ESP_LOGI(TAG, "Conectado ao Wi-Fi! IP local obtido: " IPSTR, IP2STR(&event->ip_info.ip));
is_connected = true;
}
}

/*
* @brief Inicializa a pilha Wi-Fi no modo híbrido AP+STA (Access Point + Station).
* - O modo SoftAP provê o Captive Portal (192.168.4.1).
* - O modo STA conecta à rede Wi-Fi doméstica.
*/
static void wifi_init_apsta(void) {
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));

// Registro de handlers no Event Loop do ESP-IDF
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &wifi_event_handler, NULL));

ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));

// Configuração do Ponto de Acesso (SoftAP)
wifi_config_t ap_config = {
.ap = {
.ssid = EXAMPLE_ESP_WIFI_SSID,
.ssid_len = strlen(EXAMPLE_ESP_WIFI_SSID),
.password = EXAMPLE_ESP_WIFI_PASS,
.max_connection = EXAMPLE_MAX_STA_CONN,
.authmode = WIFI_AUTH_OPEN,
.channel = 1, // Canal fixo é crucial para estabilidade em modo APSTA
},
};
if (strlen(EXAMPLE_ESP_WIFI_PASS) > 0) {
ap_config.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
}

ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_AP, &ap_config));

// Configuração de IP Estático e Servidor DHCP para o SoftAP
esp_netif_t *ap_netif = esp_netif_get_handle_from_ifkey("WIFI_AP_DEF");
if (ap_netif) {
esp_netif_dhcps_stop(ap_netif);
esp_netif_ip_info_t ip_info;
IP4_ADDR(&ip_info.ip, 192, 168, 4, 1);
IP4_ADDR(&ip_info.gw, 192, 168, 4, 1);
IP4_ADDR(&ip_info.netmask, 255, 255, 255, 0);
ESP_ERROR_CHECK(esp_netif_set_ip_info(ap_netif, &ip_info));
esp_netif_dhcps_start(ap_netif);
}

// Tenta carregar credenciais previamente salvas para conectar como Station
char saved_ssid[32] = {0};
char saved_pass[64] = {0};
if (load_wifi_credentials(saved_ssid, sizeof(saved_ssid), saved_pass, sizeof(saved_pass)) == ESP_OK && strlen(saved_ssid) > 0) {
ESP_LOGI(TAG, "Credenciais encontradas na NVS! Tentando conectar em: %s", saved_ssid);
wifi_config_t sta_config = {0};
strncpy((char *)sta_config.sta.ssid, saved_ssid, sizeof(sta_config.sta.ssid));
strncpy((char *)sta_config.sta.password, saved_pass, sizeof(saved_pass));
ESP_ERROR_CHECK(esp_wifi_set_config(ESP_IF_WIFI_STA, &sta_config));
}

ESP_ERROR_CHECK(esp_wifi_start());
ESP_LOGI(TAG, "Wi-Fi APSTA iniciado. SSID do AP: '%s'", EXAMPLE_ESP_WIFI_SSID);
}

2 - Captive Portal

O Captive Portal funciona interceptando consultas DNS e redirecionando todas as páginas acessadas para o ESP32.

Linhas 413 a 415

// 5. Inicia o Servidor DNS que responde a qualquer consulta "*" apontando para 192.168.4.1
dns_server_config_t config = DNS_SERVER_CONFIG_SINGLE("*", "WIFI_AP_DEF");
start_dns_server(&config);

Linhas 335 a 384

/*
* @brief Handler de Erro 404 (Núcleo do Captive Portal).
* Captura qualquer requisição HTTP para domínios externos (ex: google.com, apple.com/generate_204)
* e responde com HTTP 302 Redirect para 192.168.4.1, forçando o SO móvel a abrir o portal.
*/
esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err) {
ESP_LOGI(TAG, "Tentativa de acesso detectada na URI: %s", req->uri);

// Resposta direcionada para testes do Captive Network Assistant (CNA) de iOS/Android
if (strstr(req->uri, "generate_204") || strstr(req->uri, "hotspot-detect") || strstr(req->uri, "library/test")) {
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
httpd_resp_send(req, "<HTML><HEAD><TITLE>Success</TITLE></HEAD><BODY>Success</BODY></HTML>", HTTPD_RESP_USE_STRLEN);
return ESP_OK;
}

// Redirecionamento genérico para chamadas fora do escopo
httpd_resp_set_status(req, "302 Found");
httpd_resp_set_hdr(req, "Location", "http://192.168.4.1/");
httpd_resp_sendstr(req, "Redirecting to Captive Portal...");
return ESP_OK;
}

/*
* @brief Inicializa e registra as rotas do servidor HTTP (esp_http_server).
*/
static httpd_handle_t start_webserver(void) {
httpd_handle_t server = NULL;
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.max_open_sockets = 9;
config.lru_purge_enable = true;
config.uri_match_fn = httpd_uri_match_wildcard;

if (httpd_start(&server, &config) == ESP_OK) {
httpd_uri_t root_uri = { .uri = "/", .method = HTTP_GET, .handler = root_get_handler };
httpd_uri_t status_uri = { .uri = "/status", .method = HTTP_GET, .handler = status_get_handler };
httpd_uri_t connect_uri = { .uri = "/connect", .method = HTTP_POST, .handler = connect_post_handler };
httpd_uri_t led_uri = { .uri = "/toggle-led", .method = HTTP_POST, .handler = led_post_handler };

httpd_register_uri_handler(server, &root_uri);
httpd_register_uri_handler(server, &status_uri);
httpd_register_uri_handler(server, &connect_uri);
httpd_register_uri_handler(server, &led_uri);
// Registra o interceptador de erro 404 para criar a redireção do Captive Portal
httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, http_404_error_handler);
}
return server;
}

3 - Recebimento dads credenciais pela interface web

Quando o usuário envia o formulário de configuração, o ESP32 recebe os dados, salva na NVS e inicia uma nova conexão Wi-Fi.

Linhas 280 a 333

/*
* @brief POST /connect: Processa o formulário HTTP enviado pelo usuário,
* extrai o SSID e senha, salva na NVS e dispara a reconexão como Station.
*/
static esp_err_t connect_post_handler(httpd_req_t *req) {
char buf[128];
int ret, remaining = req->content_len;

if (remaining >= sizeof(buf)) {
httpd_resp_send_500(req);
return ESP_FAIL;
}

ret = httpd_req_recv(req, buf, remaining);
if (ret <= 0) return ESP_FAIL;
buf[ret] = '\0';

char raw_ssid[32] = {0};
char raw_password[64] = {0};
char ssid[32] = {0};
char password[64] = {0};

// Extrai os valores do corpo da requisição POST
httpd_query_key_value(buf, "ssid", raw_ssid, sizeof(raw_ssid));
httpd_query_key_value(buf, "password", raw_password, sizeof(raw_password));

// DECODIFICA OS CARACTERES ESPECIAIS (como o @)
url_decode(ssid, raw_ssid);
url_decode(password, raw_password);

ESP_LOGI(TAG, "Recebido via POST -> SSID decodificado: %s", ssid);

if (strlen(ssid) > 0) {
// Salva a senha correta (já limpa e decodificada) na NVS
save_wifi_credentials(ssid, password);

wifi_config_t sta_config = {0};
strncpy((char *)sta_config.sta.ssid, ssid, sizeof(sta_config.sta.ssid));
strncpy((char *)sta_config.sta.password, password, sizeof(sta_config.sta.password));

esp_wifi_disconnect();
esp_wifi_set_config(ESP_IF_WIFI_STA, &sta_config);
esp_wifi_connect();

httpd_resp_set_hdr(req, "Connection", "close");
httpd_resp_sendstr(req, "<html><head><meta http-equiv='refresh' content='3;url=/'></head>"
"<body><h1>Credenciais Salvas!</h1><p>O ESP32 esta conectando...</p></body></html>");
} else {
httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "SSID invalido");
}

return ESP_OK;
}

Configuração Do Ambiente

yml.png

1 - Instalar a dependência mDNS

Execute no terminal da esp.idf:

idf.py add-dependency "espressif/mdns^1.4.0"

Verifique se foi criado o arquivo na arvore de trabalho:

main -> cMakeLists.txt -> idf_component.yml


2 - Compile, grave e execute o projeto

Após a inicialização:

No buscador de redes wifi de seu smartphone ou computador procure a rede:

ESP32-Config

Conecte-se à rede.

Caso não ocorra redirecionamento automático, abra:

http://esp32.local

ou

http://192.168.4.1

A tela de configuração Wi-Fi deverá aparecer.

Conclusão E Proximos Passos

No projeto atual é possível realizar a conexão do ESP32 na sua rede Wi-fi via o provisionador. Após a conexão, é possivel controlar o led builtin.

Como proximos passos, pode ser interessante expandir as funcionalidades das tasks para realizar leitura de sensores, ligar e desligar relés de comando, ou até mesmo realizar uma automação residencial simples.