From 0d828caae5d2b3b578a7015a873328a25064174c Mon Sep 17 00:00:00 2001 From: XupaMisto Date: Sat, 27 Dec 2025 22:39:39 +0000 Subject: [PATCH] wifi: portal Wi-Fi top com scan, clear_wifi MQTT e fluxo limpo STA/AP --- main/dns_server.c | 95 ++++-- main/include/dns_server.h | 1 + main/include/wifi_config_portal.h | 23 +- main/mqtt_comandos.c | 32 ++ main/notas.txt | 1 + main/wifi_config_portal.c | 527 ++++++++++++++++++++++-------- 6 files changed, 495 insertions(+), 184 deletions(-) diff --git a/main/dns_server.c b/main/dns_server.c index 5b58040..7bcb1ec 100644 --- a/main/dns_server.c +++ b/main/dns_server.c @@ -1,60 +1,101 @@ #include -#include "lwip/err.h" +#include + #include "lwip/sockets.h" +#include "lwip/inet.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" #include "esp_log.h" #include "dns_server.h" - static const char *TAG = "DNS"; - #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); 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; } - 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_port = htons(DNS_PORT); addr.sin_addr.s_addr = htonl(INADDR_ANY); 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); + s_dns_running = false; + s_dns_task = NULL; + vTaskDelete(NULL); 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) { - char buf[512]; - struct sockaddr_in source_addr; - socklen_t socklen = sizeof(source_addr); + while (s_dns_running) { + uint8_t buf[512]; + struct sockaddr_in source_addr = {0}; + socklen_t slen = sizeof(source_addr); int len = recvfrom(sock, buf, sizeof(buf), 0, - (struct sockaddr*)&source_addr, &socklen); + (struct sockaddr*)&source_addr, &slen); - if (len > 0) { - // resposta simples: sempre devolve 192.168.4.1 - buf[2] |= 0x80; // resposta - buf[3] |= 0x80; // autoritativo - buf[7] = 1; // 1 resposta + if (len <= 0) { + vTaskDelay(pdMS_TO_TICKS(10)); + continue; + } - // append resposta A 192.168.4.1 + buf[2] |= 0x80; // resposta + buf[3] |= 0x80; // RA + buf[6] = 0x00; + buf[7] = 0x01; // ANCOUNT=1 + + if (len + 16 < (int)sizeof(buf)) { buf[len++] = 0xC0; buf[len++] = 0x0C; - buf[len++] = 0x00; buf[len++] = 0x01; // type A - buf[len++] = 0x00; buf[len++] = 0x01; // class IN + buf[len++] = 0x00; buf[len++] = 0x01; + buf[len++] = 0x00; buf[len++] = 0x01; buf[len++] = 0x00; buf[len++] = 0x00; - buf[len++] = 0x00; buf[len++] = 0x3C; // TTL 60s - buf[len++] = 0x00; buf[len++] = 0x04; // data length - buf[len++] = 192; buf[len++] = 168; - buf[len++] = 4; buf[len++] = 1; + buf[len++] = 0x00; buf[len++] = 0x3C; + buf[len++] = 0x00; buf[len++] = 0x04; + buf[len++] = 192; buf[len++] = 168; + buf[len++] = 4; buf[len++] = 1; - sendto(sock, buf, len, 0, - (struct sockaddr*)&source_addr, socklen); + sendto(sock, buf, len, 0, (struct sockaddr*)&source_addr, slen); } } + + 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; } diff --git a/main/include/dns_server.h b/main/include/dns_server.h index 9d9c7f7..fa81d8d 100644 --- a/main/include/dns_server.h +++ b/main/include/dns_server.h @@ -4,6 +4,7 @@ extern "C" { #endif void start_dns_server(void); +void stop_dns_server(void); #ifdef __cplusplus } diff --git a/main/include/wifi_config_portal.h b/main/include/wifi_config_portal.h index 5b0fabf..7dd5e5a 100644 --- a/main/include/wifi_config_portal.h +++ b/main/include/wifi_config_portal.h @@ -1,33 +1,14 @@ #pragma once - -#include "esp_err.h" -#include "esp_wifi_types.h" -#include "esp_event.h" +#include #ifdef __cplusplus extern "C" { #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); -/** - * @brief Inicializa o módulo Wi-Fi com modo STA ou AP de configuração. - * - * @param cb Função callback chamada quando há ligação Wi-Fi com IP. - * @param have_creds true se já tiver credenciais gravadas. - */ void wifi_config_portal_init(wifi_connected_cb_t cb, bool have_creds); - -/** - * @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); +void wifi_clear_creds(void); #ifdef __cplusplus } diff --git a/main/mqtt_comandos.c b/main/mqtt_comandos.c index 884815b..ecd03b8 100644 --- a/main/mqtt_comandos.c +++ b/main/mqtt_comandos.c @@ -2,6 +2,7 @@ #include #include #include +#include "esp_wifi.h" #include "esp_log.h" #include "esp_system.h" @@ -19,6 +20,7 @@ #include "ui.h" #include "display.h" +#include "wifi_config_portal.h" static const char *TAG = "MQTT_CMD"; @@ -90,6 +92,36 @@ void mqtt_comandos_handle(cJSON *root) vTaskDelay(pdMS_TO_TICKS(200)); 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) { diff --git a/main/notas.txt b/main/notas.txt index f77d2bc..72cc3c7 100644 --- a/main/notas.txt +++ b/main/notas.txt @@ -42,6 +42,7 @@ { "cmd": "DISP_TEST" } { "cmd": "LED_TEST" } { "cmd": "SET_PERC", "val": 70 } +{ "cmd": "CLEAR_WIFI","confirm": 1} esp/esp_BBC9A4/cmd // onde envias os comandos diff --git a/main/wifi_config_portal.c b/main/wifi_config_portal.c index d5d951d..62d3ef9 100644 --- a/main/wifi_config_portal.c +++ b/main/wifi_config_portal.c @@ -1,252 +1,507 @@ #include "wifi_config_portal.h" + +#include +#include +#include +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + #include "esp_wifi.h" #include "esp_log.h" #include "esp_event.h" #include "esp_netif.h" -#include "nvs_flash.h" #include "esp_timer.h" #include "esp_http_server.h" -#include -#include -#include +#include "esp_system.h" +#include "esp_err.h" + +#include "nvs_flash.h" +#include "nvs.h" + #include "dns_server.h" #ifndef MIN #define MIN(a,b) ((a) < (b) ? (a) : (b)) #endif +// ===== Ajustes ===== +#define FORCE_PORTAL_DEFAULT 0 // 1 = força sempre portal +#define PORTAL_AP_PASS "12345678" +// =================== + static const char *TAG = "WIFI_PORTAL"; + static httpd_handle_t server = NULL; static wifi_connected_cb_t g_on_connected = NULL; static esp_timer_handle_t wifi_watchdog_timer = NULL; static bool got_ip = false; -// --- WATCHDOG: tenta reconectar se ficar 60 s sem IP --- -static void wifi_watchdog_cb(void *arg) { +// ✅ Flag: quando true estamos em portal (APSTA só para scan), NÃO conectar +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) { - ESP_LOGW(TAG, "⏱️ 60 s sem IP — a tentar reconectar Wi-Fi..."); + ESP_LOGW(TAG, "⏱️ 60 s sem IP — reconectar..."); esp_wifi_disconnect(); - vTaskDelay(pdMS_TO_TICKS(1000)); // espera 1 s + vTaskDelay(pdMS_TO_TICKS(1000)); esp_wifi_connect(); } } -// --- EVENTOS WIFI --- +/* ========================= Eventos WiFi ========================= */ 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) { switch (event_id) { - case WIFI_EVENT_STA_START: - ESP_LOGI(TAG, "📡 STA start"); + + case WIFI_EVENT_STA_START: + ESP_LOGI(TAG, "📡 STA start"); + // ✅ Em portal mode: NÃO conectar, só scan + if (!g_portal_mode) { esp_wifi_connect(); - break; - case WIFI_EVENT_STA_CONNECTED: - ESP_LOGI(TAG, "🔗 STA conectado ao AP, aguardando IP..."); - break; - case WIFI_EVENT_STA_DISCONNECTED: { - wifi_event_sta_disconnected_t *disconn = (wifi_event_sta_disconnected_t *) event_data; - ESP_LOGW(TAG, "⚠️ STA desconectado (motivo %d)", disconn->reason); - got_ip = false; - esp_wifi_connect(); - break; + } else { + ESP_LOGI(TAG, "🛑 Portal mode: não faço connect, só scan."); } - default: - ESP_LOGI(TAG, "ℹ️ Evento Wi-Fi não tratado: %ld", (long)event_id); - break; + break; + + case WIFI_EVENT_STA_CONNECTED: + ESP_LOGI(TAG, "🔗 STA conectado, esperando IP..."); + break; + + case WIFI_EVENT_STA_DISCONNECTED: { + wifi_event_sta_disconnected_t *d = + (wifi_event_sta_disconnected_t *)event_data; + ESP_LOGW(TAG, "⚠️ STA desconectado (motivo %d)", d ? d->reason : -1); + got_ip = false; + + // ✅ só tenta reconectar no modo normal, não no portal + if (!g_portal_mode) { + esp_wifi_connect(); + } + break; + } + + default: + break; } } } -// --- Task auxiliar para iniciar o app (MQTT, LEDs, etc.) --- -static void wifi_start_app_task(void *arg) { - if (g_on_connected) - g_on_connected(); // chamada do callback da aplicação +static void wifi_start_app_task(void *arg) +{ + (void)arg; + if (g_on_connected) g_on_connected(); vTaskDelete(NULL); } -// --- EVENTO IP OBTIDO --- 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; - ESP_LOGI(TAG, "🌐 STA obteve IP!"); + ESP_LOGI(TAG, "🌐 IP obtido"); - if (wifi_watchdog_timer) - esp_timer_stop(wifi_watchdog_timer); + if (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); } -// --- NVS: guardar e ler credenciais --- -static bool wifi_load_creds(char *ssid, char *pass) { +/* ========================= NVS Credenciais (teu namespace) ========================= */ +static bool wifi_load_creds(char *ssid, char *pass) +{ nvs_handle_t nvs; 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; } - if (nvs_get_str(nvs, "pass", pass, &len2) != ESP_OK) { nvs_close(nvs); return false; } + + size_t ssid_len = 32, pass_len = 64; + 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); + + if (e1 != ESP_OK || e2 != ESP_OK) return false; + if (ssid[0] == '\0') return false; + 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; if (nvs_open("wifi", NVS_READWRITE, &nvs) != ESP_OK) return; + nvs_set_str(nvs, "ssid", ssid); nvs_set_str(nvs, "pass", pass); nvs_commit(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, "&", 5); o += 5; } + else if (c == '<') { memcpy(dst+o, "<", 4); o += 4; } + else if (c == '>') { memcpy(dst+o, ">", 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) { - 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, "" "" "Configuração Wi-Fi" "" "

