#include #include #include #include #include #include "esp_check.h" #include "esp_event.h" #include "esp_http_server.h" #include "esp_littlefs.h" #include "esp_log.h" #include "esp_netif.h" #include "esp_timer.h" #include "esp_wifi.h" #include "freertos/FreeRTOS.h" #include "freertos/portmacro.h" #include "nvs_flash.h" #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 *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; 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; } #if WIFI_CONFIG_AVAILABLE static const uint8_t kMaxReconnectExponent = 5; static esp_timer_handle_t s_reconnect_timer; 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); const esp_err_t result = esp_wifi_connect(); if (result != ESP_OK) { 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; } const esp_err_t result = esp_timer_start_once(s_reconnect_timer, (uint64_t)delay_ms * 1000U); if (result != ESP_OK) { 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)); } } 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; } 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 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"); } 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) { 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)); } }