diff --git a/.gitignore b/.gitignore
index d29451d..44fbda7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,4 @@
sdkconfig.esp32-c6-devkitm-1
.pio
.vscode
+include/wifi_config.h
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 376b49e..de4f148 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,3 +1,9 @@
cmake_minimum_required(VERSION 3.16.0)
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+
+# PlatformIO installs the pinned ESP-IDF LittleFS component per environment.
+get_filename_component(platformio_environment "${CMAKE_BINARY_DIR}" NAME)
+list(APPEND EXTRA_COMPONENT_DIRS
+ "${CMAKE_SOURCE_DIR}/.pio/libdeps/${platformio_environment}/esp_littlefs")
+
project(battleship)
diff --git a/PLANS.md b/PLANS.md
index 663b6c0..497f0e7 100644
--- a/PLANS.md
+++ b/PLANS.md
@@ -167,7 +167,7 @@ Execution record
## Milestone 002 — Prove the Wi-Fi, LittleFS, and HTTP vertical slice
-**Status:** `READY`
+**Status:** `DONE`
**Depends on:** Milestone 001
### Objective
@@ -203,11 +203,22 @@ Prove the MVP's basic delivery path: home Wi-Fi, LittleFS, and HTTP served direc
If all criteria pass, set this milestone to `DONE`, append its execution record, and change Milestone 003 from `BLOCKED` to `READY`.
+### Execution record
+
+- Date: 2026-08-27
+- Board model and revision: ESP32-C6FH4 QFN32, revision v0.2; carrier board remains an unidentified SuperMini-style ESP32-C6 Mini.
+- Toolchain and library versions: PlatformIO Core 6.1.19; `espressif32` 7.0.1; ESP-IDF 6.0.1; pinned `esp_littlefs` 1.20.4 (commit `92ac3c2`).
+- Result: PASS
+- Evidence: Implemented the station-mode Wi-Fi state machine with bounded 2–32 second reconnect backoff, LittleFS mount without auto-formatting, static routes (`/`, `/styles.css`, `/app.js`), and `GET /api/health`. Static files are streamed in 1,024-byte chunks, have explicit MIME/cache headers, and select a matching precompressed `.gz` file when supplied by the LittleFS image. `data/` contains the local Russian diagnostic page, CSS, and JavaScript; `include/wifi_config.h.example` documents the ignored local credential file. `pio run -e esp32-c6-devkitm-1 -t buildfs` successfully created `littlefs.bin` containing all three assets. Firmware and LittleFS were uploaded to the ESP32-C6FH4, and esptool verified every written hash. A serial reset/read confirmed LittleFS mount, HTTP server startup on port 80, Wi-Fi association, and DHCP assignment without a restart. Five consecutive LAN `GET /api/health` requests returned HTTP 200; `GET /styles.css` returned `Content-Type: text/css; charset=utf-8` and `Cache-Control: public, max-age=86400`.
+- Measurements: Final credential-configured firmware build used 37,164 / 327,680 bytes of RAM (11.3%) and 987,644 / 2,097,152 bytes of flash (47.1%). The LittleFS partition is 1,984 KB; the mounted image reported 2,031,616 total bytes and 20,480 used bytes. The health endpoint reported RSSI from -43 to -46 dBm, 324,488 current free heap bytes, and 316,580 minimum free heap bytes after approximately 64 seconds. No new compiler warnings were emitted by the successful builds.
+- Issues or deviations: The first device upload exposed an `Invalid mbox` assertion because the HTTP server started before `esp_netif_init`; this was corrected before the final verified upload. `pio test -e esp32-c6-devkitm-1 --without-uploading` ran but errored because `test/` contains no test suite. On 2026-08-28, the user confirmed completion of the remaining two-device, Wi-Fi interruption/reconnect, gzip-delivery, and LittleFS mount-failure checks; their physical observations are accepted as the required evidence.
+- Next action: Milestone 003 is in progress. Do not begin Milestone 004 as part of this task.
+
---
## Milestone 003 — Prove real-time transport, fallback, and state isolation
-**Status:** `BLOCKED`
+**Status:** `IN PROGRESS`
**Depends on:** Milestone 002
### Objective
diff --git a/data/app.js b/data/app.js
new file mode 100644
index 0000000..ec1deac
--- /dev/null
+++ b/data/app.js
@@ -0,0 +1,32 @@
+(() => {
+ const target = document.querySelector('#health');
+ const labels = {
+ uptime_ms: 'Время работы (мс)', wifi_state: 'Wi-Fi', rssi_dbm: 'RSSI (дБм)',
+ free_heap_bytes: 'Свободная память (байт)', min_free_heap_bytes: 'Мин. свободная память (байт)',
+ build_version: 'Версия сборки'
+ };
+
+ function render(health) {
+ target.replaceChildren();
+ for (const [key, label] of Object.entries(labels)) {
+ const term = document.createElement('dt');
+ const value = document.createElement('dd');
+ term.textContent = label;
+ value.textContent = health[key] ?? 'недоступно';
+ target.append(term, value);
+ }
+ }
+
+ async function refresh() {
+ try {
+ const response = await fetch('/api/health', { cache: 'no-store' });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ render(await response.json());
+ } catch (error) {
+ target.textContent = `Не удалось получить состояние: ${error.message}`;
+ }
+ }
+
+ refresh();
+ window.setInterval(refresh, 5000);
+})();
diff --git a/data/index.html b/data/index.html
new file mode 100644
index 0000000..edb6ad4
--- /dev/null
+++ b/data/index.html
@@ -0,0 +1,20 @@
+
+
+
+
+
+ Морской бой — ESP32
+
+
+
+
+ Морской бой
+ ESP32-C6: проверка Wi-Fi, LittleFS и HTTP.
+
+ Состояние устройства
+ Загрузка…
+
+
+
+
+
diff --git a/data/styles.css b/data/styles.css
new file mode 100644
index 0000000..510c515
--- /dev/null
+++ b/data/styles.css
@@ -0,0 +1,7 @@
+:root { color-scheme: dark; font-family: system-ui, sans-serif; }
+body { margin: 0; background: #10223a; color: #f4f7fb; }
+main { max-width: 42rem; margin: 0 auto; padding: 1.5rem; }
+section { background: #19395c; border-radius: .75rem; padding: 1rem; }
+dl { display: grid; grid-template-columns: max-content 1fr; gap: .5rem 1rem; }
+dt { font-weight: 700; }
+dd { margin: 0; overflow-wrap: anywhere; }
diff --git a/include/wifi_config.h.example b/include/wifi_config.h.example
new file mode 100644
index 0000000..be58959
--- /dev/null
+++ b/include/wifi_config.h.example
@@ -0,0 +1,6 @@
+#pragma once
+
+// Copy this file to include/wifi_config.h and set the local home-network
+// credentials. The copied file is ignored by Git and must never be committed.
+#define WIFI_CONFIG_SSID "replace-with-network-name"
+#define WIFI_CONFIG_PASSWORD "replace-with-network-password"
diff --git a/platformio.ini b/platformio.ini
index c947a06..a8a9a17 100644
--- a/platformio.ini
+++ b/platformio.ini
@@ -13,5 +13,7 @@ platform = espressif32 @ 7.0.1
board = esp32-c6-devkitm-1
framework = espidf
board_build.partitions = partitions.csv
+board_build.filesystem = littlefs
+lib_deps = https://github.com/joltwallet/esp_littlefs.git#v1.20.4
monitor_port = /dev/ttyACM0
monitor_speed = 115200
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 483bc0c..a95dd1e 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,6 +1,8 @@
# This file was automatically generated for projects
# without default 'CMakeLists.txt' file.
-FILE(GLOB_RECURSE app_sources ${CMAKE_SOURCE_DIR}/src/*.*)
-
-idf_component_register(SRCS ${app_sources})
+idf_component_register(
+ SRCS "main.c"
+ INCLUDE_DIRS "../include"
+ REQUIRES esp_event esp_http_server esp_netif esp_wifi esp_littlefs nvs_flash
+)
diff --git a/src/main.c b/src/main.c
index 0d4f8d5..0055a4e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -1,141 +1,346 @@
#include
-#include
+#include
#include
+#include
+#include
-#include "driver/gpio.h"
-#include "esp_chip_info.h"
-#include "esp_err.h"
-#include "esp_flash.h"
+#include "esp_check.h"
+#include "esp_event.h"
+#include "esp_http_server.h"
+#include "esp_littlefs.h"
#include "esp_log.h"
-#include "esp_system.h"
+#include "esp_netif.h"
#include "esp_timer.h"
+#include "esp_wifi.h"
#include "freertos/FreeRTOS.h"
-#include "freertos/task.h"
+#include "freertos/portmacro.h"
+#include "nvs_flash.h"
-static const char *const kLogTag = "bringup";
-static const gpio_num_t kCandidateLedGpio = GPIO_NUM_8;
-static const uint32_t kLedProbeCount = 10;
-static const uint32_t kLedProbeIntervalMs = 500;
-static const uint32_t kHealthIntervalMs = 30000;
-static const uint32_t kStartupAttachmentDelayMs = 10000;
+#if __has_include("wifi_config.h")
+#include "wifi_config.h"
+#define WIFI_CONFIG_AVAILABLE 1
+#else
+#define WIFI_CONFIG_AVAILABLE 0
+#endif
-static const char *reset_reason_name(esp_reset_reason_t reason) {
- switch (reason) {
- case ESP_RST_UNKNOWN:
- return "unknown";
- case ESP_RST_POWERON:
- return "power_on";
- case ESP_RST_EXT:
- return "external";
- case ESP_RST_SW:
- return "software";
- case ESP_RST_PANIC:
- return "panic";
- case ESP_RST_INT_WDT:
- return "interrupt_watchdog";
- case ESP_RST_TASK_WDT:
- return "task_watchdog";
- case ESP_RST_WDT:
- return "other_watchdog";
- case ESP_RST_DEEPSLEEP:
- return "deep_sleep";
- case ESP_RST_BROWNOUT:
- return "brownout";
- case ESP_RST_SDIO:
- return "sdio";
- case ESP_RST_USB:
- return "usb";
- case ESP_RST_JTAG:
- return "jtag";
- case ESP_RST_EFUSE:
- return "efuse";
- case ESP_RST_PWR_GLITCH:
- return "power_glitch";
- case ESP_RST_CPU_LOCKUP:
- return "cpu_lockup";
- }
+static const char *const kLogTag = "vertical_slice";
+static const char *const kBuildVersion = "m002";
+static const char *const kLittlefsBasePath = "/littlefs";
+static const char *const kLittlefsPartitionLabel = "littlefs";
+static const size_t kFileChunkBytes = 1024;
- return "unrecognized";
+typedef struct {
+ bool configured;
+ bool connected;
+ bool retry_scheduled;
+ uint8_t reconnect_attempt;
+ char ip_address[16];
+} wifi_state_t;
+
+static portMUX_TYPE s_wifi_lock = portMUX_INITIALIZER_UNLOCKED;
+static wifi_state_t s_wifi_state = {0};
+static bool s_littlefs_mounted;
+
+static wifi_state_t wifi_state_snapshot(void) {
+ wifi_state_t snapshot;
+ portENTER_CRITICAL(&s_wifi_lock);
+ snapshot = s_wifi_state;
+ portEXIT_CRITICAL(&s_wifi_lock);
+ return snapshot;
}
-static void log_heap(uint64_t uptime_ms) {
- const size_t free_heap_bytes = esp_get_free_heap_size();
- const size_t minimum_free_heap_bytes = esp_get_minimum_free_heap_size();
- uint32_t flash_size_bytes = 0;
- const esp_err_t flash_result = esp_flash_get_size(NULL, &flash_size_bytes);
+#if WIFI_CONFIG_AVAILABLE
+static const uint8_t kMaxReconnectExponent = 5;
+static esp_timer_handle_t s_reconnect_timer;
- if (flash_result == ESP_OK) {
- ESP_LOGI(kLogTag,
- "health uptime_ms=%" PRIu64 " reset_reason=%s flash_bytes=%" PRIu32
- " free_heap_bytes=%u min_free_heap_bytes=%u",
- uptime_ms,
- reset_reason_name(esp_reset_reason()),
- flash_size_bytes,
- (unsigned int)free_heap_bytes,
- (unsigned int)minimum_free_heap_bytes);
- } else {
- ESP_LOGE(kLogTag, "health flash_size_failed=%s", esp_err_to_name(flash_result));
- }
-}
+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);
-static void probe_candidate_led(void) {
- ESP_LOGI(kLogTag,
- "led_probe gpio=%d transitions=%" PRIu32 " interval_ms=%" PRIu32 "; visually confirm the LED",
- (int)kCandidateLedGpio,
- kLedProbeCount,
- kLedProbeIntervalMs);
-
- esp_err_t result = gpio_reset_pin(kCandidateLedGpio);
+ const esp_err_t result = esp_wifi_connect();
if (result != ESP_OK) {
- ESP_LOGW(kLogTag, "led_probe gpio_reset_pin failed: %s", esp_err_to_name(result));
+ ESP_LOGW(kLogTag, "wifi connect request failed: %s", esp_err_to_name(result));
+ }
+}
+
+static void schedule_reconnect(uint32_t delay_ms) {
+ bool should_start_timer = 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;
+ should_start_timer = true;
+ }
+ portEXIT_CRITICAL(&s_wifi_lock);
+
+ if (!should_start_timer) {
return;
}
-
- result = gpio_set_direction(kCandidateLedGpio, GPIO_MODE_OUTPUT);
+ const esp_err_t result = esp_timer_start_once(s_reconnect_timer, (uint64_t)delay_ms * 1000U);
if (result != ESP_OK) {
- ESP_LOGW(kLogTag, "led_probe gpio_set_direction failed: %s", esp_err_to_name(result));
- return;
+ portENTER_CRITICAL(&s_wifi_lock);
+ s_wifi_state.retry_scheduled = false;
+ portEXIT_CRITICAL(&s_wifi_lock);
+ ESP_LOGW(kLogTag, "wifi reconnect timer failed: %s", esp_err_to_name(result));
}
-
- for (uint32_t transition = 0; transition < kLedProbeCount; ++transition) {
- gpio_set_level(kCandidateLedGpio, transition % 2U);
- vTaskDelay(pdMS_TO_TICKS(kLedProbeIntervalMs));
- }
-
- gpio_set_level(kCandidateLedGpio, 0);
- ESP_LOGI(kLogTag, "led_probe complete; USB serial remained available during probe");
}
-static void log_startup(void) {
- esp_chip_info_t chip_info = {0};
- uint32_t flash_size_bytes = 0;
- const esp_err_t flash_result = esp_flash_get_size(NULL, &flash_size_bytes);
+static uint32_t next_reconnect_delay_ms(void) {
+ uint8_t exponent;
+ portENTER_CRITICAL(&s_wifi_lock);
+ if (s_wifi_state.reconnect_attempt < kMaxReconnectExponent) {
+ ++s_wifi_state.reconnect_attempt;
+ }
+ exponent = s_wifi_state.reconnect_attempt;
+ portEXIT_CRITICAL(&s_wifi_lock);
+ return 1000U << exponent;
+}
- esp_chip_info(&chip_info);
- ESP_LOGI(kLogTag,
- "startup reset_reason=%s chip_model=%d chip_revision=%d cores=%d features=0x%08" PRIx32,
- reset_reason_name(esp_reset_reason()),
- (int)chip_info.model,
- chip_info.revision,
- chip_info.cores,
- chip_info.features);
+static void wifi_event_handler(void *argument,
+ esp_event_base_t event_base,
+ int32_t event_id,
+ void *event_data) {
+ (void)argument;
+ if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) {
+ ESP_LOGI(kLogTag, "wifi station started");
+ schedule_reconnect(0);
+ return;
+ }
+ if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_DISCONNECTED) {
+ const uint32_t delay_ms = next_reconnect_delay_ms();
+ portENTER_CRITICAL(&s_wifi_lock);
+ s_wifi_state.connected = false;
+ s_wifi_state.ip_address[0] = '\0';
+ portEXIT_CRITICAL(&s_wifi_lock);
+ ESP_LOGW(kLogTag, "wifi disconnected; reconnecting in %" PRIu32 " ms", delay_ms);
+ schedule_reconnect(delay_ms);
+ return;
+ }
+ if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) {
+ const ip_event_got_ip_t *const event = (const ip_event_got_ip_t *)event_data;
+ char address[16];
+ esp_ip4addr_ntoa(&event->ip_info.ip, address, sizeof(address));
+ portENTER_CRITICAL(&s_wifi_lock);
+ s_wifi_state.connected = true;
+ s_wifi_state.reconnect_attempt = 0;
+ snprintf(s_wifi_state.ip_address, sizeof(s_wifi_state.ip_address), "%s", address);
+ portEXIT_CRITICAL(&s_wifi_lock);
+ ESP_LOGI(kLogTag, "wifi connected ip=%s", address);
+ }
+}
+#endif
- if (flash_result == ESP_OK) {
- ESP_LOGI(kLogTag, "startup flash_bytes=%" PRIu32, flash_size_bytes);
- } else {
- ESP_LOGE(kLogTag, "startup flash_size_failed=%s", esp_err_to_name(flash_result));
+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
+ wifi_init_config_t init_config = WIFI_INIT_CONFIG_DEFAULT();
+ wifi_config_t station_config = {0};
+ esp_timer_create_args_t reconnect_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();
+ 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(&reconnect_timer_args, &s_reconnect_timer),
+ kLogTag,
+ "wifi reconnect 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;
+ station_config.sta.pmf_cfg.required = false;
+ 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
+}
+
+static const char *wifi_state_name(const wifi_state_t *state) {
+ if (!state->configured) {
+ return "not_configured";
+ }
+ return state->connected ? "connected" : "connecting";
+}
+
+static esp_err_t health_handler(httpd_req_t *request) {
+ const wifi_state_t wifi_state = wifi_state_snapshot();
+ int8_t rssi_dbm = 0;
+ wifi_ap_record_t access_point = {0};
+ if (wifi_state.connected && esp_wifi_sta_get_ap_info(&access_point) == ESP_OK) {
+ rssi_dbm = access_point.rssi;
+ }
+ char response[320];
+ const int written = snprintf(response,
+ sizeof(response),
+ "{\"uptime_ms\":%" PRIu64
+ ",\"wifi_state\":\"%s\",\"rssi_dbm\":%d"
+ ",\"free_heap_bytes\":%u,\"min_free_heap_bytes\":%u"
+ ",\"build_version\":\"%s\"}",
+ (uint64_t)(esp_timer_get_time() / 1000),
+ wifi_state_name(&wifi_state),
+ (int)rssi_dbm,
+ (unsigned int)esp_get_free_heap_size(),
+ (unsigned int)esp_get_minimum_free_heap_size(),
+ kBuildVersion);
+ if (written < 0 || (size_t)written >= sizeof(response)) {
+ return httpd_resp_send_err(request, HTTPD_500_INTERNAL_SERVER_ERROR, "health response failed");
+ }
+ httpd_resp_set_type(request, "application/json");
+ httpd_resp_set_hdr(request, "Cache-Control", "no-store");
+ return httpd_resp_send(request, response, HTTPD_RESP_USE_STRLEN);
+}
+
+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, "/styles.css") == 0) {
+ return "text/css; charset=utf-8";
+ }
+ return "application/javascript; charset=utf-8";
+}
+
+static bool request_accepts_gzip(httpd_req_t *request) {
+ const size_t header_length = httpd_req_get_hdr_value_len(request, "Accept-Encoding");
+ if (header_length == 0 || header_length >= 96) {
+ return false;
+ }
+ char header[96];
+ return httpd_req_get_hdr_value_str(request, "Accept-Encoding", header, sizeof(header)) == ESP_OK &&
+ strstr(header, "gzip") != NULL;
+}
+
+static esp_err_t send_static_file(httpd_req_t *request, const char *asset_path) {
+ char filesystem_path[80];
+ char gzip_path[84];
+ bool gzip = false;
+ const char *path_to_open = filesystem_path;
+ snprintf(filesystem_path, sizeof(filesystem_path), "%s%s", kLittlefsBasePath, asset_path);
+ if (request_accepts_gzip(request)) {
+ snprintf(gzip_path, sizeof(gzip_path), "%s.gz", filesystem_path);
+ FILE *const compressed = fopen(gzip_path, "rb");
+ if (compressed != NULL) {
+ fclose(compressed);
+ path_to_open = gzip_path;
+ gzip = true;
+ }
+ }
+ FILE *const file = fopen(path_to_open, "rb");
+ if (file == NULL) {
+ return httpd_resp_send_err(request, HTTPD_404_NOT_FOUND, "static asset not found");
+ }
+ httpd_resp_set_type(request, mime_type_for_path(asset_path));
+ httpd_resp_set_hdr(request,
+ "Cache-Control",
+ strcmp(asset_path, "/index.html") == 0 ? "no-cache" : "public, max-age=86400");
+ if (gzip) {
+ httpd_resp_set_hdr(request, "Content-Encoding", "gzip");
+ httpd_resp_set_hdr(request, "Vary", "Accept-Encoding");
}
- log_heap(0);
+ char chunk[kFileChunkBytes];
+ size_t bytes_read;
+ esp_err_t result = ESP_OK;
+ while ((bytes_read = fread(chunk, 1, sizeof(chunk), file)) > 0) {
+ result = httpd_resp_send_chunk(request, chunk, bytes_read);
+ if (result != ESP_OK) {
+ break;
+ }
+ }
+ fclose(file);
+ return result == ESP_OK ? httpd_resp_send_chunk(request, NULL, 0) : result;
+}
+
+static esp_err_t root_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, "/index.html");
+}
+
+static esp_err_t static_file_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);
+ }
+ if (strcmp(request->uri, "/styles.css") != 0 && strcmp(request->uri, "/app.js") != 0) {
+ return 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_handle_t server = NULL;
+ httpd_config_t config = HTTPD_DEFAULT_CONFIG();
+ config.max_uri_handlers = 3;
+ config.uri_match_fn = httpd_uri_match_wildcard;
+ config.lru_purge_enable = true;
+ ESP_RETURN_ON_ERROR(httpd_start(&server, &config), kLogTag, "http server start failed");
+ const httpd_uri_t root = {.uri = "/", .method = HTTP_GET, .handler = root_handler};
+ const httpd_uri_t health = {.uri = "/api/health", .method = HTTP_GET, .handler = health_handler};
+ const httpd_uri_t static_files = {.uri = "/*", .method = HTTP_GET, .handler = static_file_handler};
+ ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &root), kLogTag, "root route registration failed");
+ ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &health), kLogTag, "health route registration failed");
+ ESP_RETURN_ON_ERROR(httpd_register_uri_handler(server, &static_files), kLogTag, "static route registration failed");
+ ESP_LOGI(kLogTag, "http server started on port %d", config.server_port);
+ return ESP_OK;
+}
+
+static void mount_littlefs(void) {
+ const esp_vfs_littlefs_conf_t config = {
+ .base_path = kLittlefsBasePath,
+ .partition_label = kLittlefsPartitionLabel,
+ .format_if_mount_failed = false,
+ .dont_mount = false,
+ .grow_on_mount = false,
+ };
+ const esp_err_t result = esp_vfs_littlefs_register(&config);
+ if (result != ESP_OK) {
+ ESP_LOGE(kLogTag, "littlefs mount failed: %s; HTTP static assets remain unavailable", esp_err_to_name(result));
+ return;
+ }
+ size_t total_bytes = 0;
+ size_t used_bytes = 0;
+ if (esp_littlefs_info(kLittlefsPartitionLabel, &total_bytes, &used_bytes) == ESP_OK) {
+ ESP_LOGI(kLogTag, "littlefs mounted total_bytes=%u used_bytes=%u", (unsigned int)total_bytes, (unsigned int)used_bytes);
+ }
+ s_littlefs_mounted = true;
}
void app_main(void) {
- vTaskDelay(pdMS_TO_TICKS(kStartupAttachmentDelayMs));
- log_startup();
- probe_candidate_led();
-
- while (true) {
- log_heap((uint64_t)(esp_timer_get_time() / 1000));
- vTaskDelay(pdMS_TO_TICKS(kHealthIntervalMs));
+ esp_err_t result = nvs_flash_init();
+ if (result == ESP_ERR_NVS_NO_FREE_PAGES || result == ESP_ERR_NVS_NEW_VERSION_FOUND) {
+ ESP_ERROR_CHECK(nvs_flash_erase());
+ result = nvs_flash_init();
+ }
+ ESP_ERROR_CHECK(result);
+ ESP_ERROR_CHECK(esp_netif_init());
+ ESP_ERROR_CHECK(esp_event_loop_create_default());
+ mount_littlefs();
+ ESP_ERROR_CHECK(start_http_server());
+ result = start_wifi();
+ if (result != ESP_OK) {
+ ESP_LOGW(kLogTag, "wifi unavailable: %s", esp_err_to_name(result));
}
}