Add soft-ap mode, remove wifi config.

Device defaukts to soft-ap mode. User can use the "WIFI" button to set wifi credentials. Pressing "boot" for 1 second resets to soft-ap mode.

Re-work UI to fill screen.
This commit is contained in:
Matt
2025-12-14 11:23:18 +00:00
parent 855d287e1d
commit 46514b5c76
7 changed files with 702 additions and 195 deletions

View File

@@ -1,5 +1,5 @@
idf_component_register(SRCS "main.c" idf_component_register(SRCS "main.c" "wifi_manager.c"
INCLUDE_DIRS "." INCLUDE_DIRS "."
EMBED_TXTFILES "index.html" "index.js" EMBED_TXTFILES "index.html" "index.js"
REQUIRES esp_http_server esp_adc nvs_flash esp_wifi driver esp_netif freertos esp_event) REQUIRES esp_http_server esp_adc nvs_flash esp_wifi driver esp_netif freertos esp_event esp_timer)

View File

@@ -5,16 +5,11 @@ menu "espScope Configuration"
help help
LED IO pin LED IO pin
config ESP_WIFI_SSID config BSP_CONFIG_GPIO
string "WiFi SSID" int "Factory Reset Pin (GPIO)"
default "myssid" default 9
help help
Default SSID (network name) to connect to. You can change this onece connected via the web interface. GPIO pin number to hold low on boot to erase NVS (Factory Reset).
Default is 9 (BOOT button on Seeed XIAO ESP32C6).
config ESP_WIFI_PASSWORD
string "WiFi Password"
default "mypassword"
help
Default SSID password. You can change this onece connected via the web interface.
endmenu endmenu

View File

