Merge pull request #3687 from cesanta/esp32crash

Add ESP32 minimal project with crash helper
This commit is contained in:
robertc2000 authored and GitHub committed 2026-09-09 12:09:21 +03:00
commit 863aea34c4
9 files changed
+397

No files matched your search

+6
View File
@@ -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)
+23
View File
@@ -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
+74
View File
@@ -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.
@@ -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")
+161
View File
@@ -0,0 +1,161 @@
// Copyright (c) 2026 Cesanta Software Limited
// All rights reserved
#include <stdlib.h>
#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);
}
@@ -0,0 +1,3 @@
#define MG_ARCH MG_ARCH_ESP32
#define MG_TLS MG_TLS_NONE
+104
View File
@@ -0,0 +1,104 @@
// Code taken from the ESP32 IDF WiFi station Example
#include <string.h>
#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);
}
+5
View File
@@ -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
1 # Name, Type, SubType, Offset, Size, Flags
2 nvs, data, nvs, , 0x6000
3 phy_init, data, phy, , 0x1000
4 factory, app, factory, , 1M
5 coredump, data, coredump,, 128K
@@ -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