Configuração Wi-Fi

" "
" - "SSID:

" - "Senha:


" + ); + + if (n > 0) { + o += snprintf(html+o, 9000-o, + "
Redes encontradas (scan):
" + ""); + } else { + o += snprintf(html+o, 9000-o, + "
Scan indisponível. Escreve o SSID manualmente.
" + ); + } + + o += snprintf(html+o, 9000-o, + "SSID:

" + "Senha:

" "" "
" "" - ""; + "" + ); httpd_resp_set_type(req, "text/html; charset=UTF-8"); - httpd_resp_set_hdr(req, "Cache-Control", "no-store, no-cache, must-revalidate"); - return httpd_resp_send(req, html, HTTPD_RESP_USE_STRLEN); + esp_err_t r = httpd_resp_send(req, html, HTTPD_RESP_USE_STRLEN); + free(html); + return r; } static esp_err_t handle_save(httpd_req_t *req) { - char buf[128]; - 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'; + ESP_LOGI(TAG, "📥 POST %s len=%d", req->uri, (int)req->content_len); + 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}; - 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); - ESP_LOGI(TAG, "💾 Credenciais salvas: SSID=%s", ssid); + ESP_LOGI(TAG, "💾 Guardado SSID=%s", ssid); const char *resp = - "" - "" - "

