feat: validate credentials, persist them, and switch without rebooting
This commit is contained in:
+146
-54
@@ -5,6 +5,7 @@
|
||||
|
||||
#include "app_config.h"
|
||||
#include "application.h"
|
||||
#include "captive_portal.h"
|
||||
#include "esp_check.h"
|
||||
#include "esp_event.h"
|
||||
#include "esp_heap_caps.h"
|
||||
@@ -18,24 +19,21 @@
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/portmacro.h"
|
||||
#include "http_api.h"
|
||||
#include "network_credential_store.h"
|
||||
#include "network_state.h"
|
||||
#include "nvs_flash.h"
|
||||
#include "sync_service.h"
|
||||
|
||||
#if __has_include("wifi_config.h")
|
||||
#include "wifi_config.h"
|
||||
#define WIFI_CONFIG_AVAILABLE 1
|
||||
#else
|
||||
#define WIFI_CONFIG_AVAILABLE 0
|
||||
#endif
|
||||
#include "lwip/sockets.h"
|
||||
|
||||
static const char *const kLogTag = "battleship";
|
||||
static const char *const kLittlefsBasePath = "/littlefs";
|
||||
static const char *const kLittlefsPartitionLabel = "littlefs";
|
||||
static const size_t kFileChunkBytes = 1024U;
|
||||
static const uint8_t kHttpMaxOpenSockets = 12U;
|
||||
enum { kHttpMaxOpenSockets = 12U, kFallbackMaxConnections = 10U, kDnsPort = 53U, kScanResultLimit = 12U };
|
||||
|
||||
typedef struct { bool configured; bool connected; bool retry_scheduled; uint8_t reconnect_attempt; } wifi_state_t;
|
||||
typedef struct { bool configured; bool connected; bool fallback_active; } wifi_state_t;
|
||||
static portMUX_TYPE s_wifi_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static portMUX_TYPE s_network_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static portMUX_TYPE s_diagnostics_lock = portMUX_INITIALIZER_UNLOCKED;
|
||||
static wifi_state_t s_wifi_state = {0};
|
||||
static bool s_littlefs_mounted;
|
||||
@@ -46,9 +44,11 @@ static sync_service_t s_sync;
|
||||
static uint16_t s_rejected_input;
|
||||
static esp_timer_handle_t s_sync_timer;
|
||||
static esp_timer_handle_t s_bot_timer;
|
||||
#if WIFI_CONFIG_AVAILABLE
|
||||
static esp_timer_handle_t s_reconnect_timer;
|
||||
#endif
|
||||
static esp_timer_handle_t s_network_timer;
|
||||
static network_manager_t s_network_manager;
|
||||
static bool s_scan_pending;
|
||||
static bool s_scan_ready;
|
||||
static bool s_dns_running;
|
||||
|
||||
static uint32_t platform_random(void *unused) { (void)unused; return esp_random(); }
|
||||
|
||||
@@ -75,6 +75,7 @@ static wifi_state_t wifi_state_snapshot(void) {
|
||||
}
|
||||
|
||||
static const char *wifi_state_name(const wifi_state_t *state) {
|
||||
if (state->fallback_active) return "fallback";
|
||||
if (!state->configured) return "not_configured";
|
||||
return state->connected ? "connected" : "connecting";
|
||||
}
|
||||
@@ -255,67 +256,114 @@ static esp_err_t websocket_handler(httpd_req_t *request) {
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
#if WIFI_CONFIG_AVAILABLE
|
||||
static void reconnect_timer_callback(void *unused) {
|
||||
(void)unused;
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.retry_scheduled = false; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
if (esp_wifi_connect() != ESP_OK) ESP_LOGW(kLogTag, "wifi connect request failed");
|
||||
static bool fallback_active(void) {
|
||||
const wifi_state_t state = wifi_state_snapshot();
|
||||
return state.fallback_active;
|
||||
}
|
||||
|
||||
static void schedule_reconnect(uint32_t delay_ms) {
|
||||
bool schedule = false;
|
||||
portENTER_CRITICAL(&s_wifi_lock);
|
||||
if (s_wifi_state.configured && !s_wifi_state.connected && !s_wifi_state.retry_scheduled) { s_wifi_state.retry_scheduled = true; schedule = true; }
|
||||
portEXIT_CRITICAL(&s_wifi_lock);
|
||||
if (schedule && esp_timer_start_once(s_reconnect_timer, (uint64_t)delay_ms * 1000U) != ESP_OK) {
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.retry_scheduled = false; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
static void captive_dns_task(void *unused) {
|
||||
(void)unused;
|
||||
const int socket_fd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
|
||||
if (socket_fd < 0) { ESP_LOGW(kLogTag, "captive DNS unavailable"); s_dns_running = false; vTaskDelete(NULL); return; }
|
||||
struct sockaddr_in local = {.sin_family = AF_INET, .sin_port = htons(kDnsPort), .sin_addr.s_addr = htonl(INADDR_ANY)};
|
||||
if (bind(socket_fd, (struct sockaddr *)&local, sizeof(local)) != 0) { close(socket_fd); s_dns_running = false; vTaskDelete(NULL); return; }
|
||||
const struct timeval timeout = {.tv_sec = 1, .tv_usec = 0};
|
||||
setsockopt(socket_fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout));
|
||||
const uint8_t address[] = {192U, 168U, 4U, 1U};
|
||||
while (fallback_active()) {
|
||||
uint8_t query[256]; uint8_t response[272]; struct sockaddr_in client = {0}; socklen_t client_length = sizeof(client);
|
||||
const int length = recvfrom(socket_fd, query, sizeof(query), 0, (struct sockaddr *)&client, &client_length);
|
||||
size_t response_length = 0U;
|
||||
if (length > 0 && captive_portal_dns_response(query, (size_t)length, address, response, sizeof(response), &response_length)) sendto(socket_fd, response, response_length, 0, (struct sockaddr *)&client, client_length);
|
||||
}
|
||||
close(socket_fd); s_dns_running = false; vTaskDelete(NULL);
|
||||
}
|
||||
|
||||
static void start_captive_dns(void) {
|
||||
if (s_dns_running) return;
|
||||
s_dns_running = true;
|
||||
if (xTaskCreate(captive_dns_task, "captive_dns", 3072U, NULL, 3U, NULL) != pdPASS) { s_dns_running = false; ESP_LOGW(kLogTag, "captive DNS task unavailable"); }
|
||||
}
|
||||
|
||||
static void enable_fallback_ap(void) {
|
||||
wifi_config_t access_point = {0};
|
||||
snprintf((char *)access_point.ap.ssid, sizeof(access_point.ap.ssid), "%s", "Battleship-open");
|
||||
access_point.ap.ssid_len = strlen((const char *)access_point.ap.ssid);
|
||||
access_point.ap.channel = 1U; access_point.ap.max_connection = kFallbackMaxConnections; access_point.ap.authmode = WIFI_AUTH_OPEN;
|
||||
if (esp_wifi_set_config(WIFI_IF_AP, &access_point) != ESP_OK || esp_wifi_set_mode(WIFI_MODE_APSTA) != ESP_OK) { ESP_LOGW(kLogTag, "fallback access point unavailable"); return; }
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.fallback_active = true; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
start_captive_dns();
|
||||
}
|
||||
|
||||
static void disable_fallback_ap(void) {
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.fallback_active = false; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
if (esp_wifi_set_mode(WIFI_MODE_STA) != ESP_OK) ESP_LOGW(kLogTag, "fallback access point stop failed");
|
||||
}
|
||||
|
||||
static void apply_network_action(network_action_t action) {
|
||||
if (action == NETWORK_ACTION_FALLBACK) enable_fallback_ap();
|
||||
else if (action == NETWORK_ACTION_CONNECT && esp_wifi_connect() != ESP_OK) ESP_LOGW(kLogTag, "saved network connection request failed");
|
||||
}
|
||||
|
||||
static network_action_t network_event_action(bool connected) {
|
||||
network_action_t action;
|
||||
portENTER_CRITICAL(&s_network_lock);
|
||||
action = connected ? network_manager_connected(&s_network_manager) : network_manager_disconnected(&s_network_manager, (uint32_t)(esp_timer_get_time() / 1000U));
|
||||
portEXIT_CRITICAL(&s_network_lock);
|
||||
return action;
|
||||
}
|
||||
|
||||
static void network_timer_callback(void *unused) {
|
||||
(void)unused;
|
||||
network_action_t action;
|
||||
portENTER_CRITICAL(&s_network_lock); action = network_manager_tick(&s_network_manager, (uint32_t)(esp_timer_get_time() / 1000U)); portEXIT_CRITICAL(&s_network_lock);
|
||||
apply_network_action(action);
|
||||
}
|
||||
|
||||
static void wifi_event_handler(void *argument, esp_event_base_t event_base, int32_t event_id, void *event_data) {
|
||||
(void)argument; (void)event_data;
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) schedule_reconnect(0U);
|
||||
if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) apply_network_action(network_event_action(false));
|
||||
else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
|
||||
uint8_t attempt;
|
||||
portENTER_CRITICAL(&s_wifi_lock);
|
||||
if (s_wifi_state.reconnect_attempt < 5U) ++s_wifi_state.reconnect_attempt;
|
||||
attempt = s_wifi_state.reconnect_attempt; s_wifi_state.connected = false;
|
||||
portEXIT_CRITICAL(&s_wifi_lock);
|
||||
schedule_reconnect(1000U << attempt);
|
||||
} else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.connected = true; s_wifi_state.reconnect_attempt = 0U; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.connected = false; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
apply_network_action(network_event_action(false));
|
||||
} else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_SCAN_DONE) { s_scan_pending = false; s_scan_ready = true; }
|
||||
else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
|
||||
network_event_action(true);
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.connected = true; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
if (fallback_active()) disable_fallback_ap();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static esp_err_t start_wifi(void) {
|
||||
#if !WIFI_CONFIG_AVAILABLE
|
||||
ESP_LOGE(kLogTag, "wifi is not configured; copy include/wifi_config.h.example to include/wifi_config.h");
|
||||
return ESP_ERR_INVALID_STATE;
|
||||
#else
|
||||
network_profile_t profile = {0};
|
||||
const bool has_profile = network_credential_store_load(&profile);
|
||||
wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT();
|
||||
wifi_config_t station_config = {0};
|
||||
const esp_timer_create_args_t timer_args = {.callback = reconnect_timer_callback, .name = "wifi_reconnect"};
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.configured = true; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
esp_netif_create_default_wifi_sta();
|
||||
const uint32_t now_ms = (uint32_t)(esp_timer_get_time() / 1000U);
|
||||
portENTER_CRITICAL(&s_network_lock); const network_action_t initial_action = network_manager_init(&s_network_manager, has_profile ? &profile : NULL, now_ms); portEXIT_CRITICAL(&s_network_lock);
|
||||
portENTER_CRITICAL(&s_wifi_lock); s_wifi_state.configured = has_profile; portEXIT_CRITICAL(&s_wifi_lock);
|
||||
esp_netif_create_default_wifi_sta(); esp_netif_create_default_wifi_ap();
|
||||
ESP_RETURN_ON_ERROR(esp_wifi_init(&init_config), kLogTag, "wifi initialization failed");
|
||||
ESP_RETURN_ON_ERROR(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, wifi_event_handler, NULL, NULL), kLogTag, "wifi event registration failed");
|
||||
ESP_RETURN_ON_ERROR(esp_event_handler_instance_register(IP_EVENT, IP_EVENT_STA_GOT_IP, wifi_event_handler, NULL, NULL), kLogTag, "ip event registration failed");
|
||||
ESP_RETURN_ON_ERROR(esp_timer_create(&timer_args, &s_reconnect_timer), kLogTag, "wifi timer creation failed");
|
||||
snprintf((char *)station_config.sta.ssid, sizeof(station_config.sta.ssid), "%s", WIFI_CONFIG_SSID);
|
||||
snprintf((char *)station_config.sta.password, sizeof(station_config.sta.password), "%s", WIFI_CONFIG_PASSWORD);
|
||||
station_config.sta.threshold.authmode = WIFI_AUTH_WPA2_PSK;
|
||||
station_config.sta.pmf_cfg.capable = true;
|
||||
ESP_RETURN_ON_ERROR(esp_wifi_set_mode(WIFI_MODE_STA), kLogTag, "wifi mode setup failed");
|
||||
ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &station_config), kLogTag, "wifi configuration failed");
|
||||
return esp_wifi_start();
|
||||
#endif
|
||||
const esp_timer_create_args_t timer_args = {.callback = network_timer_callback, .name = "network_state"};
|
||||
ESP_RETURN_ON_ERROR(esp_timer_create(&timer_args, &s_network_timer), kLogTag, "network timer creation failed");
|
||||
if (has_profile) {
|
||||
memcpy(station_config.sta.ssid, profile.ssid, kNetworkSsidBytes);
|
||||
memcpy(station_config.sta.password, profile.password, kNetworkPasswordBytes);
|
||||
ESP_RETURN_ON_ERROR(esp_wifi_set_config(WIFI_IF_STA, &station_config), kLogTag, "saved network configuration failed");
|
||||
}
|
||||
ESP_RETURN_ON_ERROR(esp_wifi_set_mode(initial_action == NETWORK_ACTION_FALLBACK ? WIFI_MODE_AP : WIFI_MODE_STA), kLogTag, "wifi mode setup failed");
|
||||
ESP_RETURN_ON_ERROR(esp_wifi_start(), kLogTag, "wifi start failed");
|
||||
ESP_RETURN_ON_ERROR(esp_timer_start_periodic(s_network_timer, 1000000U), kLogTag, "network timer start failed");
|
||||
if (initial_action == NETWORK_ACTION_FALLBACK) enable_fallback_ap();
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static const char *mime_type_for_path(const char *path) {
|
||||
if (strcmp(path, "/index.html") == 0) return "text/html; charset=utf-8";
|
||||
if (strcmp(path, "/index.html") == 0 || strcmp(path, "/setup.html") == 0) return "text/html; charset=utf-8";
|
||||
if (strcmp(path, "/ship-sprite.svg") == 0) return "image/svg+xml";
|
||||
return strcmp(path, "/styles.css") == 0 ? "text/css; charset=utf-8" : "application/javascript; charset=utf-8";
|
||||
return strcmp(path, "/styles.css") == 0 || strcmp(path, "/setup.css") == 0 ? "text/css; charset=utf-8" : "application/javascript; charset=utf-8";
|
||||
}
|
||||
|
||||
static bool request_accepts_gzip(httpd_req_t *request) {
|
||||
@@ -348,6 +396,46 @@ static esp_err_t send_static_file(httpd_req_t *request, const char *asset_path)
|
||||
return result == ESP_OK ? httpd_resp_send_chunk(request, NULL, 0U) : result;
|
||||
}
|
||||
|
||||
static esp_err_t redirect_to_setup(httpd_req_t *request) {
|
||||
httpd_resp_set_status(request, "302 Found");
|
||||
httpd_resp_set_hdr(request, "Location", "/setup");
|
||||
httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||
return httpd_resp_send(request, NULL, 0U);
|
||||
}
|
||||
|
||||
static esp_err_t setup_handler(httpd_req_t *request) {
|
||||
if (!s_littlefs_mounted) {
|
||||
httpd_resp_set_status(request, "503 Service Unavailable");
|
||||
return httpd_resp_send(request, "LittleFS is unavailable", HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
return send_static_file(request, "/setup.html");
|
||||
}
|
||||
|
||||
static size_t append_scan_name(char *output, size_t capacity, size_t length, const uint8_t *ssid) {
|
||||
if (length >= capacity) return length;
|
||||
for (size_t index = 0U; index < 32U && ssid[index] != '\0' && length + 1U < capacity; ++index) {
|
||||
const uint8_t value = ssid[index]; output[length++] = value >= 32U && value <= 126U ? (char)value : '?';
|
||||
}
|
||||
output[length++] = '\n'; output[length] = '\0'; return length;
|
||||
}
|
||||
|
||||
static esp_err_t setup_scan_handler(httpd_req_t *request) {
|
||||
if (!fallback_active()) { httpd_resp_set_status(request, "409 Conflict"); return httpd_resp_send(request, "fallback access point is inactive", HTTPD_RESP_USE_STRLEN); }
|
||||
if (!s_scan_pending && !s_scan_ready) {
|
||||
const esp_err_t result = esp_wifi_scan_start(NULL, false);
|
||||
if (result != ESP_OK) { httpd_resp_set_status(request, "503 Service Unavailable"); return httpd_resp_send(request, "scan unavailable", HTTPD_RESP_USE_STRLEN); }
|
||||
s_scan_pending = true;
|
||||
}
|
||||
httpd_resp_set_type(request, "text/plain; charset=utf-8"); httpd_resp_set_hdr(request, "Cache-Control", "no-store");
|
||||
if (s_scan_pending) return httpd_resp_send(request, "scanning\n", HTTPD_RESP_USE_STRLEN);
|
||||
wifi_ap_record_t records[kScanResultLimit] = {0}; uint16_t count = kScanResultLimit;
|
||||
if (esp_wifi_scan_get_ap_records(&count, records) != ESP_OK) { httpd_resp_set_status(request, "503 Service Unavailable"); return httpd_resp_send(request, "scan unavailable", HTTPD_RESP_USE_STRLEN); }
|
||||
s_scan_ready = false;
|
||||
char names[(kNetworkSsidBytes + 1U) * kScanResultLimit + 1U] = {0}; size_t length = 0U;
|
||||
for (uint16_t index = 0U; index < count; ++index) length = append_scan_name(names, sizeof(names), length, records[index].ssid);
|
||||
return httpd_resp_send(request, names, length);
|
||||
}
|
||||
|
||||
static esp_err_t root_handler(httpd_req_t *request) {
|
||||
if (s_littlefs_mounted) return send_static_file(request, "/index.html");
|
||||
httpd_resp_set_status(request, "503 Service Unavailable");
|
||||
@@ -359,19 +447,23 @@ static esp_err_t static_file_handler(httpd_req_t *request) {
|
||||
httpd_resp_set_status(request, "503 Service Unavailable");
|
||||
return httpd_resp_send(request, "LittleFS is unavailable", HTTPD_RESP_USE_STRLEN);
|
||||
}
|
||||
if (captive_portal_is_probe_path(request->uri)) return redirect_to_setup(request);
|
||||
if (strcmp(request->uri, "/styles.css") != 0 && strcmp(request->uri, "/app.js") != 0 &&
|
||||
strcmp(request->uri, "/target_interaction.js") != 0 && strcmp(request->uri, "/web_audio.js") != 0 &&
|
||||
strcmp(request->uri, "/game_sounds.js") != 0 &&
|
||||
strcmp(request->uri, "/ship-sprite.svg") != 0) return httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "asset not found");
|
||||
strcmp(request->uri, "/ship-sprite.svg") != 0 && strcmp(request->uri, "/setup.css") != 0 &&
|
||||
strcmp(request->uri, "/setup.js") != 0) return fallback_active() ? redirect_to_setup(request) : httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "asset not found");
|
||||
return send_static_file(request, request->uri);
|
||||
}
|
||||
|
||||
static esp_err_t start_http_server(void) {
|
||||
httpd_config_t config = HTTPD_DEFAULT_CONFIG();
|
||||
config.max_uri_handlers = 18U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true;
|
||||
config.max_uri_handlers = 21U; config.max_open_sockets = kHttpMaxOpenSockets; config.uri_match_fn = httpd_uri_match_wildcard; config.lru_purge_enable = true;
|
||||
ESP_RETURN_ON_ERROR(httpd_start(&s_server, &config), kLogTag, "http server start failed");
|
||||
const httpd_uri_t routes[] = {
|
||||
{.uri = "/", .method = HTTP_GET, .handler = root_handler},
|
||||
{.uri = "/setup", .method = HTTP_GET, .handler = setup_handler},
|
||||
{.uri = "/setup/scan", .method = HTTP_GET, .handler = setup_scan_handler},
|
||||
{.uri = "/api/info", .method = HTTP_GET, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_INFO},
|
||||
{.uri = "/api/health", .method = HTTP_GET, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_HEALTH},
|
||||
{.uri = "/api/session/join", .method = HTTP_POST, .handler = api_handler, .user_ctx = (void *)(uintptr_t)HTTP_API_ROUTE_JOIN},
|
||||
|
||||
Reference in New Issue
Block a user