<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>blog.tedder.dev</title><link>https://blog.tedder.dev/</link><description>code, 3d, electronics, domestic foo, assorted geekery</description><atom:link href="https://blog.tedder.dev/rss.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 &lt;a href="mailto:ted@tedder.me"&gt;tedder&lt;/a&gt; </copyright><lastBuildDate>Sun, 16 Aug 2026 21:37:26 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Building a METAR aviation weather map on an ESP32</title><link>https://blog.tedder.dev/posts/metar-map-esp32/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;I fly, which means I look at weather constantly. METARs — aviation routine weather reports — tell you ceiling, visibility, and flight category at any reporting station. The colors VFR/MVFR/IFR/LIFR map directly to green/blue/red/magenta in most flight planning apps. A while ago I decided to make that visual and physical: an LED strip that stays on my desk and shows current flight conditions at airports along my usual routes.&lt;/p&gt;
&lt;p&gt;Here's how it evolved, and what I learned.&lt;/p&gt;
&lt;h3&gt;Hardware&lt;/h3&gt;
&lt;p&gt;The platform is an &lt;a href="https://www.adafruit.com/product/5325"&gt;Adafruit QT Py ESP32-S2&lt;/a&gt;. It's small, has native USB, runs CircuitPython or ESP-IDF, and has enough flash and RAM for a WiFi-connected LED controller. A WS2812B strip plugs into one of the analog pins; an SSD1306 OLED 128×32 hangs on I2C for status readout.&lt;/p&gt;
&lt;p&gt;The strip has one LED per airport. Right now I track a handful of stations — local fields and a couple of en-route ones. The OLED shows the last update time and a count of how many airports had valid METARs.&lt;/p&gt;
&lt;h3&gt;The backend: don't do METAR parsing on the microcontroller&lt;/h3&gt;
&lt;p&gt;Aviation weather APIs return JSON blobs or text, require TLS, and occasionally change schema. Parsing that on a microcontroller is painful — you burn flash on JSON libraries, you fight TLS certificate chains, and every API hiccup means you're debugging on a device with no debugger.&lt;/p&gt;
&lt;p&gt;My approach: offload all of that to a Lambda@Edge function. The microcontroller sends a single HTTP request with a list of airport codes and gets back a simple CSV:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;KPDX,VFR,0-127-0,8,0
KTTD,VFR,0-127-0,12,0
KHIO,MVFR,0-0-127,24,0
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Fields: ICAO code, flight category, RGB color tuple, age in minutes, stale flag. The microcontroller splits the line, applies the color directly. No parsing, no JSON, no TLS from the device side (Lambda handles it outbound). This architecture has been solid — the device just asks "what color are these airports" and gets an answer.&lt;/p&gt;
&lt;h3&gt;Flight categories to LED colors&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VFR&lt;/td&gt;
&lt;td&gt;&amp;gt; 3000ft ceiling, &amp;gt; 5sm vis&lt;/td&gt;
&lt;td&gt;Green (0, 127, 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MVFR&lt;/td&gt;
&lt;td&gt;1000–3000ft or 3–5sm&lt;/td&gt;
&lt;td&gt;Blue (0, 0, 127)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IFR&lt;/td&gt;
&lt;td&gt;500–1000ft or 1–3sm&lt;/td&gt;
&lt;td&gt;Red (127, 0, 0)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LIFR&lt;/td&gt;
&lt;td&gt;&amp;lt; 500ft or &amp;lt; 1sm&lt;/td&gt;
&lt;td&gt;Magenta (255, 22, 199)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The stale flag comes back set if the METAR is older than a threshold. I dim stale airports slightly to indicate "this data is old."&lt;/p&gt;
&lt;h3&gt;CircuitPython phase&lt;/h3&gt;
&lt;p&gt;The first version ran CircuitPython. CircuitPython is genuinely great for a project like this: the REPL is fast, you edit files over USB mass storage without any flashing ceremony, the Adafruit ecosystem has everything you need, and iteration is just save-file-watch-reload.&lt;/p&gt;
&lt;p&gt;It held up well through about a year and a dozen feature additions. I added a web server for live airport configuration, NTP sync, a debug flag bitmask stored in NVM (persistent across reboots), a watchdog timer, and structured logging.&lt;/p&gt;
&lt;p&gt;Then CircuitPython's networking started biting me.&lt;/p&gt;
&lt;h4&gt;The DNS problem&lt;/h4&gt;
&lt;p&gt;On certain builds of CircuitPython for the ESP32-S2, lwIP's DNS resolver would silently fail. The device appeared connected — it had an IP, it could ping — but &lt;code&gt;getaddrinfo()&lt;/code&gt; would hang or return errors on the first few calls after boot. The workaround I ended up with was a &lt;code&gt;SmartPool&lt;/code&gt; wrapper around &lt;code&gt;socketpool.SocketPool&lt;/code&gt; that intercepts &lt;code&gt;getaddrinfo()&lt;/code&gt; and falls back to raw UDP DNS against Cloudflare and Google if lwIP's resolver fails:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;SmartPool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getaddrinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getaddrinfo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Raw UDP DNS fallback to 1.1.1.1, then 8.8.8.8&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_raw_dns_lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;All the Adafruit libraries (&lt;code&gt;adafruit_requests&lt;/code&gt;, &lt;code&gt;adafruit_ntp&lt;/code&gt;, etc.) receive &lt;code&gt;SmartPool&lt;/code&gt; instead of the raw pool, so the fallback is transparent. It worked — but it's the kind of thing that makes you wonder if you're approaching the floor of what CircuitPython's network stack can reliably support.&lt;/p&gt;
&lt;h4&gt;WiFi roaming detection&lt;/h4&gt;
&lt;p&gt;The device is always-on and needs to survive WiFi disconnects. CircuitPython doesn't give you a clean reconnect event — you discover the connection is dead when the next HTTP call throws. My workaround: catch the exception, set &lt;code&gt;requests = None&lt;/code&gt;, detect &lt;code&gt;None&lt;/code&gt; at the top of the fetch loop, reconnect, reinitialize the session. Works, but fragile.&lt;/p&gt;
&lt;h3&gt;Production reliability: debug flags and watchdog&lt;/h3&gt;
&lt;p&gt;One pattern I've carried across all my embedded projects: a 32-bit debug bitmask stored in persistent memory, and a hardware watchdog.&lt;/p&gt;
&lt;p&gt;The bitmask lets me POST the device's health history to a monitoring endpoint. Each bit tracks a category of failure — WiFi connect failures, HTTP exceptions, NTP failures, NVM write failures. Even if the device reboots and the exception is gone, the bit stays set until explicitly cleared. When I look at the debug page later, I can see "WiFi connect failed 3+ times" even if the device is healthy now.&lt;/p&gt;
&lt;p&gt;The watchdog is set to 60 minutes (this is an always-on device, not deep-sleep). It gets fed in the WiFi check, the METAR fetch, and the main loop. If anything hangs — which on CircuitPython occasionally happens with &lt;code&gt;adafruit_requests&lt;/code&gt; on flaky connections — the device reboots rather than sitting stuck forever.&lt;/p&gt;
&lt;h3&gt;ESP-IDF rewrite&lt;/h3&gt;
&lt;p&gt;The CircuitPython workarounds accumulated enough that I started an ESP-IDF C rewrite. The motivating factors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ESP-IDF WiFi is reliable in ways CircuitPython's isn't on this chip. No SmartPool, no &lt;code&gt;requests = None&lt;/code&gt; dance, no lwIP DNS workarounds.&lt;/li&gt;
&lt;li&gt;OTA updates. CircuitPython devices update by copying a file over USB. ESP-IDF devices can pull firmware from an HTTPS endpoint and self-update. For a device that sits on my desk, this matters less — but it's a pattern I use across all my ESP32 projects now.&lt;/li&gt;
&lt;li&gt;Performance. The OLED rendering in CircuitPython is noticeably slow at higher refresh rates. In C it's instant.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The backend CSV format and airport list format stay identical. The behavior from outside — what the LEDs show, what the debug page returns — is the same. The rewrite is a platform swap, not a feature change.&lt;/p&gt;
&lt;h3&gt;What I'd build differently&lt;/h3&gt;
&lt;p&gt;The Lambda@Edge backend was the best decision I made early. Keeping the wire protocol simple (plain HTTP, CSV responses) meant the firmware stayed simple. Anytime the METAR API changed, I updated the Lambda — the device didn't know anything changed.&lt;/p&gt;
&lt;p&gt;If I were starting over, I'd skip CircuitPython for a project that's always-on and WiFi-dependent. CircuitPython is excellent for sensors, USB gadgets, interactive prototyping. For a device that needs to run unattended for months on a network connection, ESP-IDF's networking stack is just more reliable.&lt;/p&gt;
&lt;p&gt;The debug flag pattern is worth copying to any embedded project. Persistent error history — even across reboots — is invaluable when you're trying to figure out why a device that's been running for a week started acting up.&lt;/p&gt;</description><category>aviation</category><category>circuitpython</category><category>esp32</category><category>hardware</category><category>led</category><category>python</category><guid>https://blog.tedder.dev/posts/metar-map-esp32/</guid><pubDate>Sun, 16 Aug 2026 16:00:00 GMT</pubDate></item><item><title>ESP-IDF 6.0: what changed, and whether to upgrade from 5.5</title><link>https://blog.tedder.dev/posts/esp-idf-6-migration-v5-to-v6/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;After running a uniform fleet of ESP32 devices on &lt;strong&gt;ESP-IDF v5.5.4&lt;/strong&gt; for months, I evaluated the jump to &lt;strong&gt;v6.0.2&lt;/strong&gt;. The short answer: no urgency for existing projects, but the upgrade is clean and v6 is worth choosing for new work.&lt;/p&gt;
&lt;h3&gt;What got better in v6&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/picolibc/picolibc"&gt;Picolibc&lt;/a&gt; replaces Newlib&lt;/strong&gt; — This is the headline win. Picolibc is smaller (flash), faster, and uses less RAM. Free win on all chips, especially memory-constrained S2 and original ESP32. &lt;a href="https://github.com/picolibc/picolibc#why-picolibc"&gt;See the Picolibc vs Newlib comparison.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New chips and matured C6 support&lt;/strong&gt; — C5, C61 (preview), and full ESP32-C6 support. The C6 in particular is where v6 shines: &lt;a href="https://en.wikipedia.org/wiki/IEEE_802.11ax"&gt;Wi-Fi 6 (802.11ax)&lt;/a&gt; with &lt;a href="https://www.espressif.com/en/news/ESP32-C6-TWT"&gt;TWT (Target Wake Time)&lt;/a&gt; for connected low-power operation, native &lt;a href="https://en.wikipedia.org/wiki/IEEE_802.15.4"&gt;802.15.4&lt;/a&gt; (&lt;a href="https://www.threadgroup.org/"&gt;Thread&lt;/a&gt;/&lt;a href="https://www.zigbeealliance.org/"&gt;Zigbee&lt;/a&gt;/&lt;a href="https://csa-iot.org/"&gt;Matter&lt;/a&gt;), and a genuine &lt;strong&gt;low-power core&lt;/strong&gt; (not a tiny ULP helper like S2/S3 have).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Recovery bootloader (C5/C61 only)&lt;/strong&gt; — Safe OTA of the bootloader itself, not just the app. If the bootloader write fails or loses power mid-flash, the chip boots a recovery copy from a dedicated partition instead of bricking. See &lt;a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32c61/api-reference/system/misc_system_api.html#bootloader-ota"&gt;ESP-IDF bootloader OTA docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Wi-Fi driver robustness&lt;/strong&gt; — &lt;a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-guides/wifi.html#wpa3-support"&gt;WPA3 mixed-mode AP&lt;/a&gt; (SoftAP serves WPA3 but still accepts WPA2-PSK clients), &lt;a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/network/esp_wifi.html"&gt;USD (peer discovery)&lt;/a&gt;, general reconnect/roaming improvements.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tooling&lt;/strong&gt; — Installation Manager (EIM) for managing versions side-by-side, MCP server support, &lt;code&gt;idf.py&lt;/code&gt; CLI extensions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Active development&lt;/strong&gt; — v5.5.x is trending maintenance-only. v6 is where new features land.&lt;/p&gt;
&lt;h3&gt;What breaks (or requires attention)&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Warnings are now errors by default.&lt;/strong&gt; This is the most likely migration friction. Each of your 13 projects may need warning cleanup on the first v6 build—either fix them or re-disable the flag. &lt;a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32/migration-guides/release-6.x/6.0/index.html"&gt;See the official migration guide.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Toolchain bumps&lt;/strong&gt; — CMake 3.22.1 minimum, esp-idf-kconfig v3 (Kconfig syntax changes, only matters if you have custom Kconfig).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;idf.py&lt;/code&gt; requires &lt;code&gt;-p&lt;/code&gt; flag for eFuse commands.&lt;/strong&gt; If you have automation that does &lt;code&gt;idf.py burn_efuse&lt;/code&gt; or similar without specifying the port, it breaks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Legacy drivers removed&lt;/strong&gt; — ADC, DAC, I2S, Timer, PCNT, MCPWM, RMT, temp-sensor drivers are gone. I scanned my fleet; none use them (though RMT's &lt;code&gt;esp_led_strip&lt;/code&gt; component is NOT removed, and &lt;code&gt;driver/ledc.h&lt;/code&gt; for PWM is fine). Check your projects for raw &lt;code&gt;driver/adc.h&lt;/code&gt;, &lt;code&gt;driver/dac.h&lt;/code&gt;, etc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deep sleep GPIO wake on C6: no EXT0, and the wakeup-cause API changed.&lt;/strong&gt; Hit this building real &lt;code&gt;esp_deep_sleep()&lt;/code&gt; support for a battery doorbell button on C6. Two separate things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;esp_sleep_enable_ext0_wakeup()&lt;/code&gt; doesn't exist on your build if you're targeting C6 — it's compiled out behind &lt;code&gt;#if SOC_PM_SUPPORT_EXT0_WAKEUP&lt;/code&gt;, and that capability is ESP32/S2/S3-only. C6 (and H2) only get &lt;code&gt;esp_sleep_enable_ext1_wakeup_io()&lt;/code&gt;, the multi-pin bitmask API, even for a single GPIO: &lt;code&gt;esp_sleep_enable_ext1_wakeup_io(1ULL &amp;lt;&amp;lt; GPIO_NUM, ESP_EXT1_WAKEUP_ANY_LOW)&lt;/code&gt;. Every ESP-IDF example and blog post written against classic ESP32 uses EXT0 for "wake on one pin" — none of that code compiles unmodified on C6, and the failure is a build error (&lt;code&gt;implicit declaration of function&lt;/code&gt;), not a runtime surprise, so at least you find out immediately.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;esp_sleep_get_wakeup_cause()&lt;/code&gt; (singular, returns one enum) is deprecated in v6 in favor of &lt;code&gt;esp_sleep_get_wakeup_causes()&lt;/code&gt; (plural, returns a &lt;code&gt;uint32_t&lt;/code&gt; bitmask — sleep can now report multiple simultaneous wake reasons). Old code still compiles, just warns. Fix is mechanical: &lt;code&gt;causes &amp;amp; BIT(ESP_SLEEP_WAKEUP_EXT1)&lt;/code&gt; instead of &lt;code&gt;cause == ESP_SLEEP_WAKEUP_EXT1&lt;/code&gt;. IDF's own &lt;code&gt;examples/system/deep_sleep_wake_stub&lt;/code&gt; already uses the new form if you want a reference.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Picolibc differences&lt;/strong&gt; — The libc swap can surface subtle printf, locale, float-formatting, or stdio differences. Low-risk, but test rather than assume.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Effort&lt;/strong&gt; — Multiplied by the number of projects. Re-running &lt;code&gt;idf.py set-target&lt;/code&gt;, rebuilding, re-OTA'ing 13 devices takes time. Not a blocker, just real.&lt;/p&gt;
&lt;h3&gt;My migration strategy&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Staged trial: install v6.0.2 side-by-side with v5.5.4.&lt;/strong&gt; Start with one low-stakes project (e.g., a development board or a device you can easily recover), clear warnings-as-errors fallout, confirm OTA works, then roll the rest opportunistically.&lt;/p&gt;
&lt;p&gt;Keep v5.5.4 until all projects are confirmed running on v6.0.2. That gives a fallback if something unexpected surfaces.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;For new projects, start directly on v6.0.2.&lt;/strong&gt; No point dragging v5.5.4 into new work.&lt;/p&gt;
&lt;h3&gt;Bootloader OTA: the chip-specific caveat&lt;/h3&gt;
&lt;p&gt;If you're planning &lt;strong&gt;safe OTA of the bootloader itself&lt;/strong&gt; (not just the app), watch chip support:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;C5 / C61:&lt;/strong&gt; Full recovery bootloader with eFuse fallback. Safe. Use it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;C6, S2, S3:&lt;/strong&gt; No recovery bootloader. Only app OTA is safe. Bootloader must be flashed once over USB and left alone—OTA-ing the bootloader on these chips is risky (no fallback if the write fails).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;C6 practical rule:&lt;/strong&gt; App OTA + rollback = safe/supported. Bootloader OTA = still risky. If bullet-proof bootloader OTA is a hard requirement, prefer &lt;strong&gt;C5 or C61&lt;/strong&gt;, not C6.&lt;/p&gt;
&lt;h3&gt;C6 vs S2 vs S3 at a glance&lt;/h3&gt;
&lt;p&gt;For battery devices that need to &lt;strong&gt;stay Wi-Fi-connected&lt;/strong&gt; on battery, the C6 is a different animal from S2/S3.&lt;/p&gt;
&lt;h4&gt;ESP32-S2&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cores:&lt;/strong&gt; 1× Xtensa LX7 @ 240 MHz&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low-power:&lt;/strong&gt; ULP-RISC-V (tiny helper, limited fixed functions)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WiFi:&lt;/strong&gt; Wi-Fi 4 (802.11n)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BLE:&lt;/strong&gt; None&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connected power:&lt;/strong&gt; ~mA (frequent DTIM wakeups)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;ESP32-S3&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cores:&lt;/strong&gt; 2× Xtensa LX7 @ 240 MHz&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low-power:&lt;/strong&gt; ULP-RISC-V (same as S2)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WiFi:&lt;/strong&gt; Wi-Fi 4 (802.11n)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BLE:&lt;/strong&gt; BLE 5&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connected power:&lt;/strong&gt; ~mA (frequent DTIM wakeups)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;ESP32-C6&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cores:&lt;/strong&gt; 1× RISC-V HP @ 160 MHz&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Low-power:&lt;/strong&gt; &lt;strong&gt;Real LP core&lt;/strong&gt; (full RISC-V with LP UART/I2C/ADC/GPIO)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WiFi:&lt;/strong&gt; &lt;a href="https://www.espressif.com/en/news/ESP32-C6-802.11ax"&gt;&lt;strong&gt;Wi-Fi 6&lt;/strong&gt;&lt;/a&gt; (802.11ax)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BLE:&lt;/strong&gt; BLE 5&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;802.15.4:&lt;/strong&gt; Yes (Thread/Zigbee/Matter)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connected power:&lt;/strong&gt; &lt;strong&gt;~µA with &lt;a href="https://www.espressif.com/en/news/ESP32-C6-TWT"&gt;TWT&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Key differences:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/IEEE_802.11ax#Target_Wake_Time"&gt;&lt;strong&gt;TWT (Target Wake Time)&lt;/strong&gt;&lt;/a&gt; is a Wi-Fi 6 feature (C6-only). The chip negotiates sleep windows with the AP while staying associated — can cut average current ~10× vs S2/S3 which need frequent DTIM wakeups.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;LP core&lt;/strong&gt; means real sensor/comms loops can run on the low-power core while the HP core sleeps — not a "tiny helper" like S2/S3 ULP, but an actual programmable MCU with peripherals.&lt;/p&gt;
&lt;h3&gt;What I'm doing&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Existing fleet (13 projects on v5.5.4):&lt;/strong&gt; Staying put unless a specific reason to upgrade surfaces (bug fix, new feature, hardware issue). Low urgency.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;New work:&lt;/strong&gt; C6 on v6.0.2 from the start, taking advantage of TWT, LP core, and the full v6 tooling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Test upgrade (Q3 2026):&lt;/strong&gt; Pick one S3 project (attic-temperature-sensor or rain-sensor), do the staged trial, confirm OTA, then roll out to the rest if no surprises.&lt;/p&gt;
&lt;p&gt;The migration is not risky — it's just not urgent for devices already working on v5.5.4.&lt;/p&gt;</description><category>c</category><category>embedded</category><category>esp32</category><category>esp-idf</category><category>firmware</category><guid>https://blog.tedder.dev/posts/esp-idf-6-migration-v5-to-v6/</guid><pubDate>Sun, 12 Jul 2026 21:41:00 GMT</pubDate></item><item><title>Automating Nikola blog deployments with GitHub Actions</title><link>https://blog.tedder.dev/posts/automating-nikola-deployments-github-actions/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;Building a Nikola static site is fast. Pushing to S3 is fast. But running them manually every time you publish a post is tedious. GitHub Actions automates both in a few minutes.&lt;/p&gt;
&lt;h3&gt;The setup we built&lt;/h3&gt;
&lt;p&gt;A workflow that:
1. Listens for pushes to &lt;code&gt;main&lt;/code&gt;
2. Installs Nikola and dependencies
3. Builds the entire site
4. Pushes the output to S3&lt;/p&gt;
&lt;p&gt;Every post goes live the moment you push.&lt;/p&gt;
&lt;h3&gt;Create limited AWS credentials&lt;/h3&gt;
&lt;p&gt;First, create an &lt;a href="https://docs.aws.amazon.com/iam/latest/userguide/id_users.html"&gt;IAM user&lt;/a&gt; with S3-only access to your bucket. This follows the &lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html"&gt;principle of least privilege&lt;/a&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;aws&lt;span class="w"&gt; &lt;/span&gt;iam&lt;span class="w"&gt; &lt;/span&gt;create-user&lt;span class="w"&gt; &lt;/span&gt;--user-name&lt;span class="w"&gt; &lt;/span&gt;blog-deploy-user
aws&lt;span class="w"&gt; &lt;/span&gt;iam&lt;span class="w"&gt; &lt;/span&gt;put-user-policy&lt;span class="w"&gt; &lt;/span&gt;--user-name&lt;span class="w"&gt; &lt;/span&gt;blog-deploy-user&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--policy-name&lt;span class="w"&gt; &lt;/span&gt;s3-bucket-deploy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--policy-document&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{&lt;/span&gt;
&lt;span class="s1"&gt;  "Version": "2012-10-17",&lt;/span&gt;
&lt;span class="s1"&gt;  "Statement": [&lt;/span&gt;
&lt;span class="s1"&gt;    {&lt;/span&gt;
&lt;span class="s1"&gt;      "Effect": "Allow",&lt;/span&gt;
&lt;span class="s1"&gt;      "Action": [&lt;/span&gt;
&lt;span class="s1"&gt;        "s3:PutObject",&lt;/span&gt;
&lt;span class="s1"&gt;        "s3:DeleteObject",&lt;/span&gt;
&lt;span class="s1"&gt;        "s3:GetObject",&lt;/span&gt;
&lt;span class="s1"&gt;        "s3:ListBucket"&lt;/span&gt;
&lt;span class="s1"&gt;      ],&lt;/span&gt;
&lt;span class="s1"&gt;      "Resource": [&lt;/span&gt;
&lt;span class="s1"&gt;        "arn:aws:s3:::your-bucket",&lt;/span&gt;
&lt;span class="s1"&gt;        "arn:aws:s3:::your-bucket/*"&lt;/span&gt;
&lt;span class="s1"&gt;      ]&lt;/span&gt;
&lt;span class="s1"&gt;    }&lt;/span&gt;
&lt;span class="s1"&gt;  ]&lt;/span&gt;
&lt;span class="s1"&gt;}'&lt;/span&gt;
aws&lt;span class="w"&gt; &lt;/span&gt;iam&lt;span class="w"&gt; &lt;/span&gt;create-access-key&lt;span class="w"&gt; &lt;/span&gt;--user-name&lt;span class="w"&gt; &lt;/span&gt;blog-deploy-user
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Save those credentials. You'll need them in a moment.&lt;/p&gt;
&lt;h3&gt;Add secrets to GitHub&lt;/h3&gt;
&lt;p&gt;In your repository settings, go to &lt;strong&gt;Secrets and variables&lt;/strong&gt; → &lt;strong&gt;Actions&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Add the following secrets:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The workflow file&lt;/h3&gt;
&lt;p&gt;Create &lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build and Deploy Blog&lt;/span&gt;

&lt;span class="nt"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;push&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;branches&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;main&lt;/span&gt;

&lt;span class="nt"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;build_and_deploy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;runs-on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;ubuntu-latest&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/checkout@v4&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Set up Python&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;actions/setup-python@v5&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;python-version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;'3.11'&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Install Nikola&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;pip install 'nikola[extras]' watchdog aiohttp&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Build blog&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;nikola build&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Configure AWS credentials&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;uses&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;with&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;aws-region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;us-west-2&lt;/span&gt;

&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p p-Indicator"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;Deploy to S3&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;run&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;|&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="no"&gt;aws s3 sync output/ s3://your-bucket/ \&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;--delete \&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;--exclude "*.md" \&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="no"&gt;--exclude ".git/*"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;--delete&lt;/code&gt; flag removes files from S3 that aren't in your build output (useful when renaming posts). The &lt;code&gt;--exclude&lt;/code&gt; flags skip markdown source and git files.&lt;/p&gt;
&lt;h3&gt;A note on the official Nikola action&lt;/h3&gt;
&lt;p&gt;The &lt;a href="https://github.com/getnikola/nikola-action"&gt;&lt;code&gt;getnikola/nikola-action&lt;/code&gt;&lt;/a&gt; is designed for &lt;a href="https://docs.github.com/en/pages/getting-started-with-github-pages"&gt;GitHub Pages deployments&lt;/a&gt; (&lt;code&gt;github_deploy&lt;/code&gt;), not S3. If you're using S3 (or another deployment method), a custom workflow like this is simpler and more transparent.&lt;/p&gt;
&lt;p&gt;The action also had a subtle issue: running inside a Docker container, it couldn't find the repository due to git's &lt;a href="https://github.com/actions/runner/issues/2033"&gt;&lt;code&gt;safe.directory&lt;/code&gt;&lt;/a&gt; check (a &lt;a href="https://github.blog/2022-04-12-git-security-vulnerability-announced/"&gt;security feature added in Git 2.36&lt;/a&gt;). The workaround was to create &lt;code&gt;.gitconfig&lt;/code&gt; in the runner's home directory before the container ran, but at that point, using a direct Python install was cleaner. (&lt;a href="https://github.com/actions/runner/issues/2033"&gt;See the related runner issue for details.&lt;/a&gt;)&lt;/p&gt;
&lt;h3&gt;Next steps&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Customize the &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html"&gt;S3 sync&lt;/a&gt; command for your bucket and path&lt;/li&gt;
&lt;li&gt;Add &lt;a href="https://docs.aws.amazon.com/cloudfront/latest/developerguide/Invalidation.html"&gt;CloudFront cache invalidation&lt;/a&gt; if you use &lt;a href="https://aws.amazon.com/cloudfront/"&gt;CloudFront&lt;/a&gt; as a CDN&lt;/li&gt;
&lt;li&gt;Add &lt;a href="https://slack.com/"&gt;Slack&lt;/a&gt; or Discord notifications on deployment success or failure using &lt;a href="https://docs.github.com/en/actions"&gt;GitHub Actions workflows&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Consider &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control"&gt;&lt;code&gt;--cache-control&lt;/code&gt;&lt;/a&gt; headers to bust caches on HTML files (e.g., &lt;code&gt;aws s3 cp ... --cache-control "max-age=300"&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Deploy from the comfort of your editor. No manual builds, no SSH tunnels.&lt;/p&gt;</description><category>automation</category><category>aws-s3</category><category>ci-cd</category><category>github-actions</category><category>nikola</category><guid>https://blog.tedder.dev/posts/automating-nikola-deployments-github-actions/</guid><pubDate>Sun, 12 Jul 2026 21:40:00 GMT</pubDate></item><item><title>ESP-IDF code patterns: OTA, diagnostics, and power management</title><link>https://blog.tedder.dev/posts/esp-idf-code-patterns/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;You've set up your project structure and partition table. Now what? The next layer is the actual firmware code — the patterns that keep your device running reliably. I've learned these through deployed devices in the field: solar-powered sensors that need to run for months, OTA updates that can brick a device if they stall, and the peculiar debugging nightmare of a device you can't reach without logging in to a VPN jump host.&lt;/p&gt;
&lt;h3&gt;OTA update strategies: when to check and how to download&lt;/h3&gt;
&lt;p&gt;OTA updates are great until they're not. The first time a firmware update stalls halfway through (on a slow cellular connection, say) and the device hangs forever because it can't finish downloading, you'll understand why this section exists.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Start OTA checks with a delay after boot.&lt;/strong&gt; Don't check on startup. If the firmware you just flashed is bad, you'll immediately try to update back to the broken version, and your device is toast. Instead, use a delay of at least a few minutes:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define OTA_INITIAL_DELAY_MS (5 * 60 * 1000)  &lt;/span&gt;&lt;span class="c1"&gt;// 5 minutes&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;app_main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... initialize, WiFi, etc ...&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Don't check OTA immediately; let the device stabilize&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;vTaskDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OTA_INITIAL_DELAY_MS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;portTICK_PERIOD_MS&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;xTaskCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ota_task&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"ota_task"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This gives you a window to manually reflash if the new firmware is broken. On OTA success, the bootloader sets the new partition as active on &lt;em&gt;next boot&lt;/em&gt;, so if something is wrong you have a chance to stop it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Space out OTA checks to hours, not minutes.&lt;/strong&gt; Polling every 5 minutes wastes power and network bandwidth for no benefit. Once every 4-6 hours is reasonable for most devices. If you're on solar or battery, make it longer:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define OTA_CHECK_INTERVAL_MS (4 * 60 * 60 * 1000)  &lt;/span&gt;&lt;span class="c1"&gt;// 4 hours&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ota_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pvParameters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;TickType_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;xTaskGetTickCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;TickType_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;xTaskGetTickCount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_check&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pdMS_TO_TICKS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OTA_CHECK_INTERVAL_MS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;perform_ota_update&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;last_check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;vTaskDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdMS_TO_TICKS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;// Check the clock once per minute&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Download in chunks and call &lt;code&gt;esp_task_wdt_reset()&lt;/code&gt; per chunk.&lt;/strong&gt; This is the hard-won lesson. If you use the ESP-IDF's &lt;code&gt;esp_https_ota_perform()&lt;/code&gt; in blocking mode (which is easy), it will sit in one function call for the entire download. On a slow connection, that's minutes. The watchdog timer gets angry and reboots your device mid-update.&lt;/p&gt;
&lt;p&gt;Instead, use the chunked OTA API:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;"esp_https_ota.h"&lt;/span&gt;

&lt;span class="n"&gt;esp_err_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ota_with_watchdog_reset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;esp_https_ota_config_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;http_config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;esp_https_ota_handle_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;https_ota_handle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;esp_err_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;esp_https_ota_begin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;https_ota_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;!=&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;esp_https_ota_perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;https_ota_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ESP_ERR_HTTPS_OTA_IN_PROGRESS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="c1"&gt;// Reset watchdog on each chunk&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;esp_task_wdt_reset&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;esp_https_ota_is_complete_data_received&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;https_ota_handle&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;esp_https_ota_finish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;https_ota_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;esp_https_ota_abort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;https_ota_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Yes, you'll need to tune the watchdog timeout itself, but resetting per chunk is the key. Without it, slow downloads fail mysteriously.&lt;/p&gt;
&lt;h3&gt;Defensive programming: tracking reset reasons and device state&lt;/h3&gt;
&lt;p&gt;If a device reboots out in the field, you need to know why. Logging doesn't help if you can't reach the serial console. Build diagnostics into the firmware itself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Capture the reset reason on every boot:&lt;/strong&gt;&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;"esp_system.h"&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cpf"&gt;"nvs_flash.h"&lt;/span&gt;

&lt;span class="k"&gt;typedef&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reset_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_reset_reason&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_ota_attempt_ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_data_post_ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;wifi_fail_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;device_state_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;track_reset_reason&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;esp_reset_reason_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;esp_reset_reason&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;device_state_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_handle_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NVS_READWRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_get_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;device_state_t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset_count&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_reset_reason&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&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;reason&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_set_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;device_state_t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Boot #%lu, last reset reason: %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset_count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_reset_reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Implement a safe-mode escape hatch.&lt;/strong&gt; If your device has rebooted 10 times in the last 5 minutes, something is very wrong. Entering a safe mode that doesn't run the main logic prevents a reboot loop from destroying data or spinning through CPU cycles:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define UNSAFE_REBOOT_THRESHOLD 10&lt;/span&gt;
&lt;span class="cp"&gt;#define REBOOT_WINDOW_MS (5 * 60 * 1000)&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;check_for_reboot_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device_state_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;time_since_reset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// If we've rebooted many times in a short window, enter safe mode&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;reset_count&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UNSAFE_REBOOT_THRESHOLD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;time_since_reset&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;REBOOT_WINDOW_MS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;ESP_LOGW&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="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Too many reboots! Entering safe mode."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Don't run main logic; just sit in a loop and wait for OTA&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &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="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;vTaskDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdMS_TO_TICKS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Expose device state via HTTP.&lt;/strong&gt; Add a simple debug endpoint that returns JSON with diagnostic info. You can query it without serial access:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;esp_err_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;debug_handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpd_req_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;device_state_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_handle_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NVS_READONLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_get_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;device_state_t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;cJSON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cJSON_CreateObject&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;cJSON_AddNumberToObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"reset_count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reset_count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;cJSON_AddNumberToObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"last_reset_reason"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_reset_reason&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;cJSON_AddNumberToObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"wifi_fail_count"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wifi_fail_count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;cJSON_AddNumberToObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"uptime_ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;json_str&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cJSON_Print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;httpd_resp_set_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;httpd_resp_sendstr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;json_str&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;cJSON_Delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;root&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;free&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_str&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&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;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then register it during HTTP server setup. Now you can SSH into a jump host, curl your device, and get a full picture without touching a serial cable.&lt;/p&gt;
&lt;h3&gt;NVS (NVRAM) diagnostics: what to track and why&lt;/h3&gt;
&lt;p&gt;Not every value is worth persisting, but the right ones save you hours of debugging. Here's what I track:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reset count&lt;/strong&gt;: how many times has this device rebooted?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Last OTA attempt timestamp&lt;/strong&gt;: did it try to update recently? (Stuck devices often have an old timestamp here.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Last successful data-post timestamp&lt;/strong&gt;: when was the last time this device actually transmitted data?&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WiFi connect failure count&lt;/strong&gt;: is WiFi flaky?&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;typedef&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;reset_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_reset_reason&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_ota_attempt_ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="c1"&gt;// Milliseconds since epoch&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_data_post_ms&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// Milliseconds since epoch&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;wifi_fail_count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;device_state_t&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;update_ota_timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;device_state_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_handle_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NVS_READWRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_get_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;device_state_t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_ota_attempt_ms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_set_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;device_state_t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;update_data_post_timestamp&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;device_state_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_handle_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"device"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;NVS_READWRITE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_get_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;device_state_t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;last_data_post_ms&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_set_blob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"state"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&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;device_state_t&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;nvs_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nvs_handle&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now your &lt;code&gt;/debug&lt;/code&gt; endpoint tells you:
- "This device last sent data 3 hours ago" → likely a WiFi issue
- "Reset reason is POWERON, reset count is 50, OTA timestamp is from this morning" → firmware is stable but something else keeps power-cycling the board
- "WiFi fail count is 200 but uptime is only 2 hours" → authentication problem&lt;/p&gt;
&lt;h3&gt;Power savings for solar+battery devices&lt;/h3&gt;
&lt;p&gt;If your ESP32 is sitting outside powered by a solar panel and a battery, every milliamp-hour matters. Deep sleep is your friend, but there are sneaky current draws you need to minimize.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use deep sleep liberally.&lt;/strong&gt; If your device only needs to transmit once per hour, sleep for 59 minutes and wake for 1 minute:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define SLEEP_TIME_US (59 * 60 * 1000000)  &lt;/span&gt;&lt;span class="c1"&gt;// 59 minutes&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;app_main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... initialize, WiFi, transmit data, update OTA ...&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Deep sleep consumes ~10 µA (vs ~80 mA active)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;esp_deep_sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SLEEP_TIME_US&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Minimize WiFi active time.&lt;/strong&gt; WiFi startup takes a second or two and draws ~200 mA. Shave every millisecond:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use a &lt;strong&gt;static IP&lt;/strong&gt; instead of DHCP. DHCP adds 100-500 ms to startup, depending on your router.&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;WPA2_PSK, not mixed WPA2/WPA3.&lt;/strong&gt; WPA2 negotiation is faster.&lt;/li&gt;
&lt;li&gt;Connect to the &lt;strong&gt;closest/strongest AP&lt;/strong&gt; to minimize retries.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;wifi_config_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;wifi_config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;CONFIG_WIFI_SSID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;CONFIG_WIFI_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authmode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WIFI_AUTH_WPA2_PSK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pmf_cfg&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Use static IP&lt;/span&gt;
&lt;span class="n"&gt;tcpip_adapter_ip_info_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ip_info&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipaddr_addr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"192.168.0.100"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netmask&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipaddr_addr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"255.255.255.0"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;addr&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ipaddr_addr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"192.168.0.1"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="n"&gt;tcpip_adapter_set_ip_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TCPIP_ADAPTER_STA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ip_info&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;(Substitute your actual IP range, of course.)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Consider ESP-NOW for ultra-low-power sensors.&lt;/strong&gt; If you have a receiver (another ESP32, always-on gateway, etc.) nearby, ESP-NOW uses 150-200 ms of active time versus 2+ seconds for WiFi. The tradeoff is that you need infrastructure:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;send_via_espnow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;uint8_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ESP-NOW transmit: ~100-150 ms active time, much less power&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Requires a paired receiver&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;esp_now_send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;broadcast_mac&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// Deep sleep immediately after&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;esp_deep_sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SLEEP_TIME_US&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Budget your power envelope.&lt;/strong&gt; If you have a 500 mAh battery and 500 mW solar charging on a cloudy day, you can sustain about 5 mAh per hour (rough math). If each transmit-and-OTA-check cycle takes 500 mA for 10 seconds, that's 1.4 mAh. Do that once per hour and you're breaking even or losing charge. Do it twice per hour on a cloudy day and your battery drains. Pick your sleep interval based on what your solar panel can actually charge:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#define SLEEP_INTERVAL_MS (2 * 60 * 60 * 1000)  &lt;/span&gt;&lt;span class="c1"&gt;// 2 hours for marginal solar&lt;/span&gt;
&lt;span class="c1"&gt;// vs.&lt;/span&gt;
&lt;span class="cp"&gt;#define SLEEP_INTERVAL_MS (30 * 60 * 1000)      &lt;/span&gt;&lt;span class="c1"&gt;// 30 min for good sun/large battery&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Test in the actual sun conditions you'll deploy in. A device that runs fine in your garage with good WiFi will fail miserably in the field.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;These patterns have saved me from devices that reboot in loops, OTA updates that hang forever, and batteries that drain in a week instead of a month. They're not ESP-IDF-specific — most of these apply to any embedded system — but they're the things I reach for in every firmware project now. The defensive diagnostics pay for themselves the first time you need to debug a device you can't physically reach.&lt;/p&gt;</description><category>embedded</category><category>esp32</category><category>esp-idf</category><category>firmware</category><guid>https://blog.tedder.dev/posts/esp-idf-code-patterns/</guid><pubDate>Tue, 07 Jul 2026 15:37:00 GMT</pubDate></item><item><title>ESP-IDF project standards and conventions</title><link>https://blog.tedder.dev/posts/esp-idf-project-standards/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;I've built a bunch of ESP32 projects using Espressif's ESP-IDF framework, and every time I start a new one, I second-guess myself on basic structure: where does the config go? How do I version the firmware? What partition layout should I use? This post is documentation-to-self, but hopefully it saves someone else from the iterative debugging I've done.&lt;/p&gt;
&lt;h3&gt;Project structure and build configuration&lt;/h3&gt;
&lt;p&gt;The ESP-IDF follows a pretty strict layout. At the top level you'll have:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;main/&lt;/code&gt;: your application code (&lt;code&gt;main.c&lt;/code&gt; is the entry point)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;CMakeLists.txt&lt;/code&gt;: project-level build configuration&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sdkconfig.defaults&lt;/code&gt;: sensible defaults for menuconfig options&lt;/li&gt;
&lt;li&gt;&lt;code&gt;partitions.csv&lt;/code&gt;: partition table (more on that below)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.clangd&lt;/code&gt;: language server config to fix false positives on macOS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;main/CMakeLists.txt&lt;/code&gt; is minimal:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;idf_component_register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SRCS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"main.c"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="n"&gt;INCLUDE_DIRS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Keep components in &lt;code&gt;main/&lt;/code&gt; until you have more than a few files, then break them into &lt;code&gt;components/mycomponent/&lt;/code&gt;.&lt;/p&gt;
&lt;h3&gt;OTA update patterns: ota_0 and ota_1&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Never use the factory+ota_0 partition layout.&lt;/strong&gt; I learned this the hard way when I bricked a device. Use &lt;strong&gt;ota_0 and ota_1 instead&lt;/strong&gt;, so you can always fall back to the previous firmware if an update goes wrong.&lt;/p&gt;
&lt;p&gt;Your &lt;code&gt;partitions.csv&lt;/code&gt; should look like:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;nvs,      data, nvs,     0x9000, 0x6000,
otadata,  data, ota,     0xf000, 0x2000,
ota_0,    app,  ota_0,   0x20000, 0x1C0000,
ota_1,    app,  ota_1,   0x1E0000, 0x1C0000,
spiffs,   data, spiffs,  0x3A0000, 0x60000,
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The sizes here depend on your flash size (I'm assuming 4MB). The key is that &lt;code&gt;otadata&lt;/code&gt; tracks which OTA partition is active, and the bootloader automatically switches between them. On a bad update, it rolls back to the previous partition on next boot.&lt;/p&gt;
&lt;p&gt;I allocate 1.75 MB (0x1C0000) per OTA partition because my firmware is usually 600-800 KB, and that leaves plenty of headroom for growth without requiring a reflash to resize.&lt;/p&gt;
&lt;h3&gt;Firmware versioning&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Every binary that gets flashed or pushed to S3 needs a version bump.&lt;/strong&gt; Use a simple semver string:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;// main/main.c&lt;/span&gt;
&lt;span class="cp"&gt;#define FIRMWARE_VERSION "1.2.3"&lt;/span&gt;
&lt;span class="cp"&gt;#define FIRMWARE_VERSION_MAJOR 1&lt;/span&gt;
&lt;span class="cp"&gt;#define FIRMWARE_VERSION_MINOR 2&lt;/span&gt;
&lt;span class="cp"&gt;#define FIRMWARE_VERSION_PATCH 3&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then in your OTA or setup code, log it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&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="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;"Firmware version: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FIRMWARE_VERSION&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This sounds obvious, but I've spent frustrating hours trying to reproduce a bug and realizing the device is running an older build than I thought. A simple version string at startup saves that pain.&lt;/p&gt;
&lt;p&gt;If you're pushing to S3 for OTA (see below), include the version in the build artifact name: &lt;code&gt;myproject-v1.2.3.bin&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;FIRMWARE_VERSION&lt;/code&gt; and &lt;code&gt;esp_app_desc_t.version&lt;/code&gt; are not the same thing — you must keep both in sync.&lt;/strong&gt; The &lt;code&gt;#define&lt;/code&gt; you log at startup is just a C string. The version embedded in the binary's &lt;code&gt;esp_app_desc_t&lt;/code&gt; struct — which is what &lt;code&gt;esp_app_get_description()-&amp;gt;version&lt;/code&gt; returns, and what OTA same-version skip logic compares — comes from CMake's &lt;code&gt;PROJECT_VER&lt;/code&gt; at build time, not your &lt;code&gt;#define&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you use same-version skip in your OTA code (and you should), this distinction will bite you: if &lt;code&gt;PROJECT_VER&lt;/code&gt; is not set, ESP-IDF embeds a fallback string like &lt;code&gt;"1"&lt;/code&gt; or &lt;code&gt;"1.2"&lt;/code&gt; in every single build. Your OTA check compares &lt;code&gt;"1.2"&lt;/code&gt; on the device against &lt;code&gt;"1.2"&lt;/code&gt; in the downloaded image, sees a match, and skips — every time, forever, no matter what you change.&lt;/p&gt;
&lt;p&gt;The fix: enable &lt;code&gt;CONFIG_APP_PROJECT_VER_FROM_CONFIG&lt;/code&gt; and set &lt;code&gt;CONFIG_APP_PROJECT_VER&lt;/code&gt; to match your &lt;code&gt;FIRMWARE_VERSION&lt;/code&gt;. &lt;strong&gt;You must update this in both &lt;code&gt;sdkconfig.defaults&lt;/code&gt; AND &lt;code&gt;sdkconfig&lt;/code&gt; directly&lt;/strong&gt; — defaults only apply to keys not already present in &lt;code&gt;sdkconfig&lt;/code&gt;, so if the key exists (even disabled), &lt;code&gt;idf.py reconfigure&lt;/code&gt; will not override it.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="gh"&gt;#&lt;/span&gt; sdkconfig.defaults
CONFIG_APP_PROJECT_VER_FROM_CONFIG=y
CONFIG_APP_PROJECT_VER="1.2.3"
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And in &lt;code&gt;sdkconfig&lt;/code&gt;, find the existing &lt;code&gt;# CONFIG_APP_PROJECT_VER_FROM_CONFIG is not set&lt;/code&gt; line and replace it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;CONFIG_APP_PROJECT_VER_FROM_CONFIG=y
CONFIG_APP_PROJECT_VER="1.2.3"
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Every firmware bump requires updating three places: &lt;code&gt;FIRMWARE_VERSION&lt;/code&gt; in &lt;code&gt;main.c&lt;/code&gt;, &lt;code&gt;CONFIG_APP_PROJECT_VER&lt;/code&gt; in &lt;code&gt;sdkconfig.defaults&lt;/code&gt;, and &lt;code&gt;CONFIG_APP_PROJECT_VER&lt;/code&gt; in &lt;code&gt;sdkconfig&lt;/code&gt;. Missing any one of them means either your logs lie, or your OTA never runs.&lt;/p&gt;
&lt;h3&gt;WiFi authentication: stick with WPA2_PSK&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Always use &lt;code&gt;WIFI_AUTH_WPA2_PSK&lt;/code&gt;, not &lt;code&gt;WIFI_AUTH_WPA2_WPA3_PSK&lt;/code&gt;.&lt;/strong&gt; The mixed mode causes mysterious auth failures on home routers, especially older ones or devices that don't advertise both protocols equally.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;wifi_config_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;wifi_config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sta&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ssid&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;CONFIG_WIFI_SSID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&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="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;CONFIG_WIFI_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;authmode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;WIFI_AUTH_WPA2_PSK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Even if your home network supports WPA3, the mixed mode is a footgun. If you're deploying to multiple locations or devices with varying WiFi hardware, stick with WPA2_PSK. It's universally supported and won't surprise you.&lt;/p&gt;
&lt;h3&gt;Partition sizing and resizing pain&lt;/h3&gt;
&lt;p&gt;Leave expansion room in your OTA partitions. I allocate 1.75 MB when my current firmware is 600 KB because resizing partitions later requires a USB cable and &lt;code&gt;esptool.py flash_id&lt;/code&gt; followed by a full reflash. It's doable but annoying.&lt;/p&gt;
&lt;p&gt;If your project grows and you need more space, you have two options:
1. Resize partitions (USB reflash required)
2. Move to a larger flash chip (harder)&lt;/p&gt;
&lt;p&gt;Allocate generously at the start. A few MB of unused space costs nothing.&lt;/p&gt;
&lt;h3&gt;Clangd/LSP false positives on macOS&lt;/h3&gt;
&lt;p&gt;If you're using clangd or VSCode's C/C++ extension on macOS, you'll see tons of red squiggles for ESP-IDF types and macros that actually compile fine. This is because clangd doesn't know about the build configuration.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;.clangd&lt;/code&gt; file at your project root:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nt"&gt;CompileFlags&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;CompilationDatabase&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;build/compile_commands.json&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Build once (so &lt;code&gt;compile_commands.json&lt;/code&gt; exists), then clangd will use it and the false positives disappear. This is especially important for understanding macro expansions and avoiding frustration while editing.&lt;/p&gt;
&lt;h3&gt;S3 OTA uploads: the cache-control trap&lt;/h3&gt;
&lt;p&gt;If you're pushing built binaries to S3 for OTA updates, &lt;strong&gt;always include &lt;code&gt;--cache-control "max-age=30"&lt;/code&gt;&lt;/strong&gt; when uploading:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="nv"&gt;AWS_PROFILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;yourprofile&lt;span class="w"&gt; &lt;/span&gt;aws&lt;span class="w"&gt; &lt;/span&gt;s3&lt;span class="w"&gt; &lt;/span&gt;cp&lt;span class="w"&gt; &lt;/span&gt;build/myproject.bin&lt;span class="w"&gt; &lt;/span&gt;s3://your-bucket/firmware/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--cache-control&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"max-age=30"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Without this, CloudFront (or any other CDN in front of S3) will serve stale versions for up to 1 hour, and your devices will pull the old firmware. I've spent hours debugging "why is the new firmware not deployed?" only to realize the CDN was serving the previous build.&lt;/p&gt;
&lt;p&gt;Set &lt;code&gt;max-age&lt;/code&gt; to something short (30 seconds works) so you get fresh binaries on each request, not "fresh every hour."&lt;/p&gt;
&lt;h3&gt;Battery-powered sensors: ESP-NOW vs WiFi&lt;/h3&gt;
&lt;p&gt;If you're building a battery-powered sensor, &lt;strong&gt;ESP-NOW uses far less power than WiFi&lt;/strong&gt;, but requires a receiver that's always on (or close to it). Tradeoffs:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WiFi:&lt;/strong&gt;
- Higher latency and power draw (radio startup is expensive)
- Works everywhere without custom infrastructure
- Built-in over-the-air update support&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ESP-NOW:&lt;/strong&gt;
- One-way or paired communication only; you need a gateway/receiver
- Extremely low power if you can wake, transmit, and sleep in milliseconds
- Mesh capability with enough peers&lt;/p&gt;
&lt;p&gt;For a humidity sensor that transmits once per hour, ESP-NOW + a gateway is overkill. For a door sensor that needs to wake and transmit immediately, ESP-NOW is worth the complexity. For anything in between, build WiFi first and optimize to ESP-NOW later if battery life becomes critical.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;That's the foundation. Every new project I start, I copy the partition table, firmware version pattern, and build wrapper from the last one, and I've saved myself countless hours of "wait, why is this partition 512K?" debugging. Hopefully this saves you from the same mistakes.&lt;/p&gt;</description><category>embedded</category><category>esp32</category><category>esp-idf</category><category>firmware</category><guid>https://blog.tedder.dev/posts/esp-idf-project-standards/</guid><pubDate>Sun, 07 Jun 2026 17:23:00 GMT</pubDate></item><item><title>Fixing Elasticsearch/Logstash/ELK's DATESTAMP grok pattern</title><link>https://blog.tedder.dev/posts/fixing-elasticsearchlogstashelks-datestamp-grok-pattern/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;Elasticsearch, including &lt;a href="https://www.elastic.co/guide/en/logstash/7.7/index.html"&gt;Logstash&lt;/a&gt; and &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/pipeline.html"&gt;Pipeline Processors&lt;/a&gt;, love to use grok patterns. These are basically named regex patterns, allowing the complexity to be hidden behind easier-to-read labels (though they &lt;em&gt;do&lt;/em&gt; require referring to the source). &lt;a href="https://xkcd.com/208/"&gt;Regexes are great&lt;/a&gt;, with the "&lt;a href="https://blog.codinghorror.com/regular-expressions-now-you-have-two-problems/"&gt;now you have two problems&lt;/a&gt;" caveat of &lt;a href="https://en.wikipedia.org/wiki/Clarke%27s_three_laws"&gt;any sufficiently advanced technology&lt;/a&gt;. (it could be worse, &lt;a href="https://xkcd.com/1171/"&gt;you could have 100 problems&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;What's the problem? &lt;strong&gt;Timestamp support&lt;/strong&gt;. Such a trivial issue has been a problem for a long time. I'll show a fix (rather, a workaround) below.&lt;/p&gt;
&lt;h2&gt;The problem: year-first timestamps&lt;/h2&gt;
&lt;p&gt;Many datestamps in logs are in a year-first format (e.g., 2020-01-01). That makes sense, as many operating systems and languages default to &lt;a href="https://en.wikipedia.org/wiki/ISO_8601"&gt;ISO 8601&lt;/a&gt; for a human-readable datetime format. For instance, here's a recent example from my system's &lt;code&gt;dpkg.log&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;2020&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;05&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;21&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;06&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;01&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;01&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;upgrade&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;tzdata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;all&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2019&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;3&lt;/span&gt;&lt;span class="n"&gt;ubuntu1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;2020&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0&lt;/span&gt;&lt;span class="n"&gt;ubuntu0&lt;/span&gt;&lt;span class="mf"&gt;.20.04&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or from a Mac's log:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;2020&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;05&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;24&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;04&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;25&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;07&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ted&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;macbook&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;pro&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;softwareupdated&lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;753&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;Removing client SUUpdateServiceClient pid=5347, uid=0, installAuth=NO rights=(), transactions=0 (/usr/sbin/softwareupdate)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Or from &lt;a href="https://octoprint.org/"&gt;Octoprint&lt;/a&gt;'s python-based logs:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;2020&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;05&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;25&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;03&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;26&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;643&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;octoprint&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;server&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="n"&gt;heartbeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;INFO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;heartbeat&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mf"&gt;3&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There are other common variants, like the 'T' delimiting the date and time sections, time zones, or other delimiters- I will also be addressing this format, found in the grok comment:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="mf"&gt;2020&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;05&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mf"&gt;25&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;16&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;02&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;11.5533&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I'm less concerned about time zones, since real computers run on UTC.&lt;/p&gt;
&lt;p&gt;What's wrong with these? &lt;strong&gt;none of them are supported in vanilla grok&lt;/strong&gt; in any version.&lt;/p&gt;
&lt;h3&gt;Digging into the problem&lt;/h3&gt;
&lt;p&gt;Let's look at &lt;a href="https://github.com/logstash-plugins/logstash-patterns-core/blob/767052713f5e6114bfc1aa9cdd79bc2976b71bd4/patterns/grok-patterns#L65"&gt;logstash in 2015&lt;/a&gt;. Or &lt;a href="https://github.com/logstash-plugins/logstash-patterns-core/blob/bcee470e87dd6a3c4ee5f6f4aa68e9619ae74a75/patterns/grok-patterns#L66"&gt;logstash in 2020&lt;/a&gt;. Or &lt;a href="https://github.com/elastic/elasticsearch/blob/eb097700cf2d2daca23e3493beb8e11f3464b5c8/libs/grok/src/main/resources/patterns/grok-patterns#L67"&gt;elasticsearch in 2020&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;They define UK-format and US-format dates:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;DATE_US %{MONTHNUM}[/-]%{MONTHDAY}[/-]%{YEAR}
DATE_EU %{MONTHDAY}[./-]%{MONTHNUM}[./-]%{YEAR}
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And in a comment, they suggest that the datestamp will be accepted with slashes:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;# datestamp is YYYY/MM/DD-HH:MM:SS.UUUU (or something like it)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Which leads to an implication that the &lt;code&gt;DATESTAMP&lt;/code&gt; would support it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;DATE %{DATE_US}|%{DATE_EU}
DATESTAMP %{DATE}[- ]%{TIME}
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But.. look back to the US/EU formats. No year-first format. Sometimes you'll see a weird match, like "20 April 2001", but it's just seeing "&lt;code&gt;20&lt;/code&gt;&lt;strong&gt;&lt;code&gt;20/04/01&lt;/code&gt;&lt;/strong&gt;", slicing off the first few digits, and parsing it as a date-first string. Weird, huh? This explains some of the weird indexes you might find in an ELK stack, where there's something like &lt;code&gt;logstash-2001.04.01&lt;/code&gt; and it's almost 20 years later.&lt;/p&gt;
&lt;p&gt;Anyhow, on to...&lt;/p&gt;
&lt;h2&gt;The easy fix&lt;/h2&gt;
&lt;p&gt;If you have the luxury of redefining &lt;code&gt;DATE&lt;/code&gt;, you can prepend a sane format to it:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;DATE_YEARFIRST %{YEAR}[\/\-\s]%{MONTHNUM}[\/\-\s]%{MONTHDAY}
DATE %{DATE_YEARFIRST}|%{DATE_US}|%{DATE_EU}
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Why prepend? That keeps it from accidentally matching &lt;code&gt;20&lt;/code&gt;&lt;strong&gt;&lt;code&gt;20&lt;/code&gt;&lt;/strong&gt; as above.&lt;/p&gt;
&lt;h2&gt;The hard fix&lt;/h2&gt;
&lt;p&gt;But if you don't have the luxury of updating your grok patterns, you'll have to whip up a custom one:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;(?&amp;lt;my_datetime_field&amp;gt;%{YEAR}/%{MONTHNUM}/%{MONTHDAY}[T\s\-]+%{TIME})
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You'll notice I've simplified the delimiters from above. It's just less readable o accept them all, but you might need to do so:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;(?&amp;lt;my_datetime_field&amp;gt;%{YEAR}[\/\-\s]%{MONTHNUM}[\/\-\s]%{MONTHDAY}[T\s\-]+%{TIME})
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So, there you go. That can easily be stuffed into Logstash, or a processor. Hooray!&lt;/p&gt;
&lt;h2&gt;Caveats&lt;/h2&gt;
&lt;p&gt;Now, there &lt;em&gt;is&lt;/em&gt; an 8601-style year-first pattern defined, so, great if it matches your format. It didn't match enough of my variants:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;TIMESTAMP_ISO8601 %{YEAR}-%{MONTHNUM}-%{MONTHDAY}[T ]%{ISO8601_HOUR}:?%{MINUTE}(?::?%{SECOND})?%{ISO8601_TIMEZONE}?
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There was also a year-first version &lt;a href="https://github.com/logstash-plugins/logstash-patterns-core/commit/ab106e9671c19ea858af38d5a387e443c040330f#diff-5d5996f507c4a9e3bcc205235e2a3ecc"&gt;added to logstash in 2016&lt;/a&gt;, then &lt;a href="https://github.com/logstash-plugins/logstash-patterns-core/commit/76ccaa0d0049db27ab2e5e043ba8b9b3adc9b6d4#diff-5d5996f507c4a9e3bcc205235e2a3ecc"&gt;the day and month were flipped&lt;/a&gt;, then it was removed or never made it to master. Hilariously, it was typoed as 8061 the whole time. It also didn't exist in elasticsearch, only logstash. It doesn't help that the Elasticsearch version of the file was &lt;a href="https://github.com/elastic/elasticsearch/commit/f611f1c99e5b402ef0c72afb36cc1998851fc5a8#diff-174cb919abddddc14dd758c607269e70"&gt;moved in 2016&lt;/a&gt;, then &lt;a href="https://github.com/elastic/elasticsearch/commit/3fad16e76cc18c581493e52a0d08e7553ac17f51#diff-bd5276a34e881a73699ad5e732b0bd6b"&gt;also moved in 2018&lt;/a&gt;, which took away the easy-to-view commit history.&lt;/p&gt;
&lt;h2&gt;Why not fix it?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/logstash-plugins/logstash-patterns-core/issues/112"&gt;Here's an issue from 2015&lt;/a&gt;. Here's &lt;a href="https://github.com/logstash-plugins/logstash-patterns-core/issues/67"&gt;another one&lt;/a&gt;. Here's a &lt;a href="https://github.com/logstash-plugins/logstash-patterns-core/pull/124"&gt;PR from 2016&lt;/a&gt;. The issue isn't submitting a PR, obviously. Don't get me started on &lt;code&gt;discuss.elastic.co&lt;/code&gt;, which auto-closes and locks discussions, meaning you're pretty much guaranteed to find an out-of-date, inferior, solution. If any at all.&lt;/p&gt;</description><guid>https://blog.tedder.dev/posts/fixing-elasticsearchlogstashelks-datestamp-grok-pattern/</guid><pubDate>Fri, 15 May 2020 18:22:41 GMT</pubDate></item><item><title>Gavin Belson Signature Edition liquid cooled server</title><link>https://blog.tedder.dev/posts/gavin-belson-edition-liquid-cooled-server/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;&lt;a href="https://photos.smugmug.com/photos/i-BxgHxL2/0/XL/i-BxgHxL2-XL.jpg"&gt;&lt;img alt="the gavin belson liquid cooled server, top open" src="https://photos.smugmug.com/photos/i-BxgHxL2/0/L/i-BxgHxL2-L.jpg" title="the gavin belson liquid cooled server"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I've been slowly collecting bits to build a liquid-cooled server. I have no practical reason to do so, but I've been intrigued by the concept. Finally I bit the bullet and got the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.newegg.com/amd-ryzen-9-3900x/p/N82E16819113103?Item=N82E16819113103"&gt;AMD Ryzen 3900X (12-core, 24-thread)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.newegg.com/gigabyte-x570-aorus-elite/p/N82E16813145160?Item=N82E16813145160"&gt;Gigabyte X570 Aorus Elite motherboard&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.newegg.com/black-rosewill-rsv-l4000c/p/N82E16811147277?Item=N82E16811147277"&gt;Rosewill "bitcoin mining" 4U case&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://amzn.to/2JGCcOR"&gt;Thermaltake 360mm ('thicc') radiator&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I then ordered a bunch of parts from EKWB:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.powercolor.com/product?id=1573110646"&gt;PowerColor Liquid Devil RX5700 XT&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ekwb.com/shop/ek-quantum-momentum-aorus-x570-elite-d-rgb-plexi"&gt;CPU/motherboard full block&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.ekwb.com/shop/ek-quantum-momentum-chipset-aorus-x570"&gt;X570 chipset block&lt;/a&gt; (the stock Aorus fan/block isn't compatible with the GPU)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything else was pretty normal to build a PC/pseudoserver. It took me quite a while to put this together- not only did I have to wait for two batches of parts from EKWB in Slovenia, plus deal with a customs delay, but I also needed to fabricate a bunch of parts.&lt;/p&gt;
&lt;p&gt;The first thing was the radiator. I designed two "sliders" to hold it in, since the width of the radiator is close to the case width. It took a lot of iterations to deal with bolt holes, drain cap of the radiator, and so on.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://photos.smugmug.com/photos/i-D379ZHh/0/XL/i-D379ZHh-XL.png"&gt;&lt;img alt="radiator CAD" src="https://photos.smugmug.com/photos/i-D379ZHh/0/S/i-D379ZHh-S.png" title="radiator CAD"&gt;&lt;/a&gt;
&lt;a href="https://photos.smugmug.com/photos/i-Qz5D3GM/0/XL/i-Qz5D3GM-XL.jpg"&gt;&lt;img alt="radiator mount" src="https://photos.smugmug.com/photos/i-Qz5D3GM/0/S/i-Qz5D3GM-S.jpg" title="radiator and 4U mount"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I then designed a block to hold the pump. It only took a couple of tries on it. I was happy to put a bit of a "lean" on the side bolt holes, which leaves room for a rounded corner on the inside of the case.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://photos.smugmug.com/photos/i-5JwMGRC/0/S/i-5JwMGRC-S.png"&gt;&lt;img alt="pump mount CAD" src="https://photos.smugmug.com/photos/i-5JwMGRC/0/S/i-5JwMGRC-S.png" title="pump mount CAD"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I installed the motherboard and &lt;em&gt;then&lt;/em&gt; tried adding the GPU and the water cooling. They both required disassembling the motherboard from its mounting plate. So many screws! The GPU was even more work when I realized the X570 chipset heatsink/fan were in the way. I first took it off and thought about cutting it, but it's a big chonky piece that would have been really difficult.&lt;/p&gt;
&lt;p&gt;The next GPU problem is the Rosewill case doesn't have expansion slots in the back. So weird! Obviously it's designed for a motherboard to be placed there, but I guess they assume nobody will need to add a card to the motherboard. So I Dremeled out the back of the case. I might design and print something to secure the card in place.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://photos.smugmug.com/photos/i-vH4wH4f/0/XL/i-vH4wH4f-XL.jpg"&gt;&lt;img alt="expansion slot cutout" src="https://photos.smugmug.com/photos/i-vH4wH4f/0/S/i-vH4wH4f-L.jpg" title="expansion slot cutout"&gt;&lt;/a&gt;
&lt;a href="https://photos.smugmug.com/photos/i-mbjPqmt/0/XL/i-mbjPqmt-XL.jpg"&gt;&lt;img alt="server window" src="https://photos.smugmug.com/photos/i-mbjPqmt/0/S/i-mbjPqmt-L.jpg" title="looking through the server window"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;To add to my list of GPU struggles, the liquid cooling makes the GPU taller than a 4U case. I took some measurements, carefully dremeled a hole, then designed a plexiglass &lt;a href="https://commons.wikimedia.org/wiki/File:Land_Rover_Santana_109,_III_Series,_SW,_1983,_used_by_Pope_John_Paul_II_-_Pontifical_Palace_-_Castel_Gandolfo_-_DSC04250.jpg"&gt;popemobile&lt;/a&gt; window. I might add a second window or enlarge that one to show the cooling and RGBs, but that is optional and would come later.&lt;/p&gt;
&lt;p&gt;I'm unhappy with a couple of things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;the motherboard only seems to have two fan headers that are usable from Linux. One has the pump, the other has puller fans. I have a few other rows of fans, so I either need to daisy them all together or control them from off the motherboard.&lt;/li&gt;
&lt;li&gt;the RGB LED headers aren't controllable.&lt;/li&gt;
&lt;li&gt;there are no ports for off-motherboard thermistors. I put four inline thermistors on the cooling loop but I have nowhere to plug them in.&lt;/li&gt;
&lt;li&gt;It's hard to know if the pump is running. It sends RPM back, but there's no other real verification. I haven't solved this yet, but I will probably stick a &lt;a href="https://amzn.to/39Du8Jm"&gt;flow indicator&lt;/a&gt; in the loop, then 3D print an optical sensor to look through it.&lt;/li&gt;
&lt;li&gt;The cabling mess. I tried heat-shrinking the fan cables, but that made things worse, partly because it's thick heatshrink that glued everything together, so I can't remove a fan from the shrink. There's a lot of cabling coming from the PSU too. I'm delaying solving this until everything is done.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I have a lead on solving the first three things- I've designed a board around the ESP32 with three fan headers and two DRGB headers. If that works I'll add at least five inputs for the thermistors and flow control. That way I can use the DRGBs to display temperature at a glance, plus control the fans and gather/transmit all the important data back to my network.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://photos.smugmug.com/photos/i-mvRJJJg/0/XL/i-mvRJJJg-XL.jpg"&gt;&lt;img alt="cooling loop" src="https://photos.smugmug.com/photos/i-mvRJJJg/0/S/i-mvRJJJg-L.jpg" title="main shot of server with cooling loop"&gt;&lt;/a&gt;
&lt;a href="https://photos.smugmug.com/photos/i-9BGSZZj/0/XL/i-9BGSZZj-XL.jpg"&gt;&lt;img alt="custom liquid cooling controller v1" src="https://photos.smugmug.com/photos/i-9BGSZZj/0/S/i-9BGSZZj-L.jpg" title="custom liquid cooling controller, v1"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What am I using it for? I have a couple things in mind. First, it sits in my Kubernetes cluster, and the GPU will let me transcode my security videos better. I thought I'd dabble with some other GPU computing, but there aren't great drivers or support for non-Nvidia cards :/. As a final use, if I can figure out how, it'd make for a nice Steam Remote gaming system, if I can figure out how to do it with a non-Windows and container-based system.&lt;/p&gt;
&lt;p&gt;The crowning touch was to add the 3D-printed replica of the Gavin Belson signature to the case. It looks perfect!&lt;/p&gt;
&lt;p&gt;&lt;a href="https://photos.smugmug.com/photos/i-pdnsKbH/0/XL/i-pdnsKbH-XL.jpg"&gt;&lt;img alt="the gavin belson liquid cooled server, top closed" src="https://photos.smugmug.com/photos/i-pdnsKbH/0/L/i-pdnsKbH-L.jpg" title="the gavin belson liquid cooled server"&gt;&lt;/a&gt;&lt;/p&gt;</description><guid>https://blog.tedder.dev/posts/gavin-belson-edition-liquid-cooled-server/</guid><pubDate>Sat, 04 Apr 2020 18:51:29 GMT</pubDate></item><item><title>Deleting (almost) all of my tweets</title><link>https://blog.tedder.dev/posts/deleting-almost-all-of-my-tweets/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;I decided a few months ago that &lt;a href="https://twitter.com/tedder42"&gt;my 10+ years of Twitter history&lt;/a&gt; wasn't helpful. It had &lt;a href="https://www.wired.com/story/im-deleting-all-my-old-tweets/"&gt;more chance to be a liability than anything&lt;/a&gt;. So I finally wrote a script to delete it.&lt;/p&gt;
&lt;p&gt;I deploy it locally as a Kubernetes cron, so all of the secrets are stored elsewhere. That means I've shared the repo:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/tedder/tweet-cleanup"&gt;https://github.com/tedder/tweet-cleanup&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It isn't perfect (for instance, dealing with pinned tweets), it's still hardcoded to my username, but I'd love PRs.&lt;/p&gt;</description><guid>https://blog.tedder.dev/posts/deleting-almost-all-of-my-tweets/</guid><pubDate>Mon, 03 Feb 2020 01:18:56 GMT</pubDate></item><item><title>Far Side comic newsfeed (RSS feed)</title><link>https://blog.tedder.dev/posts/far-side-comic-newsfeed-rss-feed/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;In late 2019, &lt;a href="https://www.thefarside.com/about/48/a-letter-from-gary-larson"&gt;the archives of Gary Larson's awesome The Far Side comic officially came back to the web&lt;/a&gt;. It's fun to see them, not just the 10 that &lt;a href="https://knowyourmeme.com/memes/needs-more-jpeg"&gt;needs more JPEG&lt;/a&gt;. Two 'daily' comics are posted per day, which is cool.&lt;/p&gt;
&lt;p&gt;Unfortunately, there's no newsfeed. So, I fixed that.  It uses the modern &lt;a href="https://jsonfeed.org/version/1"&gt;JSON feed spec&lt;/a&gt;.  The feed URL is: &lt;a href="https://dyn.tedder.me/rss/farside/daily.json"&gt;https://dyn.tedder.me/rss/farside/daily.json&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you need an older style feed, here's an Atom XML feed: &lt;a href="https://dyn.tedder.me/rss/farside/daily.xml"&gt;https://dyn.tedder.me/rss/farside/daily.xml&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can &lt;a href="http://www.newsblur.com/site/7690225/far-side-scraped-daily-feed"&gt;see a sample of it being used on Newsblur&lt;/a&gt; (even if you don't have a login).&lt;/p&gt;</description><guid>https://blog.tedder.dev/posts/far-side-comic-newsfeed-rss-feed/</guid><pubDate>Mon, 03 Feb 2020 00:42:27 GMT</pubDate></item><item><title>Robocalling in 12 minutes*</title><link>https://blog.tedder.dev/posts/robocalling-in-12-minutes/?utm_source=/rss.xml&amp;utm_medium=nikola_feed&amp;utm_campaign=rss_feed</link><dc:creator>tedder</dc:creator><description>&lt;p&gt;On John Oliver's Last Week Tonight, he showed how they were &lt;a href="https://www.youtube.com/watch?v=FO0iG_P0P6M"&gt;robocalling the FCC commissioners&lt;/a&gt;. As an offhanded comment he said their tech person literally got it running in less than 15 minutes. I took that as a challenge, and based on a comment from Kirsten, my goal was to get &lt;a href="https://soundcloud.com/tedder42/20180412-audrey-growling-hissing-during-nail-trim"&gt;audio of our Audreycat growling&lt;/a&gt; when she answered the phone.&lt;/p&gt;
&lt;p&gt;I was confident because I knew I had an account ready to go at &lt;a href="https://www.twilio.com/"&gt;Twilio&lt;/a&gt;. They have developer-friendly APIs and I had previous experience of managing over 10k phone numbers on their platform, though I hadn't done it recently.&lt;/p&gt;
&lt;p&gt;So, I bought a phone number from an area code she'd recognize and cobbled together the code from their sample:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;twilio.rest&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"+15035551212"&lt;/span&gt;
&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"+13095551212"&lt;/span&gt;
&lt;span class="n"&gt;acct_sid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"xx"&lt;/span&gt;
&lt;span class="n"&gt;acct_tok&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"yy"&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;acct_sid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;acct_tok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'https://tedder.me/twilio/hi3.xml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;from_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And here's the XML, which tells Twilio what to do after the phone is picked up, which I put on S3:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;Response&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;Say&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="na"&gt;voice=&lt;/span&gt;&lt;span class="s"&gt;"alice"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;hi&lt;span class="w"&gt; &lt;/span&gt;kirsten&lt;span class="nt"&gt;&amp;lt;/Say&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;Play&amp;gt;&lt;/span&gt;https://tedder.me/twilio/audrey.mp3&lt;span class="nt"&gt;&amp;lt;/Play&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/Response&amp;gt;&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sure enough, I had her phone ringing 12 minutes later. She picked up, and a robot voice told her there was an error. What?!&lt;/p&gt;
&lt;p&gt;So, I did it in 12 minutes, but it took another 13 minutes to figure out what was wrong. Twilio's error console said it was returning a 403 error, which didn't make sense, as I could access the content just fine.&lt;/p&gt;
&lt;p&gt;With some further digging I found that the XML URL is hit with &lt;code&gt;POST&lt;/code&gt;, so I knew the problem- the content was being served from S3, which only expects a &lt;code&gt;GET&lt;/code&gt;. So I had to dig further to find all of the parameters to the Twilio code. It was actually really hard to find good documentation for the python library, as everything pointed to &lt;a href="https://www.twilio.com/docs/libraries/python"&gt;the basics of using the library&lt;/a&gt;. Finally I found &lt;a href="https://www.twilio.com/docs/libraries/reference/twilio-python/6.25.1/index.html"&gt;the entrypoint to the documentation&lt;/a&gt;, and after a &lt;em&gt;lot&lt;/em&gt; of work I got to &lt;a href="https://www.twilio.com/docs/libraries/reference/twilio-python/6.25.1/docs/source/_rst/twilio.rest.api.v2010.account.call.html#twilio.rest.api.v2010.account.call.CallList"&gt;the &lt;code&gt;calls.create&lt;/code&gt; documentation&lt;/a&gt;. It was difficult to find but the answer was easy- I needed to add a parameter called &lt;code&gt;method&lt;/code&gt;:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;call&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'GET'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'https://tedder.me/twilio/hi3.xml'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;from_&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;
                    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After doing that it was easy. I can't say it was &lt;em&gt;done&lt;/em&gt; in 15 minutes like John Oliver's tech dude, but I still suspect his wasn't fully working after 15 minutes either.&lt;/p&gt;</description><category>code</category><category>python</category><category>tv</category><guid>https://blog.tedder.dev/posts/robocalling-in-12-minutes/</guid><pubDate>Thu, 14 Mar 2019 15:07:53 GMT</pubDate></item></channel></rss>