✅ Credenciais guardadas!

" - "

O dispositivo vai reiniciar em 2 segundos...

" - ""; + "" + "

✅ Credenciais guardadas

" + "

Reiniciando...

"; httpd_resp_set_type(req, "text/html; charset=UTF-8"); httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN); - vTaskDelay(pdMS_TO_TICKS(2000)); + vTaskDelay(pdMS_TO_TICKS(1200)); esp_restart(); - return ESP_OK; } -// --- Inicia o servidor HTTP --- -static void start_webserver(void) { - httpd_config_t config = HTTPD_DEFAULT_CONFIG(); - config.stack_size = 8192; - config.server_port = 80; - config.max_uri_handlers = 8; - config.uri_match_fn = httpd_uri_match_wildcard; - config.recv_wait_timeout = 10; - config.send_wait_timeout = 10; - config.max_resp_headers = 20; - - if (httpd_start(&server, &config) == ESP_OK) { - httpd_uri_t root = { .uri = "/", .method = HTTP_GET, .handler = handle_root }; - httpd_uri_t save = { .uri = "/save", .method = HTTP_POST, .handler = handle_save }; - httpd_register_uri_handler(server, &root); - httpd_register_uri_handler(server, &save); - ESP_LOGI(TAG, "🌐 Servidor HTTP iniciado na porta %d", config.server_port); - } else { - ESP_LOGE(TAG, "❌ Falha ao iniciar servidor HTTP!"); - } +static esp_err_t handle_captive(httpd_req_t *req) +{ + ESP_LOGI(TAG, "📥 GET %s -> redirect /", req->uri); + httpd_resp_set_status(req, "302 Found"); + httpd_resp_set_hdr(req, "Location", "/"); + return httpd_resp_send(req, NULL, 0); } -// --- Inicialização principal --- +/* ========================= Webserver ========================= */ +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; + } + + 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 captive = { .uri="/*", .method=HTTP_GET, .handler=handle_captive }; + + ESP_LOGI(TAG, "register / -> %s", esp_err_to_name(httpd_register_uri_handler(server, &root))); + ESP_LOGI(TAG, "register /save -> %s", esp_err_to_name(httpd_register_uri_handler(server, &save))); + ESP_LOGI(TAG, "register /* -> %s", esp_err_to_name(httpd_register_uri_handler(server, &captive))); +} + +/* ========================= API ========================= */ 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; - 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(); - wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); - ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + wifi_init_config_t wcfg = WIFI_INIT_CONFIG_DEFAULT(); + 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(IP_EVENT, IP_EVENT_STA_GOT_IP, &on_got_ip, NULL)); - const esp_timer_create_args_t timer_args = { - .callback = &wifi_watchdog_cb, + // Watchdog STA + const esp_timer_create_args_t tcfg = { + .callback = wifi_watchdog_cb, .name = "wifi_watchdog" }; - esp_timer_create(&timer_args, &wifi_watchdog_timer); - esp_timer_start_periodic(wifi_watchdog_timer, 60000000); // 60 s + ESP_ERROR_CHECK(esp_timer_create(&tcfg, &wifi_watchdog_timer)); + ESP_ERROR_CHECK(esp_timer_start_periodic(wifi_watchdog_timer, 60000000)); char ssid[32] = {0}, pass[64] = {0}; bool stored = wifi_load_creds(ssid, pass); - if (have_creds || stored) { - ESP_LOGI(TAG, "🔄 A ligar à rede guardada..."); - wifi_config_t wifi_cfg = {0}; - strcpy((char *)wifi_cfg.sta.ssid, ssid); - strcpy((char *)wifi_cfg.sta.password, pass); + if (force_portal) stored = false; + + if (stored) { + g_portal_mode = false; + 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_config(WIFI_IF_STA, &wifi_cfg)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &w)); ESP_ERROR_CHECK(esp_wifi_start()); - esp_wifi_connect(); - } else { - ESP_LOGW(TAG, "❌ Sem credenciais — modo AP de configuração..."); - esp_netif_create_default_wifi_ap(); - - wifi_config_t ap_cfg = { - .ap = { - .ssid_len = 0, - .password = "12345678", - .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)); - ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &ap_cfg)); - ESP_ERROR_CHECK(esp_wifi_start()); - - if (wifi_watchdog_timer) - esp_timer_stop(wifi_watchdog_timer); - - start_dns_server(); - start_webserver(); - - ESP_LOGI(TAG, "🌐 Criado AP SSID=%s Senha=%s", ap_cfg.ap.ssid, ap_cfg.ap.password); + return; } + + // ----------- PORTAL AP ----------- + g_portal_mode = true; + ESP_LOGW(TAG, "⚠️ Sem credenciais válidas -> iniciar PORTAL"); + + esp_netif_create_default_wifi_ap(); + + wifi_config_t ap = {0}; + ap.ap.max_connection = 4; + ap.ap.authmode = WIFI_AUTH_WPA_WPA2_PSK; + strncpy((char*)ap.ap.password, PORTAL_AP_PASS, sizeof(ap.ap.password)); + + uint8_t mac[6]; + 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()); + + // watchdog não faz sentido em AP + esp_timer_stop(wifi_watchdog_timer); + + // HTTP primeiro + ESP_LOGI(TAG, "🚀 Vou iniciar HTTP agora!"); + start_webserver(); + + // DNS em task + ESP_LOGI(TAG, "🚀 Vou iniciar DNS agora!"); + start_dns_server(); + + ESP_LOGI(TAG, "✅ Portal ativo (HTTP+DNS+SCAN)"); }