<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: chinmay-02</title>
    <description>The latest articles on DEV Community by chinmay-02 (@chinmay02).</description>
    <link>https://dev.to/chinmay02</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3971233%2F57f34ad9-3c38-4a81-8024-d2c77b03e40c.png</url>
      <title>DEV Community: chinmay-02</title>
      <link>https://dev.to/chinmay02</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chinmay02"/>
    <language>en</language>
    <item>
      <title>I Built Sentry.io for Microcontrollers — Here's How the Crash Capture Actually Works</title>
      <dc:creator>chinmay-02</dc:creator>
      <pubDate>Sun, 07 Jun 2026 06:59:46 +0000</pubDate>
      <link>https://dev.to/chinmay02/i-built-sentryio-for-microcontrollers-heres-how-the-crash-capture-actually-works-5gd6</link>
      <guid>https://dev.to/chinmay02/i-built-sentryio-for-microcontrollers-heres-how-the-crash-capture-actually-works-5gd6</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Dashboard&lt;/strong&gt; &lt;a href="https://firmwaresentry.io" rel="noopener noreferrer"&gt;firmwaresentry.io&lt;/a&gt; · &lt;br&gt;
&lt;strong&gt;SDK:&lt;/strong&gt; &lt;a href="https://github.com/chinmay-02/firmware-sentry-sdk" rel="noopener noreferrer"&gt;github.com/chinmay-02/firmware-sentry-sdk&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Interactive demo:&lt;/strong&gt; Play with the live dashboard right here:&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/chinmay-gupta-the-styleful/embed/QwGzyKq?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Eight years into firmware engineering — Product companies, IoT startups, contract work — and the debugging story never changed. Device crashes in the field. Customer calls. You ask them to reproduce it. They can't. You squint at a log file with no context. You ship a guess.&lt;/p&gt;

&lt;p&gt;The worst part: the information &lt;em&gt;was there&lt;/em&gt;. The CPU captured it. The fault registers had the exact address, the exact cause, the exact task. It just didn't survive the reboot.&lt;/p&gt;

&lt;p&gt;That's the problem I set out to fix. The result is &lt;strong&gt;Firmware Sentry&lt;/strong&gt; — an open-source C SDK that captures the full fault state on crash, persists it across reset, sends it to the cloud, and gets Claude AI to diagnose it for you. Like Sentry.io, but for microcontrollers.&lt;/p&gt;

&lt;p&gt;This post is about the hardest part: getting crash data to survive a reboot and reliably transmit back to the cloud.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Core Problem: Reboot Destroys Evidence
&lt;/h2&gt;

&lt;p&gt;When an ESP32 panics, here's the execution chain:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;CPU takes an exception&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;esp_panic_handler()&lt;/code&gt; is called&lt;/li&gt;
&lt;li&gt;It prints a backtrace to UART&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hard reset&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything in DRAM is zeroed on reset by the C startup code. By the time your &lt;code&gt;app_main()&lt;/code&gt; runs again, the registers are gone, the stack is overwritten, the task name is lost.&lt;/p&gt;

&lt;p&gt;The only memory that survives a software reset on ESP32 is &lt;strong&gt;RTC slow memory&lt;/strong&gt; — specifically &lt;code&gt;RTC_NOINIT_ATTR&lt;/code&gt; variables, which the startup code explicitly skips during zero-fill.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// These survive SW_CPU_RESET. They do NOT survive power-on reset.&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;s_crash_magic&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;s_crash_pc&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;s_crash_cause&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;s_crash_sp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;s_crash_a0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;s_crash_vaddr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;s_crash_uptime&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;s_crash_task_hwm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;     &lt;span class="n"&gt;s_crash_task&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;// Breadcrumb ring buffer — also survives reset&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="n"&gt;fs_event_t&lt;/span&gt; &lt;span class="n"&gt;s_rtc_events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;        &lt;span class="n"&gt;s_rtc_event_head&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;RTC_NOINIT_ATTR&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;        &lt;span class="n"&gt;s_rtc_event_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Total RTC usage: ~601 bytes out of 8KB available&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The magic number check is critical.&lt;/strong&gt; On power-on reset, RTC memory contains random garbage. You need a sentinel value to know whether the data you're reading is a real crash or uninitialized noise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#define FS_CRASH_MAGIC 0xDEADC0DE
&lt;/span&gt;
&lt;span class="n"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;fs_hal_esp32_has_pending_crash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s_crash_magic&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;FS_CRASH_MAGIC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On power-on, the magic won't match. On software reset after a crash, it will. This single check is the gate between "real crash data" and "random bytes."&lt;/p&gt;




