wifi: portal Wi-Fi top com scan, clear_wifi MQTT e fluxo limpo STA/AP

This commit is contained in:
XupaMisto 2025-12-27 22:39:39 +00:00
parent 4d28d620b6
commit 0d828caae5
6 changed files with 495 additions and 184 deletions

View File

@ -1,60 +1,101 @@
#include <string.h> #include <string.h>
#include "lwip/err.h" #include <stdbool.h>
#include "lwip/sockets.h" #include "lwip/sockets.h"
#include "lwip/inet.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h" #include "esp_log.h"
#include "dns_server.h" #include "dns_server.h"
static const char *TAG = "DNS"; static const char *TAG = "DNS";
#define DNS_PORT 53 #define DNS_PORT 53
void start_dns_server(void) { static TaskHandle_t s_dns_task = NULL;
static volatile bool s_dns_running = false;
static void dns_task(void *arg)
{
(void)arg;
int sock = socket(AF_INET, SOCK_DGRAM, 0); int sock = socket(AF_INET, SOCK_DGRAM, 0);
if (sock < 0) { if (sock < 0) {
ESP_LOGE(TAG, "❌ Falha ao criar socket DNS"); ESP_LOGE(TAG, "❌ socket DNS falhou");
s_dns_running = false;
s_dns_task = NULL;
vTaskDelete(NULL);
return; return;
} }
struct sockaddr_in addr; int yes = 1;
setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET; addr.sin_family = AF_INET;
addr.sin_port = htons(DNS_PORT); addr.sin_port = htons(DNS_PORT);
addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) { if (bind(sock, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
ESP_LOGE(TAG, "Falha ao bind socket DNS"); ESP_LOGE(TAG, "bind DNS falhou (porta %d)", DNS_PORT);
close(sock); close(sock);
s_dns_running = false;
s_dns_task = NULL;
vTaskDelete(NULL);
return; return;
} }
ESP_LOGI(TAG, "✅ DNS captive portal iniciado na porta %d", DNS_PORT); ESP_LOGI(TAG, "✅ DNS captive a correr na porta %d (task)", DNS_PORT);
while (1) { while (s_dns_running) {
char buf[512]; uint8_t buf[512];
struct sockaddr_in source_addr; struct sockaddr_in source_addr = {0};
socklen_t socklen = sizeof(source_addr); socklen_t slen = sizeof(source_addr);
int len = recvfrom(sock, buf, sizeof(buf), 0, int len = recvfrom(sock, buf, sizeof(buf), 0,
(struct sockaddr*)&source_addr, &socklen); (struct sockaddr*)&source_addr, &slen);
if (len <= 0) {
vTaskDelay(pdMS_TO_TICKS(10));
continue;
}
if (len > 0) {
// resposta simples: sempre devolve 192.168.4.1
buf[2] |= 0x80; // resposta buf[2] |= 0x80; // resposta
buf[3] |= 0x80; // autoritativo buf[3] |= 0x80; // RA
buf[7] = 1; // 1 resposta buf[6] = 0x00;
buf[7] = 0x01; // ANCOUNT=1
// append resposta A 192.168.4.1 if (len + 16 < (int)sizeof(buf)) {
buf[len++] = 0xC0; buf[len++] = 0x0C; buf[len++] = 0xC0; buf[len++] = 0x0C;
buf[len++] = 0x00; buf[len++] = 0x01; // type A buf[len++] = 0x00; buf[len++] = 0x01;
buf[len++] = 0x00; buf[len++] = 0x01; // class IN buf[len++] = 0x00; buf[len++] = 0x01;
buf[len++] = 0x00; buf[len++] = 0x00; buf[len++] = 0x00; buf[len++] = 0x00;
buf[len++] = 0x00; buf[len++] = 0x3C; // TTL 60s buf[len++] = 0x00; buf[len++] = 0x3C;
buf[len++] = 0x00; buf[len++] = 0x04; // data length buf[len++] = 0x00; buf[len++] = 0x04;
buf[len++] = 192; buf[len++] = 168; buf[len++] = 192; buf[len++] = 168;
buf[len++] = 4; buf[len++] = 1; buf[len++] = 4; buf[len++] = 1;
sendto(sock, buf, len, 0, sendto(sock, buf, len, 0, (struct sockaddr*)&source_addr, slen);
(struct sockaddr*)&source_addr, socklen);
} }
} }
close(sock);
s_dns_task = NULL;
ESP_LOGI(TAG, "DNS task terminou");
vTaskDelete(NULL);
}
void start_dns_server(void)
{
if (s_dns_task) {
ESP_LOGW(TAG, "DNS já está a correr");
return;
}
s_dns_running = true;
xTaskCreate(dns_task, "dns_task", 4096, NULL, 4, &s_dns_task);
ESP_LOGI(TAG, "✅ DNS captive portal iniciado na porta %d", DNS_PORT);
}
void stop_dns_server(void)
{
s_dns_running = false;
} }

