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)); } }