@@ -3,180 +3,316 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>espScope</title> <title>espScope</title>
<style> <style>
* {
box-sizing: border-box;
}
body { body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: #1a1a1a; background: #1a1a1a;
color: #e0e0e0; color: #e0e0e0;
margin: 0; margin: 0;
padding: 20px; padding: 0;
height: 100vh;
/* Fallback */
height: 100dvh;
/* Mobile viewport fix */
display: flex;
flex-direction: column;
overflow: hidden;
} }
.container { .container {
max-width: 1000px; display: flex;
margin: 0 auto; flex-direction: column;
height: 100%;
width: 100%;
max-width: 100%;
} }
h1 { /* Top Section: Chart (Fills available space) */
color: #4ade80; .chart-section {
flex-grow: 1;
position: relative;
background: #000;
min-height: 0;
/* Important for flex child with specific height content */
display: flex;
} }
.card { .canvas-container {
flex-grow: 1;
position: relative;
height: 100%;
width: 100%;
border: 8px solid #333;
}
canvas {
width: 100%;
height: 100%;
display: block;
}
/* Trigger Slider floating on right */
#triggerLevel {
position: absolute;
right: 0;
top: 0;
bottom: 0;
height: 100%;
width: 20px;
margin: 0;
appearance: slider-vertical;
background: transparent;
z-index: 10;
}
/* Status Text Overlay */
.status-overlay {
position: absolute;
top: 10px;
right: 20px;
pointer-events: none;
text-shadow: 1px 1px 2px black;
}
.status {
font-family: monospace;
color: #ccc;
}
#reconnectBtn {
pointer-events: auto;
margin-top: 5px;
}
/* Bottom Section: Controls */
.controls-section {
background: #2d2d2d; background: #2d2d2d;
border-radius: 8px; padding: 10px;
padding: 20px; padding-bottom: calc(10px + env(safe-area-inset-bottom));
margin-bottom: 20px; /* iOS Home Bar fix */
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); border-top: 1px solid #444;
flex-shrink: 0;
z-index: 20;
} }
.controls { .controls {
display: grid; display: flex;
grid-template-columns: repeat(auto-fit, minmax(8em, 1fr)); flex-direction: row;
gap: 15px; align-items: center;
/* gap: 15px; */
justify-content: space-around;
flex-wrap: nowrap;
overflow-x: auto;
}
.control-group {
display: flex;
flex-direction: row;
align-items: center;
gap: 5px;
white-space: nowrap;
margin-right: 15px;
} }
label { label {
display: block; font-size: 0.85em;
margin-bottom: 5px;
font-size: 0.9em;
color: #bbb; color: #bbb;
} margin-right: 4px;
label {
width: 10em;
} }
input, input,
select { select {
width: 6em;
padding: 8px;
background: #3d3d3d; background: #3d3d3d;
border: 1px solid #555; border: 1px solid #555;
color: white; color: white;
border-radius: 4px; border-radius: 4px;
padding: 6px;
font-size: 0.9em;
width: 5em;
}
select {
width: auto;
}
/* Buttons Group */
.btn-group {
display: flex;
gap: 8px;
margin-left: auto;
} }
button { button {
background: #4ade80; background: #4ade80;
color: #1a1a1a; color: #1a1a1a;
border: none; border: none;
padding: 10px 20px; padding: 8px 12px;
border-radius: 4px; border-radius: 4px;
font-weight: bold; font-weight: bold;
cursor: pointer; cursor: pointer;
transition: background 0.2s; white-space: nowrap;
font-size: 0.9em;
} }
button:hover { button:hover {
background: #22c55e; background: #22c55e;
} }
canvas { button.secondary {
width: 100%; background: #444;
height: 400px; color: #ccc;
background: #000;
border-radius: 4px;
}
.status {
font-family: monospace;
margin-top: 10px;
}
.info {
font-family: monospace;
margin-top: 10px;
} }
#deltaPanel { #deltaPanel {
position: absolute; position: absolute;
background: #2d2d2d; background: rgba(45, 45, 45, 0.9);
color: #e0e0e0; color: #e0e0e0;
padding: 5px 10px; padding: 5px 10px;
border-radius: 4px; border-radius: 4px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3); pointer-events: none;
display: none; display: none;
z-index: 50;
} }
#triggerLevel { /* Modal Styles */
/* appearance: slider-vertical; */ .modal {
position: relative; display: none;
height: 408px; position: fixed;
top: -16px; top: 0;
margin-left: 10px; left: 0;
width: 1px; width: 100%;
writing-mode: vertical-lr; height: 100%;
direction: rtl; background: rgba(0, 0, 0, 0.8);
z-index: 1000;
align-items: center;
justify-content: center;
} }
#resetBtn, #powerOff { .modal-content {
background: #222; background: #2d2d2d;
color: #e66; padding: 20px;
border: 1px solid; border-radius: 8px;
font-size: 0.8rem; width: 300px;
height: 4em; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.5);
width: 6em; }
vertical-align: middle;
.modal-header {
font-size: 1.2em;
margin-bottom: 15px;
color: #4ade80;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
width: 100%;
display: block;
margin-bottom: 5px;
}
.form-group input {
width: 100%;
}
.modal-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
} }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="card"> <!-- Top Section: Chart -->
<div style="display: flex; position: relative;"> <div class="chart-section">
<div class="canvas-container">
<canvas id="adcChart"></canvas> <canvas id="adcChart"></canvas>
<input type="range" id="triggerLevel" orient="vertical" min="0" max="4096" value="2048">
</div> </div>
<div style="display:flex; justify-content:space-between; align-items:center;">
<!-- Floating Trigger Slider -->
<input type="range" id="triggerLevel" orient="vertical" min="0" max="4096" value="2048">
<!-- Overlay Status -->
<div class="status-overlay">
<div class="status" id="status">Connecting...</div> <div class="status" id="status">Connecting...</div>
<button id="reconnectBtn" <button id="reconnectBtn"
style="display:none; background:#eab308; color:#000; padding:4px 8px; font-size:0.8rem;">Reconnect style="display:none; background:#eab308; color:#000; padding:4px 8px; font-size:0.8rem;">Reconnect</button>
Now</button>
<div class="info" id="info"></div> <div class="info" id="info"></div>
</div> </div>
<!-- Mouseover Info Tooltip -->
<div id="deltaPanel"></div>
</div> </div>
<div class="card"> <!-- Bottom Section: Controls -->
<div class="controls-section">
<div class="controls"> <div class="controls">
<div>
<label>Sample Rate (Hz)</label> <div class="control-group">
<label>Rate(Hz)</label>
<input type="number" id="sampleRate" value="10000" min="1" max="83333"> <input type="number" id="sampleRate" value="10000" min="1" max="83333">
</div> </div>
<div style="display: none;">
<label>Bit Width</label> <div class="control-group" style="display: none;">
<label>BitWidth</label>
<select id="bitWidth"> <select id="bitWidth">
<option value="12" selected>12-bit</option> <option value="12" selected>12</option>
<option value="11">11-bit</option> <option value="11">11</option>
<option value="10">10-bit</option> <option value="10">10</option>
<option value="9">9-bit</option> <option value="9">9</option>
</select> </select>
</div> </div>
<div>
<label>Attenuation</label> <div class="control-group">
<label>Atten</label>
<select id="atten"> <select id="atten">
<option value="0">0 dB</option> <option value="0">0dB</option>
<option value="1">2.5 dB</option> <option value="1">2.5dB</option>
<option value="2">6 dB</option> <option value="2">6dB</option>
<option value="3" selected>11 dB</option> <option value="3" selected>11dB</option>
</select> </select>
</div> </div>
<div>
<div> <div class="control-group">
<label>Test Hz</label> <label>TestHz</label>
<input type="number" id="testHz" value="100" min="1" max="10000"> <input type="number" id="testHz" value="100" min="1" max="10000">
</div>
</div> </div>
<div>
<button id="resetBtn">Reset</button> <div class="btn-group">
<button id="powerOff">Power off</button> <button id="resetBtn" class="secondary">Rst</button>
<button id="wifiBtn" class="secondary">Wifi</button>
<button id="powerOff" class="secondary">Off</button>
</div> </div>
</div> </div>
</div> </div>
</div>
<div id="deltaPanel"></div> <!-- Wifi Modal -->
<div id="wifiModal" class="modal">
<div class="modal-content">
<div class="modal-header">Configure Wi-Fi</div>
<div class="form-group">
<label>SSID</label>
<input type="text" id="wifiSsid" placeholder="Network Name">
</div>
<div class="form-group">
<label>Password</label>
<input type="password" id="wifiPass" placeholder="Password">
</div>
<div class="modal-actions">
<button id="closeWifi" style="background:#555; color:#fff;">Cancel</button>
<button id="saveWifi">Save</button>
</div>
</div>
</div> </div>
<script src="index.js"></script> <script src="index.js"></script>