&lt;h2&gt;
  
  
  Intercepting the Panic Handler
&lt;/h2&gt;

&lt;p&gt;The standard approach — overriding &lt;code&gt;HardFault_Handler&lt;/code&gt; on ARM — doesn't exist on Xtensa. ESP-IDF's panic handler is a regular C function. But we can use the GNU linker &lt;code&gt;--wrap&lt;/code&gt; trick to intercept it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In your CMakeLists.txt&lt;/span&gt;
&lt;span class="nb"&gt;target_link_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;COMPONENT_TARGET&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; INTERFACE
    -Wl,--wrap=esp_panic_handler
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Our interceptor. Called before ESP-IDF's handler.&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;__wrap_esp_panic_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;panic_info_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Capture everything while we still can&lt;/span&gt;
    &lt;span class="n"&gt;fs_hal_esp32_capture_crash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Let ESP-IDF print its backtrace and reboot normally&lt;/span&gt;
    &lt;span class="n"&gt;__real_esp_panic_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is clean — we don't replace ESP-IDF's handler, we wrap it. The original handler still runs, prints its backtrace to UART, and performs the reset. We just intercept it first.&lt;/p&gt;




&lt;h2&gt;
  
  
  Extracting Registers from &lt;code&gt;panic_info_t&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is where it gets platform-specific. The &lt;code&gt;panic_info_t&lt;/code&gt; struct contains a &lt;code&gt;void *frame&lt;/code&gt; pointer, but what that frame actually is depends on the CPU architecture.&lt;/p&gt;

&lt;p&gt;On ESP32 (Xtensa LX6):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;fs_hal_esp32_capture_crash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;panic_info_t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Get PC from the panic info — this matches exactly what&lt;/span&gt;
    &lt;span class="c1"&gt;// IDF's monitor tool reports&lt;/span&gt;
    &lt;span class="n"&gt;s_crash_pc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;panic_get_address&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Cast to Xtensa exception frame for the rest&lt;/span&gt;
    &lt;span class="n"&gt;XtExcFrame&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;XtExcFrame&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;s_crash_sp&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;a1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// A1 is the stack pointer on Xtensa&lt;/span&gt;
    &lt;span class="n"&gt;s_crash_a0&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;a0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// A0 is the return address / LR equivalent&lt;/span&gt;
    &lt;span class="n"&gt;s_crash_cause&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;exccause&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;s_crash_vaddr&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;frame&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;excvaddr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// The bad memory address on a load/store fault&lt;/span&gt;

    &lt;span class="c1"&gt;// Capture runtime context&lt;/span&gt;
    &lt;span class="n"&gt;s_crash_uptime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;esp_timer_get_time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Get the name of the FreeRTOS task that crashed&lt;/span&gt;
    &lt;span class="n"&gt;TaskHandle_t&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xTaskGetCurrentTaskHandle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pcTaskGetName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;strncpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_crash_task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_crash_task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;s_crash_task_hwm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;uxTaskGetStackHighWaterMark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Set the magic — now this data is valid&lt;/span&gt;
    &lt;span class="n"&gt;s_crash_magic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FS_CRASH_MAGIC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight here: &lt;code&gt;frame-&amp;gt;a1&lt;/code&gt; on Xtensa is the stack pointer, not &lt;code&gt;a0&lt;/code&gt;. On ARM this would be different. The HAL layer is exactly what abstracts this difference — the same &lt;code&gt;fs_init()&lt;/code&gt; call works on both architectures because each platform's HAL knows its own register layout.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Breadcrumb System
&lt;/h2&gt;

&lt;p&gt;Registers tell you &lt;em&gt;where&lt;/em&gt; it crashed. Breadcrumbs tell you &lt;em&gt;what it was doing&lt;/em&gt; before it crashed.&lt;/p&gt;

&lt;p&gt;The concept is simple: your firmware logs short string events to a ring buffer in RTC memory throughout its operation. When it crashes, those events survive the reset and get included in the crash report.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Scatter these throughout your application code&lt;/span&gt;
&lt;span class="n"&gt;fs_log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"boot"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;fs_log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"nvs_init_ok"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;fs_log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"wifi_connected"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;fs_log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"mqtt_sub_ok"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;fs_log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ota_check_start"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;fs_log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ota_dl_begin"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// ...crash occurs here&lt;/span&gt;

