-
+
+
+
-
-
+
+
+
-
-
-
-
-
+
+
+
+
-
-
-
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/main/index.js b/main/index.js
index a0dfaaa..abeacd3 100644
--- a/main/index.js
+++ b/main/index.js
@@ -461,5 +461,44 @@ document.getElementById('resetBtn').addEventListener('click', () => {
window.location.reload();
});
document.getElementById('powerOff').addEventListener('click', () => window.location.href = "/poweroff");
+
+function setupWifiListeners() {
+ const modal = document.getElementById('wifiModal');
+ const btn = document.getElementById('wifiBtn');
+ const closeBtn = document.getElementById('closeWifi');
+ const saveBtn = document.getElementById('saveWifi');
+
+ if (btn) btn.onclick = () => {
+ modal.style.display = "flex";
+ document.getElementById('wifiSsid').focus();
+ };
+ if (closeBtn) closeBtn.onclick = () => modal.style.display = "none";
+ if (saveBtn) saveBtn.onclick = () => {
+ const ssid = document.getElementById('wifiSsid').value;
+ const pass = document.getElementById('wifiPass').value;
+
+ if (!ssid) {
+ alert("SSID is required");
+ return;
+ }
+
+ saveBtn.innerText = "Saving...";
+ fetch('/api/save_wifi', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ ssid, password: pass })
+ })
+ .then(res => res.text())
+ .then(text => {
+ alert(text);
+ window.location.reload();
+ })
+ .catch(err => {
+ alert("Error: " + err);
+ saveBtn.innerText = "Save";
+ });
+ };
+}
+setupWifiListeners();
setParams();
connect();
diff --git a/main/main.c b/main/main.c
index 24a0d3f..4a0cc11 100644
--- a/main/main.c
+++ b/main/main.c
@@ -14,29 +14,20 @@
#include "esp_sleep.h"
#include "esp_system.h"
#include "esp_wifi.h"
+#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/task.h"
+#include "wifi_manager.h"
#include "nvs_flash.h"
// Tag for logging
static const char* TAG = "ESP-SCOPE";
-// WiFi configuration from Kconfig
-/* See untracked wifi-credentials.h */
-#ifndef ESP_WIFI_SSID
-#define ESP_WIFI_SSID CONFIG_ESP_WIFI_SSID
-#define ESP_WIFI_PASS CONFIG_ESP_WIFI_PASSWORD
-#endif
-
#define ESP_MAXIMUM_RETRY 5
/* FreeRTOS event group to signal when we are connected*/
-static EventGroupHandle_t s_wifi_event_group;
-#define WIFI_CONNECTED_BIT BIT0
-#define WIFI_FAIL_BIT BIT1
-static int s_retry_num = 0;
// Embedded index.html
extern const uint8_t index_html_start[] asm("_binary_index_html_start");
@@ -45,7 +36,6 @@ extern const uint8_t index_js_start[] asm("_binary_index_js_start");
extern const uint8_t index_js_end[] asm("_binary_index_js_end");
// Forward declarations
-static void wifi_init_sta(void);
static void start_webserver(void);
// ADC Configuration
@@ -62,7 +52,7 @@ static void start_webserver(void);
* Web Server Configuration
*/
static httpd_handle_t s_server = NULL;
-
+static bool is_ap = false;
#define ADC_READ_LEN 4096
static adc_continuous_handle_t adc_handle = NULL;
@@ -78,13 +68,6 @@ static adc_bitwidth_t s_bit_width = ADC_BIT_WIDTH;
static uint16_t s_test_hz = 100;
// Forward declarations
-static void wifi_init_sta(void);
-static void continuous_adc_init(adc_channel_t* channel, uint8_t channel_num,
- adc_continuous_handle_t* out_handle);
-static esp_err_t ws_handler(httpd_req_t* req);
-
-// Forward declarations
-static void wifi_init_sta(void);
static void continuous_adc_init(adc_channel_t* channel, uint8_t channel_num,
adc_continuous_handle_t* out_handle);
static esp_err_t ws_handler(httpd_req_t* req);
@@ -287,14 +270,44 @@ static void start_test_signal(uint32_t hz) {
}
static void show_status_led() {
-#ifdef CONFIG_LED_BUILTIN
+ #ifdef CONFIG_BSP_CONFIG_GPIO
+ gpio_config_t rst_conf = {
+ .pin_bit_mask = (1ULL << CONFIG_BSP_CONFIG_GPIO),
+ .mode = GPIO_MODE_INPUT,
+ .pull_up_en = GPIO_PULLUP_ENABLE,
+ .pull_down_en = GPIO_PULLDOWN_DISABLE,
+ .intr_type = GPIO_INTR_DISABLE
+ };
+ gpio_config(&rst_conf);
+ #endif
+
+ int64_t reset_pressed_time = 0;
while (true) {
- vTaskDelay(pdMS_TO_TICKS(100));
+ int64_t now = esp_timer_get_time() / 1000;
+ #ifdef CONFIG_LED_BUILTIN
+ vTaskDelay(pdMS_TO_TICKS(is_ap ? 500 : 100));
gpio_set_level(CONFIG_LED_BUILTIN, 1);
vTaskDelay(pdMS_TO_TICKS(s_ws_client_fd == -1 ? 900 : 200));
gpio_set_level(CONFIG_LED_BUILTIN, 0);
+ #else
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ #endif
+
+ // Check Reset Pin
+ #ifdef CONFIG_BSP_CONFIG_GPIO
+ if (!is_ap && gpio_get_level(CONFIG_BSP_CONFIG_GPIO) == 0) {
+ if (now - reset_pressed_time > 1000) {
+ ESP_LOGW(TAG, "Factory Reset Triggered via GPIO %d", CONFIG_BSP_CONFIG_GPIO);
+ wifi_manager_erase_config();
+ esp_restart();
+ } else {
+ reset_pressed_time = now;
+ }
+ } else {
+ reset_pressed_time = 0;
+ }
+ #endif
}
-#endif
}
void app_main(void) {
@@ -318,7 +331,6 @@ void app_main(void) {
gpio_set_level(CONFIG_LED_BUILTIN, 0);
#endif
- ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
/* Board-specific WiFi init (if any) */
// Seeed XIAO ESP32C6: Configure GPIO 3 and GPIO 14 as outputs
gpio_config_t board_io_conf = {
@@ -334,82 +346,21 @@ void app_main(void) {
gpio_set_level(14, 0);
/* end board-specific WiFi init (if any) */
- wifi_init_sta();
+ is_ap = wifi_manager_init_wifi();
+
start_test_signal(100);
xTaskCreate(adc_read_task, "adc_read_task", 8192 + ADC_READ_LEN, NULL, 5, NULL);
- // Wait for WiFi connection
- EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group,
- WIFI_CONNECTED_BIT | WIFI_FAIL_BIT,
- pdFALSE, pdFALSE, portMAX_DELAY);
+ // Wait for WiFi connection (Not strict blocking anymore, manager handles it)
+ // But let's keep it to print status
+ // Note: s_wifi_event_group is local to this file, we removed it in place of manager's.
+ // Proper way: expose event group from manager or just dont block here.
+ // For now, let's just proceed. The webserver will start and wait for connections.
- if (bits & WIFI_CONNECTED_BIT) {
- ESP_LOGI(TAG, "connected to ap SSID:%s", ESP_WIFI_SSID);
- start_webserver();
- show_status_led();
- } else if (bits & WIFI_FAIL_BIT) {
- ESP_LOGI(TAG, "Failed to connect to SSID:%s", ESP_WIFI_SSID);
- } else {
- ESP_LOGE(TAG, "UNEXPECTED EVENT");
- }
+ start_webserver();
+ show_status_led();
}
-static void 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) {
- if (s_retry_num < ESP_MAXIMUM_RETRY) {
- esp_wifi_connect();
- s_retry_num++;
- ESP_LOGI(TAG, "retry to connect to the AP");
- } else {
- xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT);
- }
- ESP_LOGI(TAG, "connect to the AP fail");
- } 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, "got ip:" IPSTR, IP2STR(&event->ip_info.ip));
- s_retry_num = 0;
- xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
- }
-}
-
-static void wifi_init_sta(void) {
- s_wifi_event_group = xEventGroupCreate();
-
- ESP_ERROR_CHECK(esp_netif_init());
-
- ESP_ERROR_CHECK(esp_event_loop_create_default());
- esp_netif_t* netif = esp_netif_create_default_wifi_sta();
- esp_netif_set_hostname(netif, "esp-scope"); // Set hostname for the STA interface
-
- wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
- ESP_ERROR_CHECK(esp_wifi_init(&cfg));
-
- esp_event_handler_instance_t instance_any_id;
- esp_event_handler_instance_t instance_got_ip;
- ESP_ERROR_CHECK(esp_event_handler_instance_register(
- WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id));
- ESP_ERROR_CHECK(esp_event_handler_instance_register(
- IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip));
-
- wifi_config_t wifi_config = {
- .sta =
- {
- .ssid = ESP_WIFI_SSID,
- .password = ESP_WIFI_PASS,
- .threshold.authmode = WIFI_AUTH_WPA2_PSK,
- },
- };
- ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
- ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
- ESP_ERROR_CHECK(esp_wifi_start());
- ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE));
-
- ESP_LOGI(TAG, "wifi_init_sta finished.");
-}
/*
* WebSocket Handler
@@ -562,6 +513,16 @@ static esp_err_t power_handler(httpd_req_t* req) {
static const httpd_uri_t uri_power = {
.uri = "/poweroff", .method = HTTP_GET, .handler = power_handler, .user_ctx = NULL};
+/* Error handler for 404 - Redirects to captive portal */
+static esp_err_t http_404_error_handler(httpd_req_t *req, httpd_err_code_t err)
+{
+ /* Set status 302 Redirect */
+ httpd_resp_set_status(req, "302 Found");
+ httpd_resp_set_hdr(req, "Location", "/");
+ httpd_resp_send(req, NULL, 0); // No body needed
+ return ESP_OK;
+}
+
static void start_webserver(void) {
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.lru_purge_enable = true;
@@ -573,7 +534,13 @@ static void start_webserver(void) {
httpd_register_uri_handler(s_server, &uri_index_js);
httpd_register_uri_handler(s_server, &uri_ws);
httpd_register_uri_handler(s_server, &uri_params);
+
+ // Register WiFi Manager endpoints
+ wifi_manager_register_uri(s_server);
httpd_register_uri_handler(s_server, &uri_power);
+
+ // Register 404 handler for Captive Portal redirection
+ httpd_register_err_handler(s_server, HTTPD_404_NOT_FOUND, http_404_error_handler);
} else {
ESP_LOGI(TAG, "Error starting server!");
}
diff --git a/main/wifi_manager.c b/main/wifi_manager.c
new file mode 100644
index 0000000..264386d
--- /dev/null
+++ b/main/wifi_manager.c
@@ -0,0 +1,354 @@
+#include "wifi_manager.h"
+#include
+#include "esp_mac.h"
+#include
+#include "freertos/FreeRTOS.h"
+#include "freertos/task.h"
+#include "freertos/event_groups.h"
+#include "esp_system.h"
+#include "esp_wifi.h"
+#include "esp_event.h"
+#include "esp_log.h"
+#include "nvs_flash.h"
+#include "nvs.h"
+#include "lwip/err.h"
+#include "lwip/sys.h"
+#include "lwip/sockets.h"
+#include "cJSON.h"
+
+// Logs
+static const char *TAG = "WIFI_MGR";
+
+// Event Group
+static EventGroupHandle_t s_wifi_event_group;
+#define WIFI_CONNECTED_BIT BIT0
+#define WIFI_FAIL_BIT BIT1
+
+// Server Handle for registering handlers
+static httpd_handle_t s_server_handle = NULL;
+
+// DNS Port
+#define DNS_PORT 53
+
+// SoftAP Config
+#define AP_SSID "ESP-Scope"
+#define AP_PASS ""
+
+// Forward decls
+
+static void wifi_init_softap(void);
+static void wifi_init_station(const char* ssid, const char* pass);
+
+// NVS Keys
+#define NVS_NAMESPACE "wifi_cfg"
+#define NVS_KEY_SSID "ssid"
+#define NVS_KEY_PASS "pass"
+
+// WiFi Event Handler
+static void 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) {
+ ESP_LOGI(TAG, "Retry connecting to AP...");
+ esp_wifi_connect();
+ // In a real product, we might count retries and switch to AP mode if failing.
+ // For now, infinite retry is simple.
+ } 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, "Got IP: " IPSTR, IP2STR(&event->ip_info.ip));
+ xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT);
+ } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
+ wifi_event_ap_staconnected_t *event = (wifi_event_ap_staconnected_t *)event_data;
+ ESP_LOGI(TAG, "station "MACSTR" join, AID=%d", MAC2STR(event->mac), event->aid);
+ } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
+ wifi_event_ap_stadisconnected_t *event = (wifi_event_ap_stadisconnected_t *)event_data;
+ ESP_LOGI(TAG, "station "MACSTR" leave, AID=%d", MAC2STR(event->mac), event->aid);
+ }
+}
+
+// -------------------------------------------------------------------------
+// DNS Server Task (Captive Portal)
+// -------------------------------------------------------------------------
+static void dns_server_task(void *pvParameters) {
+ int sock;
+ struct sockaddr_in server_addr;
+ struct sockaddr_in client_addr;
+ socklen_t client_addr_len = sizeof(client_addr);
+ uint8_t rx_buffer[128];
+ uint8_t tx_buffer[128];
+
+ // Create UDP socket
+ sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
+ if (sock < 0) {
+ ESP_LOGE(TAG, "Unable to create DNS socket: errno %d", errno);
+ vTaskDelete(NULL);
+ }
+
+ server_addr.sin_family = AF_INET;
+ server_addr.sin_port = htons(DNS_PORT);
+ server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ if (bind(sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {
+ ESP_LOGE(TAG, "Socket unable to bind: errno %d", errno);
+ close(sock);
+ vTaskDelete(NULL);
+ }
+
+ ESP_LOGI(TAG, "DNS Server started on port 53");
+
+ while (1) {
+ int len = recvfrom(sock, rx_buffer, sizeof(rx_buffer), 0, (struct sockaddr *)&client_addr, &client_addr_len);
+ if (len < 0) {
+ ESP_LOGE(TAG, "recvfrom failed: errno %d", errno);
+ break;
+ }
+
+ // Basic DNS Header Parsing
+ // We just want to spoof the answer for any A-record query.
+ // Transaction ID: bytes 0-1 (Copy)
+ // Flags: bytes 2-3 (Standard Query -> Standard Response)
+
+ if (len > 12) {
+ // Copy Transaction ID
+ tx_buffer[0] = rx_buffer[0];
+ tx_buffer[1] = rx_buffer[1];
+
+ // Flags: Response (QR=1), Opcode=0, AA=0, TC=0, RD=1, RA=0, Z=0, RCODE=0
+ // 0x8180 (Standard query response, No error)
+ tx_buffer[2] = 0x81;
+ tx_buffer[3] = 0x80;
+
+ // Questions Count: Copy (should be 1)
+ tx_buffer[4] = rx_buffer[4];
+ tx_buffer[5] = rx_buffer[5];
+
+ // Answer RRs: 1
+ tx_buffer[6] = 0x00;
+ tx_buffer[7] = 0x01;
+
+ // Authority RRs: 0
+ tx_buffer[8] = 0x00;
+ tx_buffer[9] = 0x00;
+
+ // Additional RRs: 0
+ tx_buffer[10] = 0x00;
+ tx_buffer[11] = 0x00;
+
+ // Copy Question Section
+ // In a real server we'd parse this length. For simplicity in captive portal,
+ // we assume the question fits in the buffer and just echo it.
+ // Point to end of question to append answer.
+ int question_len = len - 12;
+ if (question_len > 0 && (12 + question_len + 16 < sizeof(tx_buffer))) {
+ memcpy(&tx_buffer[12], &rx_buffer[12], question_len);
+
+ // Answer Section
+ int offset = 12 + question_len;
+
+ // Name Pointer (0xC00C -> Point to start of packet header + 12 = start of question name)
+ tx_buffer[offset++] = 0xC0;
+ tx_buffer[offset++] = 0x0C;
+
+ // Type: A (Host Address) = 1
+ tx_buffer[offset++] = 0x00;
+ tx_buffer[offset++] = 0x01;
+
+ // Class: IN (Internet) = 1
+ tx_buffer[offset++] = 0x00;
+ tx_buffer[offset++] = 0x01;
+
+ // TTL: 60 seconds
+ tx_buffer[offset++] = 0x00;
+ tx_buffer[offset++] = 0x00;
+ tx_buffer[offset++] = 0x00;
+ tx_buffer[offset++] = 0x3C;
+
+ // Data Length: 4 (IPv4)
+ tx_buffer[offset++] = 0x00;
+ tx_buffer[offset++] = 0x04;
+
+ // Address: 192.168.4.1 (Default SoftAP IP)
+ tx_buffer[offset++] = 192;
+ tx_buffer[offset++] = 168;
+ tx_buffer[offset++] = 4;
+ tx_buffer[offset++] = 1;
+
+ // Send
+ sendto(sock, tx_buffer, offset, 0, (struct sockaddr *)&client_addr, client_addr_len);
+ }
+ }
+ }
+ close(sock);
+ vTaskDelete(NULL);
+}
+
+// -------------------------------------------------------------------------
+// Init Functions
+// -------------------------------------------------------------------------
+static void wifi_init_softap(void) {
+ ESP_LOGI(TAG, "Starting SoftAP Provisioning Mode...");
+
+ // Explicitly set mode to NULL first to ensure clean state
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
+
+ wifi_config_t wifi_config = {
+ .ap = {
+ .ssid = AP_SSID,
+ .ssid_len = strlen(AP_SSID),
+ .channel = 1,
+ .password = AP_PASS,
+ .max_connection = 4,
+ .authmode = WIFI_AUTH_OPEN
+ },
+ };
+ if (strlen(AP_PASS) == 0) {
+ wifi_config.ap.authmode = WIFI_AUTH_OPEN;
+ }
+
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
+ ESP_ERROR_CHECK(esp_wifi_start());
+ ESP_LOGI(TAG, "SoftAP Started. SSID: %s", AP_SSID);
+
+ // Start DNS Server Task
+ xTaskCreate(dns_server_task, "dns_task", 2048, NULL, 5, NULL);
+}
+
+static void wifi_init_station(const char* ssid, const char* pass) {
+ ESP_LOGI(TAG, "Starting Station Mode. Connecting to %s...", ssid);
+
+ wifi_config_t wifi_config = {
+ .sta = {
+ .threshold.authmode = WIFI_AUTH_WPA2_PSK,
+ },
+ };
+ strncpy((char*)wifi_config.sta.ssid, ssid, sizeof(wifi_config.sta.ssid));
+ strncpy((char*)wifi_config.sta.password, pass, sizeof(wifi_config.sta.password));
+
+ ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
+ ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wifi_config));
+ ESP_ERROR_CHECK(esp_wifi_start());
+ ESP_ERROR_CHECK(esp_wifi_set_ps(WIFI_PS_NONE)); // Maintain low jitter optimization
+}
+
+// -------------------------------------------------------------------------
+// HTTP Handler: /api/save_wifi
+// -------------------------------------------------------------------------
+static esp_err_t save_wifi_handler(httpd_req_t *req) {
+ char buf[200]; // reasonable limit for JSON
+ 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';
+
+ cJSON *root = cJSON_Parse(buf);
+ if (root) {
+ cJSON *ssid_item = cJSON_GetObjectItem(root, "ssid");
+ cJSON *pass_item = cJSON_GetObjectItem(root, "password");
+
+ if (cJSON_IsString(ssid_item) && (cJSON_IsString(pass_item))) {
+ const char* ssid = ssid_item->valuestring;
+ const char* pass = pass_item->valuestring;
+
+ ESP_LOGI(TAG, "Saving WiFi Credentials: SSID=%s", ssid);
+
+ // Save to NVS
+ nvs_handle_t nvs_handle;
+ esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle);
+ if (err == ESP_OK) {
+ nvs_set_str(nvs_handle, NVS_KEY_SSID, ssid);
+ nvs_set_str(nvs_handle, NVS_KEY_PASS, pass);
+ nvs_commit(nvs_handle);
+ nvs_close(nvs_handle);
+
+ httpd_resp_send(req, "Saved. Rebooting...", HTTPD_RESP_USE_STRLEN);
+
+ // Give time for response to flush
+ vTaskDelay(pdMS_TO_TICKS(1000));
+ esp_restart();
+ } else {
+ ESP_LOGE(TAG, "NVS Open Failed");
+ httpd_resp_send_500(req);
+ }
+ }
+ cJSON_Delete(root);
+ }
+ return ESP_OK;
+}
+
+static const httpd_uri_t uri_save_wifi = {
+ .uri = "/api/save_wifi",
+ .method = HTTP_POST,
+ .handler = save_wifi_handler,
+ .user_ctx = NULL
+};
+
+// -------------------------------------------------------------------------
+// Public API
+// -------------------------------------------------------------------------
+
+void wifi_manager_register_uri(httpd_handle_t server) {
+ s_server_handle = server;
+ if (s_server_handle) {
+ httpd_register_uri_handler(s_server_handle, &uri_save_wifi);
+ }
+}
+
+bool wifi_manager_init_wifi(void) {
+ s_wifi_event_group = xEventGroupCreate();
+
+ // 1. Netif Init (Common)
+ ESP_ERROR_CHECK(esp_netif_init());
+ ESP_ERROR_CHECK(esp_event_loop_create_default());
+
+ esp_netif_t *sta_netif = esp_netif_create_default_wifi_sta();
+ esp_netif_set_hostname(sta_netif, "esp-scope");
+ esp_netif_create_default_wifi_ap();
+
+ wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
+ ESP_ERROR_CHECK(esp_wifi_init(&cfg));
+
+ esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, NULL);
+ esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, NULL);
+
+ // 2. Check NVS
+ nvs_handle_t nvs_handle;
+ char ssid[33] = {0};
+ char pass[64] = {0};
+ size_t ssid_len = sizeof(ssid);
+ size_t pass_len = sizeof(pass);
+ bool has_creds = false;
+
+ esp_err_t err = nvs_open(NVS_NAMESPACE, NVS_READONLY, &nvs_handle);
+ if (err == ESP_OK) {
+ if (nvs_get_str(nvs_handle, NVS_KEY_SSID, ssid, &ssid_len) == ESP_OK &&
+ nvs_get_str(nvs_handle, NVS_KEY_PASS, pass, &pass_len) == ESP_OK) {
+ has_creds = true;
+ }
+ nvs_close(nvs_handle);
+ }
+
+ // 3. Start Mode
+ if (has_creds && strlen(ssid) > 0) {
+ wifi_init_station(ssid, pass);
+ return false;
+ } else {
+ wifi_init_softap();
+ return true;
+ }
+}
+
+void wifi_manager_erase_config(void) {
+ ESP_LOGW(TAG, "Erasing WiFi Config from NVS...");
+ nvs_handle_t nvs_handle;
+ if (nvs_open(NVS_NAMESPACE, NVS_READWRITE, &nvs_handle) == ESP_OK) {
+ nvs_erase_all(nvs_handle);
+ nvs_commit(nvs_handle);
+ nvs_close(nvs_handle);
+ }
+}
diff --git a/main/wifi_manager.h b/main/wifi_manager.h
new file mode 100644
index 0000000..2f6ff47
--- /dev/null
+++ b/main/wifi_manager.h
@@ -0,0 +1,16 @@
+#ifndef WIFI_MANAGER_H
+#define WIFI_MANAGER_H
+
+#include "esp_err.h"
+#include "esp_http_server.h"
+
+// Initialize Wi-Fi (Checks NVS, starts AP or STA)
+bool wifi_manager_init_wifi(void);
+
+// Register provisioning URI handlers to the server
+void wifi_manager_register_uri(httpd_handle_t server);
+
+// Helper to erase NVS credentials (can be called by button handler)
+void wifi_manager_erase_config(void);
+
+#endif // WIFI_MANAGER_H