View File

@@ -461,5 +461,44 @@ document.getElementById('resetBtn').addEventListener('click', () => {
window.location.reload(); window.location.reload();
}); });
document.getElementById('powerOff').addEventListener('click', () => window.location.href = "/poweroff"); 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(); setParams();
connect(); connect();

View File

@@ -14,29 +14,20 @@
#include "esp_sleep.h" #include "esp_sleep.h"
#include "esp_system.h" #include "esp_system.h"
#include "esp_wifi.h" #include "esp_wifi.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h" #include "freertos/event_groups.h"
#include "freertos/task.h" #include "freertos/task.h"
#include "wifi_manager.h"
#include "nvs_flash.h" #include "nvs_flash.h"
// Tag for logging // Tag for logging
static const char* TAG = "ESP-SCOPE"; 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 #define ESP_MAXIMUM_RETRY 5
/* FreeRTOS event group to signal when we are connected*/ /* 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 // Embedded index.html
extern const uint8_t index_html_start[] asm("_binary_index_html_start"); 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"); extern const uint8_t index_js_end[] asm("_binary_index_js_end");
// Forward declarations // Forward declarations
static void wifi_init_sta(void);
static void start_webserver(void); static void start_webserver(void);
// ADC Configuration // ADC Configuration
@@ -62,7 +52,7 @@ static void start_webserver(void);
* Web Server Configuration * Web Server Configuration
*/ */
static httpd_handle_t s_server = NULL; static httpd_handle_t s_server = NULL;
static bool is_ap = false;
#define ADC_READ_LEN 4096 #define ADC_READ_LEN 4096
static adc_continuous_handle_t adc_handle = NULL; 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; static uint16_t s_test_hz = 100;
// Forward declarations // 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, static void continuous_adc_init(adc_channel_t* channel, uint8_t channel_num,
adc_continuous_handle_t* out_handle); adc_continuous_handle_t* out_handle);
static esp_err_t ws_handler(httpd_req_t* req); 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() { 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) { 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); gpio_set_level(CONFIG_LED_BUILTIN, 1);
vTaskDelay(pdMS_TO_TICKS(s_ws_client_fd == -1 ? 900 : 200)); vTaskDelay(pdMS_TO_TICKS(s_ws_client_fd == -1 ? 900 : 200));
gpio_set_level(CONFIG_LED_BUILTIN, 0); 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) { void app_main(void) {
@@ -318,7 +331,6 @@ void app_main(void) {
gpio_set_level(CONFIG_LED_BUILTIN, 0); gpio_set_level(CONFIG_LED_BUILTIN, 0);
#endif #endif
ESP_LOGI(TAG, "ESP_WIFI_MODE_STA");
/* Board-specific WiFi init (if any) */ /* Board-specific WiFi init (if any) */
// Seeed XIAO ESP32C6: Configure GPIO 3 and GPIO 14 as outputs // Seeed XIAO ESP32C6: Configure GPIO 3 and GPIO 14 as outputs
gpio_config_t board_io_conf = { gpio_config_t board_io_conf = {
@@ -334,82 +346,21 @@ void app_main(void) {
gpio_set_level(14, 0); gpio_set_level(14, 0);
/* end board-specific WiFi init (if any) */ /* end board-specific WiFi init (if any) */
wifi_init_sta(); is_ap = wifi_manager_init_wifi();
start_test_signal(100); start_test_signal(100);
xTaskCreate(adc_read_task, "adc_read_task", 8192 + ADC_READ_LEN, NULL, 5, NULL); xTaskCreate(adc_read_task, "adc_read_task", 8192 + ADC_READ_LEN, NULL, 5, NULL);
// Wait for WiFi connection // Wait for WiFi connection (Not strict blocking anymore, manager handles it)
EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, // But let's keep it to print status
WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, // Note: s_wifi_event_group is local to this file, we removed it in place of manager's.
pdFALSE, pdFALSE, portMAX_DELAY); // 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) { start_webserver();
ESP_LOGI(TAG, "connected to ap SSID:%s", ESP_WIFI_SSID); show_status_led();
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");
}
} }
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 * WebSocket Handler
@@ -562,6 +513,16 @@ static esp_err_t power_handler(httpd_req_t* req) {
static const httpd_uri_t uri_power = { static const httpd_uri_t uri_power = {
.uri = "/poweroff", .method = HTTP_GET, .handler = power_handler, .user_ctx = NULL}; .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) { static void start_webserver(void) {
httpd_config_t config = HTTPD_DEFAULT_CONFIG(); httpd_config_t config = HTTPD_DEFAULT_CONFIG();
config.lru_purge_enable = true; 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_index_js);
httpd_register_uri_handler(s_server, &uri_ws); httpd_register_uri_handler(s_server, &uri_ws);
httpd_register_uri_handler(s_server, &uri_params); 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); 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 { } else {
ESP_LOGI(TAG, "Error starting server!"); ESP_LOGI(TAG, "Error starting server!");
} }

354
main/wifi_manager.c Normal file
View File

@@ -0,0 +1,354 @@
#include "wifi_manager.h"
#include <string.h>
#include "esp_mac.h"
#include <sys/param.h>
#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);
}
}

16
main/wifi_manager.h Normal file
View File

@@ -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