feat: dump crash report to sdcard (#1145)

## Summary

This allow dumping crash message (i.e. assertion fail) and stack trace
to `crash_report.txt` file on sdcard. The stack trace can then be
decoded using https://esphome.github.io/esp-stacktrace-decoder/

Could be useful to debug things like
https://github.com/crosspoint-reader/crosspoint-reader/issues/1137 where
error doesn't always happen.

May also be useful to show a screen to tell what happen (show on next
boot after crash), similar to [flipper zero crash
message](https://www.reddit.com/r/flipperzero/comments/10f8m3f/anyone_who_can_tell_me_why_this_message_pops_up/)
, but this is better to be a dedicated PR (I'm missing the
`drawTextWrapped` function, too lazy to code it ; update: exactly what I
need in
https://github.com/crosspoint-reader/crosspoint-reader/pull/1141)

To test this:
- Option 1: add an `assert(false)` somewhere in the code
- Option 2: try dereferencing a nullptr
- Option 3: try `throw` an exception

Example of a crash report:

```
CrossPoint version: 1.1.0-dev

Panic reason: abort() was called at PC 0x4214585b on core 0

Recent logs:
[196] [DBG] [GFX] Time = 2 ms from clearScreen to displayBuffer
[1831] [DBG] [RBS] Recent books loaded from file (7 entries)
[1832] [DBG] [ACT] Exiting activity: Boot
[1832] [DBG] [ACT] Entering activity: Home
[1891] [DBG] [GFX] Time = 54 ms from clearScreen to displayBuffer
[2521] [DBG] [GFX] Time = 46 ms from clearScreen to displayBuffer
[4839] [DBG] [PWR] Going to low-power mode
[10048] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[20060] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[30072] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes
[34453] [DBG] [PWR] Restoring normal CPU frequency
[34485] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[35182] [DBG] [GFX] Time = 31 ms from clearScreen to displayBuffer
[36675] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[38800] [DBG] [GFX] Time = 30 ms from clearScreen to displayBuffer
[40079] [INF] [MEM] Free: 134164 bytes, Total: 232372 bytes, Min Free: 133664 bytes


Stack memory:
0x3FCB0650: 0x00000000 0x00000000 0x3FCB0668 0x4038DBB6 0x00000000 0x00000000 0x3FCA0030 0x3FC936D0 
0x3FCB0670: 0x3FCB067C 0x3FC936EC 0x3FCB0668 0x34313234 0x62353835 0x00000000 0x726F6261 0x20292874 
0x3FCB0690: 0x20736177 0x6C6C6163 0x61206465 0x43502074 0x34783020 0x35343132 0x20623538 0x63206E6F 
0x3FCB06B0: 0x2065726F 0x00000030 0x3FCA0000 0xB37A603F 0x00000001 0x3FCA7000 0x3FCABCDC 0x4214585E 
0x3FCB06D0: 0x3FCA7000 0x3FCA7000 0x3FCABCDC 0x421458AA 0x3FCABCDC 0x3FCA7000 0x3FCABCDC 0x421459CC 
0x3FCB06F0: 0x3FCA7000 0x3FCA7000 0x42145D5A 0x3C205624 0x40388560 0x3FCA7000 0x3FCABCFC 0x42079866 
0x3FCB0710: 0x3FCA7000 0x3FCA7000 0x00009C9A 0x4207B7F6 0x3FCA7000 0x42090000 0x001B7740 0x00000001 
0x3FCB0730: 0x3FCA7000 0x3FCA7000 0x00000001 0x600C0028 0x00000001 0x3FCA1000 0x00000000 0x00000000 
0x3FCB0750: 0x00000000 0x00000000 0x00000000 0xB37A603F 0x00000000 0x00000000 0x00000000 0x00000000 
0x3FCB0770: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0x42090000 0x3FCA7000 0x4208F9C4 
0x3FCB0790: 0x00000000 0x00000000 0x00000000 0x40388368 0x00000000 0x00000000 0x00000000 0x00000000 
0x3FCB07B0: 0x00000000 0x00000000 0x00000000 0x00000000 0x00000000 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 
0x3FCB07D0: 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0xA5A5A5A5 0xBAAD5678 0xDA6D3601 0x5EB5B9C5 0x2602E480 
0x3FCB07F0: 0x2BCDD33F 0x15556D4A 0x1F2140A0 0x5D59BEE3 0x8E76449F 0x6FB2D0CE 0xF5F46FAC 0x0112946A 
0x3FCB0810: 0x3B0B32E0 0x7A52B537 0x46801DB4 0xDA85DF9F 0x37E83D20 0x12861028 0x47A702BB 0x287A3C8A 
0x3FCB0830: 0x03632209 0xD44C5489 0x5E258453 0xFDA77529 0xE6748E23 0xADCF1394 0x67AD6778 0x2C208663 
0x3FCB0850: 0xC7985786 0xD4AA3AB2 0x312E1760 0xEC7AEAAE 0x1857020E 0x48003E7E 0xD6CB8763 0x9B4A3F66 
0x3FCB0870: 0x4B79E9F6 0xCBF739F0 0x3794C641 0xD0DBA3CB 0x95B9BE15 0x581C9983 0xDE62EFB6 0x20C67C5B 
0x3FCB0890: 0x1E4A3DF3 0xFB317C74 0xC0D86103 0x1D79ED56 0x72FE0862 0x3D38B0C8 0xD27EB587 0x0E0A4C40 
0x3FCB08B0: 0xF643ADC0 0x56D114D7 0x703AF879 0xAC7F3075 0x89C78C23 0xEDA86814 0xF767B3E3 0x0528838F 
0x3FCB08D0: 0x50ED4662 0x11FD38E7 0x8A5A83BB 0x658159BD 0x781AF696 0x8A700F79 0x526DDE23 0xC8472505 
0x3FCB08F0: 0x21AACC02 0xCB89369E 0xB82E5BE2 0x4C6C9D7D 0x9E724D9B 0xDC1067F7 0x84478FBC 0x4E89C444 
0x3FCB0910: 0x973F4229 0x49F93DA8 0xE30200F6 0xD1B5C391 0x8363A89F 0x2409E74C 0x3AFF7B52 0xCBEC2349 
0x3FCB0930: 0xD38F6695 0xBC3EA980 0xF067EBB1 0x7F87D167 0x92B3823B 0x9F0617D7 0xA7537C57 0x12CAB3D4 
0x3FCB0950: 0xC82EEE37 0x84D4B4BC 0xE1E2261C 0x488F0ADA 0x96EAF2FF 0x0BC493A0 0xCE614467 0x3829053D 
0x3FCB0970: 0xA41156BE 0x2747B77D 0x64DEA90B 0xE704AB0A 0xE4B01006 0x8D51903C 0x56CD3CF2 0x07E0A8E8 
0x3FCB0990: 0xD1DE05CE 0x33368522 0xD1889988 0x3A3097F4 0xB0796D09 0xC78948AA 0x6DEFC56E 0xD5C2E1D9 
0x3FCB09B0: 0xFD6DD8FA 0xA957B675 0xC202D80D 0x733FF8F4 0xA1484913 0x0B9AFBA6 0x330C07EA 0x2C09AD4C 
0x3FCB09D0: 0x3B1E08F7 0x3FCAE7D0 0x00000170 0xABBA1234 0x0000015C 0x3FCB00E0 0x00009C93 0x3FCA13C4 
0x3FCB09F0: 0x3FCA13C4 0x3FCB09E4 0x3FCA13BC 0x00000018 0x00000000 0x00000000 0x3FCB09E4 0x00000000 
0x3FCB0A10: 0x00000001 0x3FCAE7E0 0x706F6F6C 0x6B736154 0x00000000 0x00000000 0x3FCB07D0 0x00000005 
0x3FCB0A30: 0x00000000 0x00000001 0x00000000 0x3FCAB444 0x4209AFF0 0x0017E38F 0x00000000 0x3FCA7BD0 

```

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? **NO**

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
Xuan-Son Nguyen
2026-03-06 17:46:13 +01:00
committed by GitHub
parent a35f372e1b
commit 18b36efbae
6 changed files with 216 additions and 2 deletions

View File

@@ -1,5 +1,21 @@
#include "Logging.h"
#include <string>
#define MAX_ENTRY_LEN 256
#define MAX_LOG_LINES 16
// Simple ring buffer log, useful for error reporting when we encounter a crash
RTC_NOINIT_ATTR char logMessages[MAX_LOG_LINES][MAX_ENTRY_LEN];
RTC_NOINIT_ATTR size_t logHead = 0;
void addToLogRingBuffer(const char* message) {
// Add the message to the ring buffer, overwriting old messages if necessary
strncpy(logMessages[logHead], message, MAX_ENTRY_LEN - 1);
logMessages[logHead][MAX_ENTRY_LEN - 1] = '\0';
logHead = (logHead + 1) % MAX_LOG_LINES;
}
// Since logging can take a large amount of flash, we want to make the format string as short as possible.
// This logPrintf prepend the timestamp, level and origin to the user-provided message, so that the user only needs to
// provide the format string for the message itself.
@@ -9,7 +25,7 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
}
va_list args;
va_start(args, format);
char buf[256];
char buf[MAX_ENTRY_LEN];
char* c = buf;
// add the timestamp
{
@@ -43,5 +59,26 @@ void logPrintf(const char* level, const char* origin, const char* format, ...) {
// add the user message
vsnprintf(c, sizeof(buf) - (c - buf), format, args);
va_end(args);
logSerial.print(buf);
if (logSerial) {
logSerial.print(buf);
}
addToLogRingBuffer(buf);
}
std::string getLastLogs() {
std::string output;
for (size_t i = 0; i < MAX_LOG_LINES; i++) {
size_t idx = (logHead + i) % MAX_LOG_LINES;
if (logMessages[idx][0] != '\0') {
output += logMessages[idx];
}
}
return output;
}
void clearLastLogs() {
for (size_t i = 0; i < MAX_LOG_LINES; i++) {
logMessages[i][0] = '\0';
}
logHead = 0;
}

View File

@@ -2,6 +2,8 @@
#include <HardwareSerial.h>
#include <string>
/*
Define ENABLE_SERIAL_LOG to enable logging
Can be set in platformio.ini build_flags or as a compile definition
@@ -53,6 +55,9 @@ void logPrintf(const char* level, const char* origin, const char* format, ...);
#define LOG_INF(origin, format, ...)
#endif
std::string getLastLogs();
void clearLastLogs();
class MySerialImpl : public Print {
public:
void begin(unsigned long baud) { logSerial.begin(baud); }

137
lib/hal/HalSystem.cpp Normal file
View File

@@ -0,0 +1,137 @@
#include "HalSystem.h"
#include <string>
#include "Arduino.h"
#include "HalStorage.h"
#include "Logging.h"
#include "esp_debug_helpers.h"
#include "esp_private/esp_cpu_internal.h"
#include "esp_private/esp_system_attr.h"
#include "esp_private/panic_internal.h"
#define MAX_PANIC_STACK_DEPTH 32
RTC_NOINIT_ATTR char panicMessage[256];
RTC_NOINIT_ATTR HalSystem::StackFrame panicStack[MAX_PANIC_STACK_DEPTH];
extern "C" {
static DRAM_ATTR const char PANIC_REASON_UNKNOWN[] = "(unknown panic reason)";
void IRAM_ATTR __wrap_panic_abort(const char* message) {
if (!message) message = PANIC_REASON_UNKNOWN;
// IRAM-safe bounded copy (strncpy is not IRAM-safe in panic context)
int i = 0;
for (; i < (int)sizeof(panicMessage) - 1 && message[i]; i++) {
panicMessage[i] = message[i];
}
panicMessage[i] = '\0';
__real_panic_abort(message);
}
void IRAM_ATTR __wrap_panic_print_backtrace(const void* frame, int core) {
if (!frame) {
__real_panic_print_backtrace(frame, core);
return;
}
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
panicStack[i].sp = 0;
}
// Copied from components/esp_system/port/arch/riscv/panic_arch.c
uint32_t sp = (uint32_t)((RvExcFrame*)frame)->sp;
const int per_line = 8;
int depth = 0;
for (int x = 0; x < 1024; x += per_line * sizeof(uint32_t)) {
uint32_t* spp = (uint32_t*)(sp + x);
// panic_print_hex(sp + x);
// panic_print_str(": ");
panicStack[depth].sp = sp + x;
for (int y = 0; y < per_line; y++) {
// panic_print_str("0x");
// panic_print_hex(spp[y]);
// panic_print_str(y == per_line - 1 ? "\r\n" : " ");
panicStack[depth].spp[y] = spp[y];
}
depth++;
if (depth >= MAX_PANIC_STACK_DEPTH) {
break;
}
}
__real_panic_print_backtrace(frame, core);
}
}
namespace HalSystem {
void begin() {
// This is mostly for the first boot, we need to initialize the panic info and logs to empty state
// If we reboot from a panic state, we want to keep the panic info until we successfully dump it to the SD card, use
// `clearPanic()` to clear it after dumping
if (!isRebootFromPanic()) {
clearPanic();
}
}
void checkPanic() {
if (isRebootFromPanic()) {
auto panicInfo = getPanicInfo(true);
auto file = Storage.open("/crash_report.txt", O_WRITE | O_CREAT | O_TRUNC);
if (file) {
file.write(panicInfo.c_str(), panicInfo.size());
file.close();
LOG_INF("SYS", "Dumped panic info to SD card");
} else {
LOG_ERR("SYS", "Failed to open crash_report.txt for writing");
}
}
}
void clearPanic() {
panicMessage[0] = '\0';
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
panicStack[i].sp = 0;
}
clearLastLogs();
}
std::string getPanicInfo(bool full) {
if (!full) {
return panicMessage;
} else {
std::string info;
info += "CrossPoint version: " CROSSPOINT_VERSION;
info += "\n\nPanic reason: " + std::string(panicMessage);
info += "\n\nLast logs:\n" + getLastLogs();
info += "\n\nStack memory:\n";
auto toHex = [](uint32_t value) {
char buffer[9];
snprintf(buffer, sizeof(buffer), "%08X", value);
return std::string(buffer);
};
for (size_t i = 0; i < MAX_PANIC_STACK_DEPTH; i++) {
if (panicStack[i].sp == 0) {
break;
}
info += "0x" + toHex(panicStack[i].sp) + ": ";
for (size_t j = 0; j < 8; j++) {
info += "0x" + toHex(panicStack[i].spp[j]) + " ";
}
info += "\n";
}
return info;
}
}
bool isRebootFromPanic() {
const auto resetReason = esp_reset_reason();
return resetReason == ESP_RST_PANIC || resetReason == ESP_RST_CPU_LOCKUP;
}
} // namespace HalSystem

29
lib/hal/HalSystem.h Normal file
View File

@@ -0,0 +1,29 @@
#pragma once
#include <cstdint>
#include <string>
extern "C" {
void __real_panic_abort(const char* message);
void __wrap_panic_abort(const char* message);
void __real_panic_print_backtrace(const void* frame, int core);
void __wrap_panic_print_backtrace(const void* frame, int core);
}
namespace HalSystem {
struct StackFrame {
uint32_t sp;
uint32_t spp[8];
};
void begin();
// Dump panic info to SD card if necessary
void checkPanic();
void clearPanic();
std::string getPanicInfo(bool full = false);
bool isRebootFromPanic();
} // namespace HalSystem

View File

@@ -35,6 +35,7 @@ build_flags =
# Default is (320*4+1)*2=2562, we need more for larger images
-DPNG_MAX_BUFFERED_PIXELS=16416
-Wno-bidi-chars
-Wl,--wrap=panic_print_backtrace,--wrap=panic_abort
build_unflags =
-std=gnu++11

View File

@@ -6,6 +6,7 @@
#include <HalGPIO.h>
#include <HalPowerManager.h>
#include <HalStorage.h>
#include <HalSystem.h>
#include <I18n.h>
#include <Logging.h>
#include <SPI.h>
@@ -227,6 +228,7 @@ void setupDisplayAndFonts() {
void setup() {
t1 = millis();
HalSystem::begin();
gpio.begin();
powerManager.begin();
@@ -249,6 +251,9 @@ void setup() {
return;
}
HalSystem::checkPanic();
HalSystem::clearPanic(); // TODO: move this to an activity when we have one to display the panic info
SETTINGS.loadFromFile();
I18N.loadSettings();
KOREADER_STORE.loadFromFile();