feat: prove the Wi-Fi, LittleFS, and HTTP vertical slice
This commit is contained in:
+317
-112
@@ -1,141 +1,346 @@
|
||||
#include <inttypes.h>
|
||||
#include <stddef.h>
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user