- Split text into lines before calling process_stacktrace in client.py,
since process_stacktrace uses re.match and expects individual lines.
- Use memcpy instead of direct pointer cast for reading the instruction
before a return address, since RISC-V C extension means code addresses
are only 2-byte aligned and addr-4 may not be 4-byte aligned.
Co-Authored-By: J. Nick Koston <nick@koston.org>
- Add compressed c.jalr (2-byte) instruction check alongside 4-byte
JAL/JALR, since ESP32 RISC-V targets have the C extension enabled.
- Track register-sourced entries (MEPC/RA) separately from stack-scanned
ones, and skip return-address validation for register entries since
they are known-good values from the exception frame.
Co-Authored-By: J. Nick Koston <nick@koston.org>
- s_raw_crash_data and s_crash_data_valid must be mutable globals
- __real_/__wrap_ names are mandated by the --wrap linker mechanism
Co-Authored-By: J. Nick Koston <nick@koston.org>
IDF doesn't decode RISC-V backtraces on the console either — it just
dumps raw stack memory. Our crash handler provides a better trace.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Stack scanning captures any value that looks like a code address, which
includes false positives. At log time (flash cache is up), validate each
address by checking if the preceding instruction is a JAL/JALR with
rd=ra. This filters spurious entries like FreeRTOS internals that happen
to be on the stack but aren't part of the actual call chain.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Version first ensures future firmware can always identify the struct
layout without depending on any other field positions.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Allows future firmware to detect and skip incompatible crash data
layouts. Placed alongside backtrace_count to avoid adding padding.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Moving backtrace_count before the variable-length backtrace array
ensures magic, pc, and count are at fixed offsets regardless of
MAX_BACKTRACE value. This makes the .noinit data readable across
firmware versions that may change the max backtrace depth.
Co-Authored-By: J. Nick Koston <nick@koston.org>
Saves 76 bytes of RAM by removing the validated BSS copy and reading
directly from the .noinit struct after magic validation. A single bool
tracks whether valid crash data was found this boot.
Co-Authored-By: J. Nick Koston <nick@koston.org>
8 frames was cutting off useful call stack information. 16 frames
costs an additional 32 bytes of .noinit RAM and covers typical
ESPHome call chains which can be 10-12 frames deep.
The esphome logs command via API wasn't running process_stacktrace
on received log lines, so crash handler backtrace addresses were
displayed but not decoded with addr2line.
- Make s_raw_crash_data static with inline .noinit definition (no extern needed)
- Remove valid=false clearing from crash_handler_log() so both serial (boot)
and API (subscribe) paths can emit the crash data
- Clear valid flag after logging to prevent re-logging on API reconnects
- Cache esp_cpu_process_stack_pc result to avoid redundant call
- Remove unused <cstdint> include from header
When an ESP32 crashes, the backtrace is printed to UART and lost.
Users without a serial cable never see this diagnostic information.
This adds a crash handler that:
- Intercepts esp_panic_handler() via --wrap linker flag
- Captures the faulting PC and backtrace into .noinit memory
- Supports both Xtensa (ESP32/S2/S3) and RISC-V (C3/C6/H2/C2)
- Logs crash data at boot via ESP_LOGE (serial output)
- Re-logs when HA subscribes to logs (visible in HA log viewer)
- Adds CLI stacktrace decoding for the new log format
Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>