View File

@ -4,6 +4,7 @@ extern "C" {
#endif #endif
void start_dns_server(void); void start_dns_server(void);
void stop_dns_server(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@ -1,33 +1,14 @@
#pragma once #pragma once
#include <stdbool.h>
#include "esp_err.h"
#include "esp_wifi_types.h"
#include "esp_event.h"
#ifdef __cplusplus #ifdef __cplusplus
extern "C" { extern "C" {
#endif #endif
/**
* @brief Callback chamado quando o Wi-Fi se conecta e obtém IP.
* (usado para iniciar MQTT, LEDs, etc.)
*/
typedef void (*wifi_connected_cb_t)(void); typedef void (*wifi_connected_cb_t)(void);
/**
* @brief Inicializa o módulo Wi-Fi com modo STA ou AP de configuração.
*
* @param cb Função callback chamada quando ligação Wi-Fi com IP.
* @param have_creds true se tiver credenciais gravadas.
*/
void wifi_config_portal_init(wifi_connected_cb_t cb, bool have_creds); void wifi_config_portal_init(wifi_connected_cb_t cb, bool have_creds);
void wifi_clear_creds(void);
/**
* @brief Funções auxiliares do portal cativo (DNS + HTTP)
* São definidas em dns_server.c
*/
void start_dns_server(void);
void stop_dns_server(void);
#ifdef __cplusplus #ifdef __cplusplus
} }

View File

@ -2,6 +2,7 @@
#include <string.h> #include <string.h>
#include <stdint.h> #include <stdint.h>
#include <stdbool.h> #include <stdbool.h>
#include "esp_wifi.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_system.h" #include "esp_system.h"
@ -19,6 +20,7 @@
#include "ui.h" #include "ui.h"
#include "display.h" #include "display.h"
#include "wifi_config_portal.h"
static const char *TAG = "MQTT_CMD"; static const char *TAG = "MQTT_CMD";
@ -90,6 +92,36 @@ void mqtt_comandos_handle(cJSON *root)
vTaskDelay(pdMS_TO_TICKS(200)); vTaskDelay(pdMS_TO_TICKS(200));
esp_restart(); esp_restart();
} }
// --------------------------------------------------
else if (strcmp(c, "CLEAR_WIFI") == 0) {
// confirmação OBRIGATÓRIA
cJSON *conf = cJSON_GetObjectItem(root, "confirm");
if (!cJSON_IsNumber(conf) || conf->valueint != 1) {
ESP_LOGW(TAG, "CLEAR_WIFI recusado: confirm!=1");
return;
}
ESP_LOGW(TAG, "🧹 CLEAR_WIFI -> apagar credenciais + reset WiFi interno + reboot");
// 1) apaga as tuas credenciais (namespace "wifi")
wifi_clear_creds();
// 2) apaga credenciais/config interna guardada pelo driver WiFi (nvs.net80211)
esp_err_t err = esp_wifi_restore();
ESP_LOGW(TAG, "esp_wifi_restore -> %s", esp_err_to_name(err));
// 3) parar WiFi (opcional, mas deixa tudo limpinho)
esp_wifi_disconnect();
esp_wifi_stop();
// responde ao servidor (opcional)
esp_mqtt_client_publish(mqtt_client, topic_resp,
"{\"wifi\":\"cleared\",\"restore\":1,\"reboot\":1}", 0, 1, false);
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
}
// -------------------------------------------------- // --------------------------------------------------
else if (strcmp(c, "STATUS") == 0) { else if (strcmp(c, "STATUS") == 0) {

View File

@ -42,6 +42,7 @@
{ "cmd": "DISP_TEST" } { "cmd": "DISP_TEST" }
{ "cmd": "LED_TEST" } { "cmd": "LED_TEST" }
{ "cmd": "SET_PERC", "val": 70 } { "cmd": "SET_PERC", "val": 70 }
{ "cmd": "CLEAR_WIFI","confirm": 1}
esp/esp_BBC9A4/cmd // onde envias os comandos esp/esp_BBC9A4/cmd // onde envias os comandos

View File

@ -1,252 +1,507 @@
#include "wifi_config_portal.h" #include "wifi_config_portal.h"
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
#include <stdbool.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "esp_log.h" #include "esp_log.h"
#include "esp_event.h" #include "esp_event.h"
#include "esp_netif.h" #include "esp_netif.h"
#include "nvs_flash.h"
#include "esp_timer.h" #include "esp_timer.h"
#include "esp_http_server.h" #include "esp_http_server.h"
#include <string.h> #include "esp_system.h"
#include <stdio.h> #include "esp_err.h"
#include <stdlib.h>
#include "nvs_flash.h"
#include "nvs.h"
#include "dns_server.h" #include "dns_server.h"
#ifndef MIN #ifndef MIN
#define MIN(a,b) ((a) < (b) ? (a) : (b)) #define MIN(a,b) ((a) < (b) ? (a) : (b))
#endif #endif
// ===== Ajustes =====
#define FORCE_PORTAL_DEFAULT 0 // 1 = força sempre portal
#define PORTAL_AP_PASS "12345678"
// ===================
static const char *TAG = "WIFI_PORTAL"; static const char *TAG = "WIFI_PORTAL";
static httpd_handle_t server = NULL; static httpd_handle_t server = NULL;
static wifi_connected_cb_t g_on_connected = NULL; static wifi_connected_cb_t g_on_connected = NULL;
static esp_timer_handle_t wifi_watchdog_timer = NULL; static esp_timer_handle_t wifi_watchdog_timer = NULL;
static bool got_ip = false; static bool got_ip = false;
// --- WATCHDOG: tenta reconectar se ficar 60 s sem IP --- // ✅ Flag: quando true estamos em portal (APSTA só para scan), NÃO conectar
static void wifi_watchdog_cb(void *arg) { static bool g_portal_mode = false;
/* ========================= NVS init seguro ========================= */
static void ensure_nvs_ready(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_LOGW(TAG, "NVS (%s) -> apagar e reiniciar", esp_err_to_name(err));
ESP_ERROR_CHECK(nvs_flash_erase());
ESP_ERROR_CHECK(nvs_flash_init());
} else if (err != ESP_OK) {
ESP_LOGE(TAG, "nvs_flash_init falhou: %s", esp_err_to_name(err));
}
}
/* ========================= URL decode ========================= */
static void url_decode(char *dst, const char *src)
{
char a, b;
while (*src) {
if (*src == '%' &&
(a = src[1]) && (b = src[2]) &&
isxdigit((unsigned char)a) && isxdigit((unsigned char)b)) {
if (a >= 'a') a -= 'a' - 'A';
if (a >= 'A') a -= ('A' - 10);
else a -= '0';
if (b >= 'a') b -= 'a' - 'A';
if (b >= 'A') b -= ('A' - 10);
else b -= '0';
*dst++ = 16 * a + b;
src += 3;
} else if (*src == '+') {
*dst++ = ' ';
src++;
} else {
*dst++ = *src++;
}
}
*dst = '\0';
}
/* ========================= Watchdog STA ========================= */
static void wifi_watchdog_cb(void *arg)
{
(void)arg;
if (!got_ip) { if (!got_ip) {
ESP_LOGW(TAG, "⏱️ 60 s sem IP — a tentar reconectar Wi-Fi..."); ESP_LOGW(TAG, "⏱️ 60 s sem IP — reconectar...");
esp_wifi_disconnect(); esp_wifi_disconnect();
vTaskDelay(pdMS_TO_TICKS(1000)); // espera 1 s vTaskDelay(pdMS_TO_TICKS(1000));
esp_wifi_connect(); esp_wifi_connect();
} }
} }
// --- EVENTOS WIFI --- /* ========================= Eventos WiFi ========================= */
static void on_wifi_event(void *arg, esp_event_base_t event_base, static void on_wifi_event(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) { int32_t event_id, void *event_data)
{
(void)arg;
if (event_base == WIFI_EVENT) { if (event_base == WIFI_EVENT) {
switch (event_id) { switch (event_id) {
case WIFI_EVENT_STA_START: case WIFI_EVENT_STA_START:
ESP_LOGI(TAG, "📡 STA start"); ESP_LOGI(TAG, "📡 STA start");
// ✅ Em portal mode: NÃO conectar, só scan
if (!g_portal_mode) {
esp_wifi_connect(); esp_wifi_connect();
} else {
ESP_LOGI(TAG, "🛑 Portal mode: não faço connect, só scan.");
}
break; break;
case WIFI_EVENT_STA_CONNECTED: case WIFI_EVENT_STA_CONNECTED:
ESP_LOGI(TAG, "🔗 STA conectado ao AP, aguardando IP..."); ESP_LOGI(TAG, "🔗 STA conectado, esperando IP...");
break; break;
case WIFI_EVENT_STA_DISCONNECTED: { case WIFI_EVENT_STA_DISCONNECTED: {
wifi_event_sta_disconnected_t *disconn = (wifi_event_sta_disconnected_t *) event_data; wifi_event_sta_disconnected_t *d =
ESP_LOGW(TAG, "⚠️ STA desconectado (motivo %d)", disconn->reason); (wifi_event_sta_disconnected_t *)event_data;
ESP_LOGW(TAG, "⚠️ STA desconectado (motivo %d)", d ? d->reason : -1);
got_ip = false; got_ip = false;
// ✅ só tenta reconectar no modo normal, não no portal
if (!g_portal_mode) {
esp_wifi_connect(); esp_wifi_connect();
}
break; break;
} }
default: default:
ESP_LOGI(TAG, " Evento Wi-Fi não tratado: %ld", (long)event_id);
break; break;
} }
} }
} }
// --- Task auxiliar para iniciar o app (MQTT, LEDs, etc.) --- static void wifi_start_app_task(void *arg)
static void wifi_start_app_task(void *arg) { {
if (g_on_connected) (void)arg;
g_on_connected(); // chamada do callback da aplicação if (g_on_connected) g_on_connected();
vTaskDelete(NULL); vTaskDelete(NULL);
} }
// --- EVENTO IP OBTIDO ---
static void on_got_ip(void *arg, esp_event_base_t event_base, static void on_got_ip(void *arg, esp_event_base_t event_base,
int32_t event_id, void *event_data) { int32_t event_id, void *event_data)
{
(void)arg; (void)event_base; (void)event_id; (void)event_data;
got_ip = true; got_ip = true;
ESP_LOGI(TAG, "🌐 STA obteve IP!"); ESP_LOGI(TAG, "🌐 IP obtido");
if (wifi_watchdog_timer) if (wifi_watchdog_timer) esp_timer_stop(wifi_watchdog_timer);
esp_timer_stop(wifi_watchdog_timer);
// 🧠 executa o callback noutra task (stack própria)
xTaskCreate(wifi_start_app_task, "wifi_start_app", 6144, NULL, 5, NULL); xTaskCreate(wifi_start_app_task, "wifi_start_app", 6144, NULL, 5, NULL);
} }
// --- NVS: guardar e ler credenciais --- /* ========================= NVS Credenciais (teu namespace) ========================= */
static bool wifi_load_creds(char *ssid, char *pass) { static bool wifi_load_creds(char *ssid, char *pass)
{
nvs_handle_t nvs; nvs_handle_t nvs;
if (nvs_open("wifi", NVS_READONLY, &nvs) != ESP_OK) return false; if (nvs_open("wifi", NVS_READONLY, &nvs) != ESP_OK) return false;
size_t len1 = 32, len2 = 64;
if (nvs_get_str(nvs, "ssid", ssid, &len1) != ESP_OK) { nvs_close(nvs); return false; } size_t ssid_len = 32, pass_len = 64;
if (nvs_get_str(nvs, "pass", pass, &len2) != ESP_OK) { nvs_close(nvs); return false; } esp_err_t e1 = nvs_get_str(nvs, "ssid", ssid, &ssid_len);
esp_err_t e2 = nvs_get_str(nvs, "pass", pass, &pass_len);
nvs_close(nvs); nvs_close(nvs);
if (e1 != ESP_OK || e2 != ESP_OK) return false;
if (ssid[0] == '\0') return false;
return true; return true;
} }
static void wifi_save_creds(const char *ssid, const char *pass) { static void wifi_save_creds(const char *ssid, const char *pass)
{
nvs_handle_t nvs; nvs_handle_t nvs;
if (nvs_open("wifi", NVS_READWRITE, &nvs) != ESP_OK) return; if (nvs_open("wifi", NVS_READWRITE, &nvs) != ESP_OK) return;
nvs_set_str(nvs, "ssid", ssid); nvs_set_str(nvs, "ssid", ssid);
nvs_set_str(nvs, "pass", pass); nvs_set_str(nvs, "pass", pass);
nvs_commit(nvs); nvs_commit(nvs);
nvs_close(nvs); nvs_close(nvs);
} }
// --- Handlers HTTP --- // ✅ Exportável: apaga as tuas credenciais (namespace "wifi")
void wifi_clear_creds(void)
{
nvs_handle_t nvs;
esp_err_t err = nvs_open("wifi", NVS_READWRITE, &nvs);
if (err != ESP_OK) {
ESP_LOGE(TAG, "❌ NVS open falhou: %s", esp_err_to_name(err));
return;
}
nvs_erase_key(nvs, "ssid");
nvs_erase_key(nvs, "pass");
nvs_commit(nvs);
nvs_close(nvs);
ESP_LOGW(TAG, "🧹 Credenciais Wi-Fi apagadas (namespace wifi)");
}
/* ========================= SCAN SSID (APSTA) ========================= */
#define MAX_APS 20
static wifi_ap_record_t g_aps[MAX_APS];
static uint16_t g_ap_count = 0;
static void html_escape(char *dst, size_t dstlen, const char *src)
{
size_t o = 0;
for (size_t i = 0; src[i] && o + 6 < dstlen; i++) {
unsigned char c = (unsigned char)src[i];
if (c == '&') { memcpy(dst+o, "&amp;", 5); o += 5; }
else if (c == '<') { memcpy(dst+o, "&lt;", 4); o += 4; }
else if (c == '>') { memcpy(dst+o, "&gt;", 4); o += 4; }
else if (c < 32) { /* ignora */ }
else { dst[o++] = (char)c; }
}
dst[o] = 0;
}
static int wifi_scan_try(void)
{
g_ap_count = 0;
ESP_LOGI(TAG, "📶 Scan: a iniciar...");
wifi_scan_config_t sc = {
.ssid = 0,
.bssid = 0,
.channel = 0,
.show_hidden = true
};
esp_err_t err = esp_wifi_scan_start(&sc, true); // bloqueante
if (err != ESP_OK) {
ESP_LOGW(TAG, "📶 Scan: scan_start falhou: %s", esp_err_to_name(err));
return 0;
}
uint16_t n = MAX_APS;
err = esp_wifi_scan_get_ap_records(&n, g_aps);
if (err != ESP_OK) {
ESP_LOGW(TAG, "📶 Scan: get_ap_records falhou: %s", esp_err_to_name(err));
return 0;
}
g_ap_count = n;
ESP_LOGI(TAG, "📶 Scan: encontrei %d redes", (int)n);
if (n > 0) ESP_LOGI(TAG, "📶 Exemplo SSID: %s", (char*)g_aps[0].ssid);
return (int)n;
}
/* ========================= HTTP handlers ========================= */
static esp_err_t handle_root(httpd_req_t *req) static esp_err_t handle_root(httpd_req_t *req)
{ {
const char *html = ESP_LOGI(TAG, "📥 GET %s", req->uri);
// tenta scan (se falhar, continua com input manual)
int n = wifi_scan_try();
char *html = malloc(9000);
if (!html) return httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "Sem memória");
int o = 0;
o += snprintf(html+o, 9000-o,
"<!DOCTYPE html><html lang='pt'><head>" "<!DOCTYPE html><html lang='pt'><head>"
"<meta charset='UTF-8'>" "<meta charset='UTF-8'>"
"<title>Configuração Wi-Fi</title>" "<title>Configuração Wi-Fi</title>"
"<style>" "<style>"
"body{font-family:Arial,sans-serif;background:#eef2f3;text-align:center;}" "body{font-family:Arial;background:#eef2f3;text-align:center;}"
"form{background:#fff;padding:20px;margin:50px auto;width:360px;" "form{background:#fff;padding:20px;margin:50px auto;width:360px;"
"border-radius:10px;box-shadow:0 0 10px rgba(0,0,0,0.2);}" "border-radius:10px;box-shadow:0 0 10px rgba(0,0,0,.2);}"
"input{width:90%;padding:10px;margin:10px;font-size:16px;}" "input,select{width:90%%;padding:10px;margin:10px;font-size:16px;}"
"button{background:#007bff;color:#fff;border:none;padding:10px 20px;" "button{background:#007bff;color:#fff;border:none;padding:10px 20px;"
"border-radius:6px;font-size:16px;cursor:pointer;}" "border-radius:6px;font-size:16px;}"
"button:hover{background:#0056b3;}" ".mini{font-size:12px;color:#555;}"
"</style></head><body>" "</style></head><body>"
"<h2>Configuração Wi-Fi</h2>" "<h2>Configuração Wi-Fi</h2>"
"<form id='wifiForm' action='/save' method='POST'>" "<form id='wifiForm' action='/save' method='POST'>"
"SSID:<br><input name='ssid' maxlength='31' required><br>" );
"Senha:<br><input name='pass' type='password' maxlength='63' required><br><br>"
if (n > 0) {
o += snprintf(html+o, 9000-o,
"<div class='mini'>Redes encontradas (scan):</div>"
"<select id='ssid_sel'>"
"<option value=''>-- selecionar --</option>"
);
for (int i = 0; i < n && o < 8300; i++) {
if (!g_aps[i].ssid[0]) continue;
char esc[80];
html_escape(esc, sizeof(esc), (const char*)g_aps[i].ssid);
const char *lock = (g_aps[i].authmode == WIFI_AUTH_OPEN) ? "🔓" : "🔒";
o += snprintf(html+o, 9000-o,
"<option value='%s'>%s %s (RSSI %d)</option>",
esc, lock, esc, g_aps[i].rssi
);
}
o += snprintf(html+o, 9000-o, "</select>");
} else {
o += snprintf(html+o, 9000-o,
"<div class='mini'>Scan indisponível. Escreve o SSID manualmente.</div>"
);
}
o += snprintf(html+o, 9000-o,
"SSID:<br><input id='ssid' name='ssid' maxlength='31' required><br>"
"Senha:<br><input name='pass' type='password' maxlength='63' required><br>"
"<button type='submit'>Guardar</button>" "<button type='submit'>Guardar</button>"
"</form>" "</form>"
"<script>" "<script>"
"var sel=document.getElementById('ssid_sel');"
"if(sel){sel.addEventListener('change',function(){"
" if(this.value) document.getElementById('ssid').value=this.value;"
"});}"
"document.getElementById('wifiForm').onsubmit=function(e){" "document.getElementById('wifiForm').onsubmit=function(e){"
"e.preventDefault();" "e.preventDefault();"
"fetch('/save',{method:'POST',body:new URLSearchParams(new FormData(this))})" "fetch('/save',{method:'POST',body:new URLSearchParams(new FormData(this))})"
".then(()=>alert('Credenciais enviadas! Reinicie o dispositivo.'));" ".then(()=>alert('Credenciais enviadas!'));"
"};" "};"
"</script>" "</script></body></html>"
"</body></html>"; );
httpd_resp_set_type(req, "text/html; charset=UTF-8"); httpd_resp_set_type(req, "text/html; charset=UTF-8");
httpd_resp_set_hdr(req, "Cache-Control", "no-store, no-cache, must-revalidate"); esp_err_t r = httpd_resp_send(req, html, HTTPD_RESP_USE_STRLEN);
return httpd_resp_send(req, html, HTTPD_RESP_USE_STRLEN); free(html);
return r;
} }
static esp_err_t handle_save(httpd_req_t *req) static esp_err_t handle_save(httpd_req_t *req)
{ {
char buf[128]; ESP_LOGI(TAG, "📥 POST %s len=%d", req->uri, (int)req->content_len);
int ret = httpd_req_recv(req, buf, MIN(req->content_len, sizeof(buf) - 1));
if (ret <= 0) {
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Nada recebido");
}
buf[ret] = '\0';
int total = req->content_len;
int cur = 0;
char buf[256];
if (total <= 0 || total >= (int)sizeof(buf)) {
return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Body inválido/grande");
}
while (cur < total) {
int r = httpd_req_recv(req, buf + cur, total - cur);
if (r <= 0) return httpd_resp_send_err(req, HTTPD_400_BAD_REQUEST, "Falha recv");
cur += r;
}
buf[cur] = 0;
char ssid_enc[32] = {0}, pass_enc[64] = {0};
char ssid[32] = {0}, pass[64] = {0}; char ssid[32] = {0}, pass[64] = {0};
sscanf(buf, "ssid=%31[^&]&pass=%63s", ssid, pass);
sscanf(buf, "ssid=%31[^&]&pass=%63s", ssid_enc, pass_enc);
url_decode(ssid, ssid_enc);
url_decode(pass, pass_enc);
wifi_save_creds(ssid, pass); wifi_save_creds(ssid, pass);
ESP_LOGI(TAG, "💾 Credenciais salvas: SSID=%s", ssid); ESP_LOGI(TAG, "💾 Guardado SSID=%s", ssid);
const char *resp = const char *resp =
"<!DOCTYPE html><html lang='pt'><meta charset='UTF-8'>" "<html><body style='text-align:center;margin-top:50px;'>"
"<body style='font-family:sans-serif;text-align:center;margin-top:50px;'>" "<h2>✅ Credenciais guardadas</h2>"
"<h2>✅ Credenciais guardadas!</h2>" "<p>Reiniciando...</p></body></html>";
"<p>O dispositivo vai reiniciar em 2 segundos...</p>"
"</body></html>";
httpd_resp_set_type(req, "text/html; charset=UTF-8"); httpd_resp_set_type(req, "text/html; charset=UTF-8");
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
vTaskDelay(pdMS_TO_TICKS(2000)); vTaskDelay(pdMS_TO_TICKS(1200));
esp_restart(); esp_restart();
return ESP_OK; return ESP_OK;
} }
// --- Inicia o servidor HTTP --- static esp_err_t handle_captive(httpd_req_t *req)
static void start_webserver(void) { {
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); ESP_LOGI(TAG, "📥 GET %s -> redirect /", req->uri);
config.stack_size = 8192; httpd_resp_set_status(req, "302 Found");
config.server_port = 80; httpd_resp_set_hdr(req, "Location", "/");
config.max_uri_handlers = 8; return httpd_resp_send(req, NULL, 0);
config.uri_match_fn = httpd_uri_match_wildcard; }
config.recv_wait_timeout = 10;
config.send_wait_timeout = 10; /* ========================= Webserver ========================= */
config.max_resp_headers = 20; static void start_webserver(void)
{
ESP_LOGI(TAG, "🌍 start_webserver() chamado");
if (server) {
ESP_LOGW(TAG, "⚠️ HTTP server já existia, a parar...");
httpd_stop(server);
server = NULL;
}
httpd_config_t cfg = HTTPD_DEFAULT_CONFIG();
cfg.stack_size = 8192;
cfg.server_port = 80;
cfg.uri_match_fn = httpd_uri_match_wildcard;
esp_err_t err = httpd_start(&server, &cfg);
ESP_LOGI(TAG, "httpd_start -> %s", esp_err_to_name(err));
if (err != ESP_OK) {
ESP_LOGE(TAG, "❌ HTTP server não arrancou");
return;
}
if (httpd_start(&server, &config) == ESP_OK) {
httpd_uri_t root = { .uri="/", .method=HTTP_GET, .handler=handle_root }; httpd_uri_t root = { .uri="/", .method=HTTP_GET, .handler=handle_root };
httpd_uri_t save = { .uri="/save", .method=HTTP_POST, .handler=handle_save }; httpd_uri_t save = { .uri="/save", .method=HTTP_POST, .handler=handle_save };
httpd_register_uri_handler(server, &root); httpd_uri_t captive = { .uri="/*", .method=HTTP_GET, .handler=handle_captive };
httpd_register_uri_handler(server, &save);
ESP_LOGI(TAG, "🌐 Servidor HTTP iniciado na porta %d", config.server_port); ESP_LOGI(TAG, "register / -> %s", esp_err_to_name(httpd_register_uri_handler(server, &root)));
} else { ESP_LOGI(TAG, "register /save -> %s", esp_err_to_name(httpd_register_uri_handler(server, &save)));
ESP_LOGE(TAG, "❌ Falha ao iniciar servidor HTTP!"); ESP_LOGI(TAG, "register /* -> %s", esp_err_to_name(httpd_register_uri_handler(server, &captive)));
}
} }
// --- Inicialização principal --- /* ========================= API ========================= */
void wifi_config_portal_init(wifi_connected_cb_t cb, bool have_creds) void wifi_config_portal_init(wifi_connected_cb_t cb, bool have_creds)
{ {
bool force_portal = (FORCE_PORTAL_DEFAULT != 0) || have_creds;
ESP_LOGW(TAG, "🔥 ENTREI no wifi_config_portal_init() force_portal=%d", (int)force_portal);
ensure_nvs_ready();
g_on_connected = cb; g_on_connected = cb;
esp_netif_init();
esp_event_loop_create_default(); ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());
// STA netif sempre
esp_netif_create_default_wifi_sta(); esp_netif_create_default_wifi_sta();
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); wifi_init_config_t wcfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg)); ESP_ERROR_CHECK(esp_wifi_init(&wcfg));
ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &on_wifi_event, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &on_wifi_event, NULL));
ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &on_got_ip, NULL)); ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &on_got_ip, NULL));
const esp_timer_create_args_t timer_args = { // Watchdog STA
.callback = &wifi_watchdog_cb, const esp_timer_create_args_t tcfg = {
.callback = wifi_watchdog_cb,
.name = "wifi_watchdog" .name = "wifi_watchdog"
}; };
esp_timer_create(&timer_args, &wifi_watchdog_timer); ESP_ERROR_CHECK(esp_timer_create(&tcfg, &wifi_watchdog_timer));
esp_timer_start_periodic(wifi_watchdog_timer, 60000000); // 60 s ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_watchdog_timer, 60000000));
char ssid[32] = {0}, pass[64] = {0}; char ssid[32] = {0}, pass[64] = {0};
bool stored = wifi_load_creds(ssid, pass); bool stored = wifi_load_creds(ssid, pass);
if (have_creds || stored) { if (force_portal) stored = false;
ESP_LOGI(TAG, "🔄 A ligar à rede guardada...");
wifi_config_t wifi_cfg = {0}; if (stored) {
strcpy((char *)wifi_cfg.sta.ssid, ssid); g_portal_mode = false;
strcpy((char *)wifi_cfg.sta.password, pass); ESP_LOGI(TAG, "✅ Credenciais encontradas: SSID=%s", ssid);
wifi_config_t w = {0};
strncpy((char *)w.sta.ssid, ssid, sizeof(w.sta.ssid));
strncpy((char *)w.sta.password, pass, sizeof(w.sta.password));
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_cfg)); ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &w));
ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_start());
esp_wifi_connect(); return;
} else { }
ESP_LOGW(TAG, "❌ Sem credenciais — modo AP de configuração...");
// ----------- PORTAL AP -----------
g_portal_mode = true;
ESP_LOGW(TAG, "⚠️ Sem credenciais válidas -> iniciar PORTAL");
esp_netif_create_default_wifi_ap(); esp_netif_create_default_wifi_ap();
wifi_config_t ap_cfg = { wifi_config_t ap = {0};
.ap = { ap.ap.max_connection = 4;
.ssid_len = 0, ap.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK;
.password = "12345678", strncpy((char*)ap.ap.password, PORTAL_AP_PASS, sizeof(ap.ap.password));
.max_connection = 4,
.authmode = WIFI_AUTH_WPA_WPA2_PSK
}
};
uint8_t mac[6];
esp_wifi_get_mac(WIFI_IF_AP, mac);
snprintf((char *)ap_cfg.ap.ssid, sizeof(ap_cfg.ap.ssid),
"ESP32_%02X%02X%02X", mac[3], mac[4], mac[5]);
if (strlen((char *)ap_cfg.ap.password) == 0)
ap_cfg.ap.authmode = WIFI_AUTH_OPEN;
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP)); uint8_t mac[6];
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_cfg)); ESP_ERROR_CHECK(esp_wifi_get_mac(WIFI_IF_AP, mac));
snprintf((char *)ap.ap.ssid, sizeof(ap.ap.ssid),
"ESP32_%02X%02X%02X", mac[3], mac[4], mac[5]);
// ✅ portal em APSTA para scan
ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_APSTA));
// ✅ no portal, usa RAM storage para não gravar config interna
ESP_ERROR_CHECK(esp_wifi_set_storage(WIFI_STORAGE_RAM));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap));
ESP_ERROR_CHECK(esp_wifi_start()); ESP_ERROR_CHECK(esp_wifi_start());
if (wifi_watchdog_timer) // watchdog não faz sentido em AP
esp_timer_stop(wifi_watchdog_timer); esp_timer_stop(wifi_watchdog_timer);
start_dns_server(); // HTTP primeiro
ESP_LOGI(TAG, "🚀 Vou iniciar HTTP agora!");
start_webserver(); start_webserver();
ESP_LOGI(TAG, "🌐 Criado AP SSID=%s Senha=%s", ap_cfg.ap.ssid, ap_cfg.ap.password); // DNS em task
} ESP_LOGI(TAG, "🚀 Vou iniciar DNS agora!");
start_dns_server();
ESP_LOGI(TAG, "✅ Portal ativo (HTTP+DNS+SCAN)");
} }