&lt;span class="c1"&gt;// Cloud dashboard shows:&lt;/span&gt;
&lt;span class="c1"&gt;// boot → nvs_init_ok → wifi_connected → mqtt_sub_ok → ota_check_start → ota_dl_begin ← crash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ring buffer implementation uses integer head/count indices (also in RTC NOINIT) and overwrites the oldest entry when full:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;fs_log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!*&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_rtc_event_head&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;s_rtc_event_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;FS_MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;strncpy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_rtc_events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FS_MAX_EVENT_LEN&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;s_rtc_events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FS_MAX_EVENT_LEN&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sc"&gt;'\0'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;s_rtc_events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;esp_timer_get_time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_rtc_event_count&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;FS_MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;s_rtc_event_count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;// Ring is full — overwrite oldest&lt;/span&gt;
        &lt;span class="n"&gt;s_rtc_event_head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_rtc_event_head&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;FS_MAX_EVENTS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No malloc. No mutex. No RTOS dependency. Works from any context, including interrupt handlers (with the caveat that you're in a faulting state — you want this to be as simple as possible).&lt;/p&gt;




&lt;h2&gt;
  
  
  The Build Hash: Linking Crash to Binary
&lt;/h2&gt;

&lt;p&gt;One of the subtle problems in crash reporting is knowing &lt;em&gt;which firmware build&lt;/em&gt; a crash came from. Firmware version strings are developer-set and often wrong ("I forgot to bump the version"). Build hashes are automatic.&lt;/p&gt;

&lt;p&gt;On ESP32, the IDF computes a SHA256 hash of the entire firmware binary and stores it in the app descriptor. We capture it at startup with a constructor attribute:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;s_build_hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;  &lt;span class="c1"&gt;// 8 hex chars + null&lt;/span&gt;

&lt;span class="n"&gt;__attribute__&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;capture_build_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;uint8_t&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="n"&gt;esp_app_get_elf_sha256&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;snprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_build_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s_build_hash&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
             &lt;span class="s"&gt;"%02x%02x%02x%02x"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sha256&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;__attribute__((constructor))&lt;/code&gt; fires before &lt;code&gt;app_main()&lt;/code&gt;. This means the hash is captured before any application code runs — even before the RTOS scheduler starts. The first 8 hex characters (32 bits of SHA256) give you enough uniqueness to identify a build without sending the full hash.&lt;/p&gt;

&lt;p&gt;This hash goes into every crash report. When you upload your ELF file to the cloud dashboard, it's indexed by this hash. Symbol resolution (&lt;code&gt;addr2line&lt;/code&gt; over DWARF) links the crash's PC address to the exact function name, file, and line number in that specific build.&lt;/p&gt;




&lt;h2&gt;
  
  
  Sending the Crash
&lt;/h2&gt;

&lt;p&gt;On boot, &lt;code&gt;fs_init()&lt;/code&gt; checks the magic number. If there's a pending crash, it bundles everything into a JSON payload and POSTs it over HTTPS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;esp_err_t&lt;/span&gt; &lt;span class="nf"&gt;fs_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... wifi must be up before calling this ...&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fs_hal_has_pending_crash&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;ESP_LOGI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Pending crash detected — sending to cloud"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="n"&gt;fs_build_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

        &lt;span class="n"&gt;esp_err_t&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fs_https_post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FS_ENDPOINT_CRASHES&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;ESP_OK&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;fs_hal_clear_crash&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// Clear magic so we don't send again&lt;/span&gt;
            &lt;span class="n"&gt;ESP_LOGI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Crash report sent successfully"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ESP_OK&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payload includes every captured register, the breadcrumb trail, task name, stack high watermark, uptime at crash, build hash, and firmware version. The cloud endpoint receives it, stores it in Postgres, and immediately triggers AI diagnosis in the background.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Happens on the Cloud
&lt;/h2&gt;

&lt;p&gt;The crash lands in Supabase PostgreSQL with all the raw registers. A background task then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Checks if an ELF file exists for this build hash&lt;/li&gt;
&lt;li&gt;If yes: runs &lt;code&gt;pyelftools&lt;/code&gt; DWARF lookup on the PC address to get function name, file, and line number&lt;/li&gt;
&lt;li&gt;Builds a context-rich prompt and calls Claude Sonnet&lt;/li&gt;
&lt;li&gt;Stores the AI diagnosis back to the crash record&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The AI prompt includes the decoded CFSR/HFSR fault bits in human-readable form (not raw hex), the breadcrumb trail, the resolved function name if available, and the task stack high watermark. That last one is surprisingly useful — a high watermark near zero means stack overflow, which explains a lot of seemingly random crashes.&lt;/p&gt;

&lt;p&gt;Claude's response looks like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Root cause: Null pointer dereference in &lt;code&gt;mqtt_publish_task&lt;/code&gt; at &lt;code&gt;mqtt_client.c:247&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The EXCVADDR register (0x00000004) indicates a load from address 0x4, which is a null pointer plus 4-byte offset — consistent with dereferencing a struct pointer where the struct's second field is being accessed. The breadcrumb trail shows the device had just completed an OTA download (&lt;code&gt;ota_dl_begin&lt;/code&gt; at +3,847ms) before crashing in &lt;code&gt;mqtt_publish_task&lt;/code&gt;. This suggests the OTA process may have corrupted or freed a message queue handle that the MQTT task continued to use. Check that OTA completion properly re-initializes the MQTT client handle before the publisher task resumes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's a real diagnosis, not a template. It uses the actual register values and breadcrumb sequence.&lt;/p&gt;




&lt;h2&gt;
  
  
  ARM Cortex-M: The Same Interface, Different Internals
&lt;/h2&gt;

&lt;p&gt;The same &lt;code&gt;fs_init()&lt;/code&gt; / &lt;code&gt;fs_log_event()&lt;/code&gt; API works on STM32 because the HAL layer abstracts the differences:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;ESP32 (Xtensa)&lt;/th&gt;
&lt;th&gt;STM32L476 (Cortex-M4)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fault hook&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;__wrap_esp_panic_handler&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;HardFault_Handler&lt;/code&gt; weak override&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Registers&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;EXCCAUSE, EXCVADDR, A0-A15&lt;/td&gt;
&lt;td&gt;CFSR, HFSR, MMFAR, BFAR, R0-R12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Persistent storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;RTC_NOINIT_ATTR&lt;/code&gt; (~8KB RTC RAM)&lt;/td&gt;
&lt;td&gt;SRAM2 top 2KB at &lt;code&gt;0x10007800&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transport&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTTPS direct (WiFi)&lt;/td&gt;
&lt;td&gt;UART → gateway.py → cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;API key storage&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;NVS flash&lt;/td&gt;
&lt;td&gt;Flash page 255&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The SRAM2 placement deserves a call-out: C startup code zeros the &lt;em&gt;entire&lt;/em&gt; SRAM2 region on reset. You can't just put your persistent data anywhere in SRAM2 — you have to reserve a region in your linker script that the startup code explicitly skips. Getting this wrong means your crash data silently vanishes on every reset. This took a full debugging session on real hardware to catch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Open Source
&lt;/h2&gt;

&lt;p&gt;The SDK is Apache 2.0 licensed. It's designed to drop into an existing ESP-IDF or STM32 HAL project with minimal changes to &lt;code&gt;CMakeLists.txt&lt;/code&gt; (ESP32) or &lt;code&gt;Core/Src&lt;/code&gt; (STM32).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/chinmay-02/firmware-sentry-sdk" rel="noopener noreferrer"&gt;GitHub: firmware-sentry-sdk&lt;/a&gt;&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ESP32 — three lines to integrate&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;"firmware_sentry.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;app_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;nvs_flash_init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// ... wifi connect ...&lt;/span&gt;
    &lt;span class="n"&gt;fs_init&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// Sends any pending crash, registers fault hooks&lt;/span&gt;

    &lt;span class="c1"&gt;// Scatter breadcrumbs throughout your code&lt;/span&gt;
    &lt;span class="n"&gt;fs_log_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"boot_complete"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Your application starts here&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cloud dashboard is at &lt;strong&gt;&lt;a href="https://firmware-sentry.vercel.app" rel="noopener noreferrer"&gt;firmware-sentry.vercel.app&lt;/a&gt;&lt;/strong&gt; — create a free account, add a device, drop the SDK in.&lt;/p&gt;




&lt;p&gt;If you've ever shipped a device that crashed in the field and had no idea why, I'd love to hear about it in the comments. What was your worst debugging war story?&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SDK:&lt;/strong&gt; &lt;a href="https://github.com/chinmay-02/firmware-sentry-sdk" rel="noopener noreferrer"&gt;github.com/chinmay-02/firmware-sentry-sdk&lt;/a&gt; — drop into ESP-IDF or STM32 HAL in minutes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dashboard:&lt;/strong&gt; &lt;a href="https://firmwaresentry.io" rel="noopener noreferrer"&gt;firmwaresentry.io&lt;/a&gt; — free tier, no credit card&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interactive demo:&lt;/strong&gt; Play with the live dashboard right here:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/chinmay-gupta-the-styleful/embed/QwGzyKq?height=600&amp;amp;default-tab=result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Follow for the next post: how symbol resolution works with DWARF and pyelftools, and why amalgamated builds break it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>iot</category>
      <category>esp32</category>
      <category>firmware</category>
      <category>stm</category>
    </item>
  </channel>
</rss>
