diff --git a/tutorials/esp32/minimal/CMakeLists.txt b/tutorials/esp32/minimal/CMakeLists.txt new file mode 100644 index 00000000..965466a6 --- /dev/null +++ b/tutorials/esp32/minimal/CMakeLists.txt @@ -0,0 +1,6 @@ +# The following lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.5) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(mongoose-esp32-minimal) diff --git a/tutorials/esp32/minimal/Makefile b/tutorials/esp32/minimal/Makefile new file mode 100644 index 00000000..232eec2f --- /dev/null +++ b/tutorials/esp32/minimal/Makefile @@ -0,0 +1,23 @@ +THISDIR = $(realpath $(CURDIR)) +ROOTDIR = $(realpath $(CURDIR)/../../..) +DOCKER ?= docker run --rm $(DA) -v $(ROOTDIR):$(ROOTDIR) -w $(THISDIR) espressif/idf +CMD ?= idf.py build +PORT ?= /dev/ttyUSB0 + +all: example + +example: + true + +build: Makefile $(wildcard main/*) + $(DOCKER) $(CMD) + +flash: +flash: CMD = idf.py flash +flash: DA = --device $(PORT) +flash: build + +clean: + test -d build && $(DOCKER) rm -rf build sdkconfig || true + +.PHONY: build diff --git a/tutorials/esp32/minimal/README.md b/tutorials/esp32/minimal/README.md new file mode 100644 index 00000000..bc28263a --- /dev/null +++ b/tutorials/esp32/minimal/README.md @@ -0,0 +1,74 @@ +# ESP32 minimal crash capture + +ESP-IDF already captures the ESP32 equivalent of an ARM HardFault. Use the +ESP-IDF panic handler and ESP-IDF core dump support instead of replacing the +low-level exception path. + +The panic handler captures the architecture-specific fault frame: + +- Xtensa targets: PC, PS, A0..A15, SAR, EXCCAUSE, EXCVADDR, loop registers, and + backtrace. +- RISC-V targets: MEPC, RA, SP, MSTATUS, MCAUSE, MTVAL, MHARTID, GPRs, and + backtrace. + +Fatal classes include CPU exceptions, interrupt watchdog, task watchdog, cache +errors, brownout, stack overflow, heap corruption, UBSAN checks, and assertions. + +This example enables core dump to flash. No filesystem is involved: ESP-IDF +writes directly to a raw partition table entry of type `data`, subtype +`coredump`. + +```csv +coredump, data, coredump,, 128K +``` + +At panic time, ESP-IDF serializes the crash state into that raw flash partition. +On the next boot, the application can read it with the user-mode coredump APIs: + +```c +esp_reset_reason(); +esp_core_dump_get_panic_reason(reason, sizeof(reason)); +esp_core_dump_get_summary(summary); +esp_core_dump_image_get(&addr, &size); +esp_core_dump_image_erase(); +``` + +This example exposes: + +- `/api/crash`: JSON metadata: reset reason, panic reason, crashed task, PC, and + coredump size. +- `/api/crash.bin`: raw ESP-IDF coredump image, suitable for offline decoding + with the application ELF. + +The raw coredump can include task stacks, TCBs, selected memory regions, and +therefore customer-private data. Production upload must treat it as sensitive +diagnostic material. + +## Wizard integration notes + +The Wizard ESP-IDF template should keep its existing `mongoose_init()` / +`mongoose_poll()` structure and register the crash endpoints as custom API +handlers. The relevant ESP-IDF pieces from this tutorial are: + +```ini +CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y +CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=y +CONFIG_ESP_COREDUMP_MAX_TASKS_NUM=16 +CONFIG_ESP_COREDUMP_STACK_SIZE=2048 +``` + +The ESP-IDF component needs `espcoredump` and `esp_partition`. + +Wizard's current 4 MB OTA partition table leaves `0x10000` bytes after SPIFFS, +so a 64 KB coredump partition fits without changing the app or SPIFFS sizes. +The 128 KB partition used by this tutorial requires shrinking SPIFFS or one of +the app slots. + +```csv +coredump, data, coredump,, 64K +``` + +The WiFi code is not materially related to crash capture. Apart from where the +credentials come from, the differences are retry policy, temporary event handler +lifetime, WPA2/PMF station config, and whether the password is logged. Wizard +can keep its existing WiFi code for this feature. diff --git a/tutorials/esp32/minimal/main/CMakeLists.txt b/tutorials/esp32/minimal/main/CMakeLists.txt new file mode 100644 index 00000000..647593e4 --- /dev/null +++ b/tutorials/esp32/minimal/main/CMakeLists.txt @@ -0,0 +1,14 @@ +idf_component_register(SRCS "main.c" + "wifi.c" + "../../../../mongoose.c" + INCLUDE_DIRS "." + "../../../.." + REQUIRES esp_event + esp_netif + esp_partition + esp_wifi + esp_timer + espcoredump + nvs_flash + app_update) +component_compile_options(-DHTTP_URL="http://0.0.0.0:80") diff --git a/tutorials/esp32/minimal/main/main.c b/tutorials/esp32/minimal/main/main.c new file mode 100644 index 00000000..fbcfe6a4 --- /dev/null +++ b/tutorials/esp32/minimal/main/main.c @@ -0,0 +1,161 @@ +// Copyright (c) 2026 Cesanta Software Limited +// All rights reserved + +#include +#include "esp_core_dump.h" +#include "esp_err.h" +#include "esp_partition.h" +#include "esp_system.h" +#include "mongoose.h" +#include "sdkconfig.h" + +#define WIFI_SSID "YOUR_WIFI_NETWORK_NAME" // SET THIS! +#define WIFI_PASS "YOUR_WIFI_PASSWORD" // SET THIS! + +#ifndef HTTP_URL +#define HTTP_URL "http://0.0.0.0:80" +#endif + +#ifndef CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH +#define CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH 0 +#endif + +#ifndef ESP_PARTITION_SUBTYPE_DATA_COREDUMP +#define ESP_PARTITION_SUBTYPE_DATA_COREDUMP 0x03 +#endif + +struct crash_download { + const esp_partition_t *partition; + size_t offset; + size_t size; + size_t sent; +}; + +static void crash_json(struct mg_connection *c) { +#if CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH + esp_core_dump_summary_t summary; + char reason[200] = ""; + size_t address = 0, size = 0; + esp_err_t err = esp_core_dump_image_get(&address, &size); + esp_err_t sr = err == ESP_OK ? esp_core_dump_get_summary(&summary) : err; + esp_err_t rr = err == ESP_OK ? esp_core_dump_get_panic_reason( + reason, sizeof(reason)) : err; + + mg_http_reply(c, 200, "Content-Type: application/json\r\n", + "{%m:%s,%m:%d,%m:%u,%m:%u,%m:%m,%m:%m,%m:%d,%m:%d,%m:%m}\n", + MG_ESC("present"), err == ESP_OK ? "true" : "false", + MG_ESC("reset_reason"), (int) esp_reset_reason(), + MG_ESC("address"), (unsigned) address, MG_ESC("size"), + (unsigned) size, MG_ESC("panic_reason"), + MG_ESC(rr == ESP_OK ? reason : ""), MG_ESC("task"), + MG_ESC(sr == ESP_OK ? summary.exc_task : ""), MG_ESC("pc"), + sr == ESP_OK ? (int) summary.exc_pc : 0, MG_ESC("error"), + (int) err, MG_ESC("binary"), MG_ESC("/api/crash.bin")); +#else + mg_http_reply(c, 501, "Content-Type: application/json\r\n", + "{%m:false,%m:%d,%m:%m}\n", MG_ESC("present"), + MG_ESC("reset_reason"), (int) esp_reset_reason(), + MG_ESC("error"), MG_ESC("coredump to flash is disabled")); +#endif +} + +static void crash_send(struct mg_connection *c) { +#if CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH + struct crash_download *d = (struct crash_download *) c->fn_data; + while (d != NULL && d->sent < d->size && c->send.len < 2048) { + uint8_t buf[512]; + size_t left = d->size - d->sent; + size_t len = left > sizeof(buf) ? sizeof(buf) : left; + esp_err_t err = esp_partition_read(d->partition, d->offset + d->sent, buf, + len); + if (err != ESP_OK) { + MG_ERROR(("esp_partition_read: %s", esp_err_to_name(err))); + c->is_draining = 1; + return; + } + mg_send(c, buf, len); + d->sent += len; + } + if (d != NULL && d->sent >= d->size) { + free(d); + c->fn_data = NULL; + c->is_draining = 1; + } +#else + (void) c; +#endif +} + +static void crash_bin(struct mg_connection *c) { +#if CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH + const esp_partition_t *p = esp_partition_find_first( + ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_COREDUMP, NULL); + struct crash_download *d; + size_t address = 0, size = 0, offset = 0; + esp_err_t err = esp_core_dump_image_get(&address, &size); + + if (err != ESP_OK) { + mg_http_reply(c, 404, "Content-Type: application/json\r\n", + "{%m:%m}\n", MG_ESC("error"), MG_ESC(esp_err_to_name(err))); + } else if (p == NULL || address < p->address || + address + size > p->address + p->size) { + mg_http_reply(c, 500, "Content-Type: application/json\r\n", + "{%m:%m}\n", MG_ESC("error"), + MG_ESC("invalid coredump partition")); + } else if ((d = (struct crash_download *) calloc(1, sizeof(*d))) == NULL) { + mg_http_reply(c, 500, "Content-Type: application/json\r\n", + "{%m:%m}\n", MG_ESC("error"), MG_ESC("out of memory")); + } else { + offset = address - p->address; + d->partition = p; + d->offset = offset; + d->size = size; + c->fn_data = d; + mg_printf(c, + "HTTP/1.1 200 OK\r\n" + "Content-Type: application/octet-stream\r\n" + "Content-Length: %u\r\n" + "Content-Disposition: attachment; filename=\"esp32-coredump." + "bin\"\r\n" + "\r\n", + (unsigned) size); + crash_send(c); + } +#else + mg_http_reply(c, 501, "Content-Type: application/json\r\n", + "{%m:%m}\n", MG_ESC("error"), + MG_ESC("coredump to flash is disabled")); +#endif +} + +static void http_ev_handler(struct mg_connection *c, int ev, void *ev_data) { + if (ev == MG_EV_HTTP_MSG) { + struct mg_http_message *hm = (struct mg_http_message *) ev_data; + if (mg_match(hm->uri, mg_str("/api/crash.bin"), NULL)) { + crash_bin(c); + } else if (mg_match(hm->uri, mg_str("/api/crash"), NULL)) { + crash_json(c); + } else { + mg_http_reply(c, 200, "", "Hi from Mongoose\n"); + } + } else if (ev == MG_EV_POLL && c->fn_data != NULL) { + crash_send(c); + } else if (ev == MG_EV_CLOSE && c->fn_data != NULL) { + free(c->fn_data); + c->fn_data = NULL; + } +} + +void app_main(void) { + void wifi_init(const char *ssid, const char *pass); + wifi_init(WIFI_SSID, WIFI_PASS); + + struct mg_mgr mgr; + mg_log_set(MG_LL_INFO); + mg_mgr_init(&mgr); + MG_INFO(("Mongoose version : v%s", MG_VERSION)); + MG_INFO(("Listening on : %s", HTTP_URL)); + mg_http_listen(&mgr, HTTP_URL, http_ev_handler, NULL); + + for (;;) mg_mgr_poll(&mgr, 1000); +} diff --git a/tutorials/esp32/minimal/main/mongoose_config.h b/tutorials/esp32/minimal/main/mongoose_config.h new file mode 100644 index 00000000..3282da6a --- /dev/null +++ b/tutorials/esp32/minimal/main/mongoose_config.h @@ -0,0 +1,3 @@ +#define MG_ARCH MG_ARCH_ESP32 + +#define MG_TLS MG_TLS_NONE diff --git a/tutorials/esp32/minimal/main/wifi.c b/tutorials/esp32/minimal/main/wifi.c new file mode 100644 index 00000000..010cba45 --- /dev/null +++ b/tutorials/esp32/minimal/main/wifi.c @@ -0,0 +1,104 @@ +// Code taken from the ESP32 IDF WiFi station Example + +#include +#include "esp_event.h" +#include "esp_log.h" +#include "esp_system.h" +#include "esp_wifi.h" +#include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" +#include "freertos/task.h" +#include "nvs_flash.h" + +#include "lwip/err.h" +#include "lwip/sys.h" + +#include "mongoose.h" + +static EventGroupHandle_t s_wifi_event_group; + +/* The event group allows multiple bits for each event, but we only care about + * two events: + * - we are connected to the AP with an IP + * - we failed to connect after the maximum amount of retries */ +#define WIFI_CONNECTED_BIT BIT0 +#define WIFI_FAIL_BIT BIT1 + +static int s_retry_num = 0; + +static void event_handler(void *arg, esp_event_base_t event_base, + int32_t event_id, void *event_data) { + if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_STA_START) { + esp_wifi_connect(); + } else if (event_base == WIFI_EVENT && + event_id == WIFI_EVENT_STA_DISCONNECTED) { + if (s_retry_num < 10) { + esp_wifi_connect(); + s_retry_num++; + MG_INFO(("retry to connect to the AP")); + } else { + xEventGroupSetBits(s_wifi_event_group, WIFI_FAIL_BIT); + } + MG_ERROR(("connect to the AP fail")); + } else if (event_base == IP_EVENT && event_id == IP_EVENT_STA_GOT_IP) { + ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data; + MG_INFO(("IP ADDRESS:" IPSTR, IP2STR(&event->ip_info.ip))); + s_retry_num = 0; + xEventGroupSetBits(s_wifi_event_group, WIFI_CONNECTED_BIT); + } +} + +void wifi_init(const char *ssid, const char *pass) { + esp_err_t ret = nvs_flash_init(); + if (ret == ESP_ERR_NVS_NO_FREE_PAGES || + ret == ESP_ERR_NVS_NEW_VERSION_FOUND) { + ESP_ERROR_CHECK(nvs_flash_erase()); + ret = nvs_flash_init(); + } + ESP_ERROR_CHECK(ret); + + s_wifi_event_group = xEventGroupCreate(); + + ESP_ERROR_CHECK(esp_netif_init()); + + ESP_ERROR_CHECK(esp_event_loop_create_default()); + esp_netif_create_default_wifi_sta(); + + wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_wifi_init(&cfg)); + + esp_event_handler_instance_t instance_any_id; + esp_event_handler_instance_t instance_got_ip; + ESP_ERROR_CHECK(esp_event_handler_instance_register( + WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, NULL, &instance_any_id)); + ESP_ERROR_CHECK(esp_event_handler_instance_register( + IP_EVENT, IP_EVENT_STA_GOT_IP, &event_handler, NULL, &instance_got_ip)); + + wifi_config_t c = {.sta = {.threshold = {.authmode = WIFI_AUTH_WPA2_PSK}, + .pmf_cfg = {.capable = true, .required = false}}}; + snprintf((char *) c.sta.ssid, sizeof(c.sta.ssid), "%s", ssid); + snprintf((char *) c.sta.password, sizeof(c.sta.password), "%s", pass); + ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA)); + ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &c)); + ESP_ERROR_CHECK(esp_wifi_start()); + MG_DEBUG(("wifi_init_sta finished.")); + + EventBits_t bits = xEventGroupWaitBits(s_wifi_event_group, + WIFI_CONNECTED_BIT | WIFI_FAIL_BIT, + pdFALSE, pdFALSE, portMAX_DELAY); + + if (bits & WIFI_CONNECTED_BIT) { + MG_INFO(("connected to ap SSID:%s", ssid)); + } else if (bits & WIFI_FAIL_BIT) { + MG_ERROR(("Failed to connect to SSID:%s", ssid)); + } else { + MG_ERROR(("UNEXPECTED EVENT")); + } + + /* The event will not be processed after unregister */ + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + IP_EVENT, IP_EVENT_STA_GOT_IP, instance_got_ip)); + ESP_ERROR_CHECK(esp_event_handler_instance_unregister( + WIFI_EVENT, ESP_EVENT_ANY_ID, instance_any_id)); + vEventGroupDelete(s_wifi_event_group); +} diff --git a/tutorials/esp32/minimal/partitions.csv b/tutorials/esp32/minimal/partitions.csv new file mode 100644 index 00000000..9624a6d7 --- /dev/null +++ b/tutorials/esp32/minimal/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size, Flags +nvs, data, nvs, , 0x6000 +phy_init, data, phy, , 0x1000 +factory, app, factory, , 1M +coredump, data, coredump,, 128K diff --git a/tutorials/esp32/minimal/sdkconfig.defaults b/tutorials/esp32/minimal/sdkconfig.defaults new file mode 100644 index 00000000..89d7695f --- /dev/null +++ b/tutorials/esp32/minimal/sdkconfig.defaults @@ -0,0 +1,7 @@ +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH=y +CONFIG_ESP_COREDUMP_DATA_FORMAT_ELF=y +CONFIG_ESP_COREDUMP_MAX_TASKS_NUM=16 +CONFIG_ESP_COREDUMP_STACK_SIZE=2048