508 lines
15 KiB
C
508 lines
15 KiB
C
#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_log.h"
|
|
#include "esp_event.h"
|
|
#include "esp_netif.h"
|
|
#include "esp_timer.h"
|
|
#include "esp_http_server.h"
|
|
#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;
|
|
|
|
// ✅ 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 — reconectar...");
|
|
esp_wifi_disconnect();
|
|
vTaskDelay(pdMS_TO_TICKS(1000));
|
|
esp_wifi_connect();
|
|
}
|
|
}
|
|
|
|
/* ========================= Eventos WiFi ========================= */
|
|
static void on_wifi_event(void *arg, esp_event_base_t event_base,
|
|
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");
|
|
// ✅ Em portal mode: NÃO conectar, só scan
|
|
if (!g_portal_mode) {
|
|
esp_wifi_connect();
|
|
} else {
|
|
ESP_LOGI(TAG, "🛑 Portal mode: não faço connect, só scan.");
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void wifi_start_app_task(void *arg)
|
|
{
|
|
(void)arg;
|
|
if (g_on_connected) g_on_connected();
|
|
vTaskDelete(NULL);
|
|
}
|
|
|
|
static void on_got_ip(void *arg, esp_event_base_t event_base,
|
|
int32_t event_id, void *event_data)
|
|
{
|
|
(void)arg; (void)event_base; (void)event_id; (void)event_data;
|
|
|
|
got_ip = true;
|
|
ESP_LOGI(TAG, "🌐 IP obtido");
|
|
|
|
if (wifi_watchdog_timer) esp_timer_stop(wifi_watchdog_timer);
|
|
|
|
xTaskCreate(wifi_start_app_task, "wifi_start_app", 6144, NULL, 5, NULL);
|
|
}
|
|
|
|
/* ========================= 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 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)
|
|
{
|
|
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);
|
|
}
|
|
|
|
// ✅ 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)
|
|
{
|
|
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>"
|
|
"<meta charset='UTF-8'>"
|
|
"<title>Configuração Wi-Fi</title>"
|
|
"<style>"
|
|
"body{font-family:Arial;background:#eef2f3;text-align:center;}"
|
|
"form{background:#fff;padding:20px;margin:50px auto;width:360px;"
|
|
"border-radius:10px;box-shadow:0 0 10px rgba(0,0,0,.2);}"
|
|
"input,select{width:90%%;padding:10px;margin:10px;font-size:16px;}"
|
|
"button{background:#007bff;color:#fff;border:none;padding:10px 20px;"
|
|
"border-radius:6px;font-size:16px;}"
|
|
".mini{font-size:12px;color:#555;}"
|
|
"</style></head><body>"
|
|
"<h2>Configuração Wi-Fi</h2>"
|
|
"<form id='wifiForm' action='/save' method='POST'>"
|
|
);
|
|
|
|
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>"
|
|
"</form>"
|
|
"<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){"
|
|
"e.preventDefault();"
|
|
"fetch('/save',{method:'POST',body:new URLSearchParams(new FormData(this))})"
|
|
".then(()=>alert('Credenciais enviadas!'));"
|
|
"};"
|
|
"</script></body></html>"
|
|
);
|
|
|
|
httpd_resp_set_type(req, "text/html; charset=UTF-8");
|
|
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)
|
|
{
|
|
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_enc, pass_enc);
|
|
url_decode(ssid, ssid_enc);
|
|
url_decode(pass, pass_enc);
|
|
|
|
wifi_save_creds(ssid, pass);
|
|
ESP_LOGI(TAG, "💾 Guardado SSID=%s", ssid);
|
|
|
|
const char *resp =
|
|
"<html><body style='text-align:center;margin-top:50px;'>"
|
|
"<h2>✅ Credenciais guardadas</h2>"
|
|
"<p>Reiniciando...</p></body></html>";
|
|
|
|
httpd_resp_set_type(req, "text/html; charset=UTF-8");
|
|
httpd_resp_send(req, resp, HTTPD_RESP_USE_STRLEN);
|
|
|
|
vTaskDelay(pdMS_TO_TICKS(1200));
|
|
esp_restart();
|
|
return ESP_OK;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/* ========================= 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_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 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));
|
|
|
|
// Watchdog STA
|
|
const esp_timer_create_args_t tcfg = {
|
|
.callback = wifi_watchdog_cb,
|
|
.name = "wifi_watchdog"
|
|
};
|
|
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 (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, &w));
|
|
ESP_ERROR_CHECK(esp_wifi_start());
|
|
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)");
|
|
}
|