ESP32 Firmware skill for Codex

Reconnect, sleep and update without a cable. Codex treats an ESP32 project as a device that must recover alone: Wi-Fi is event-driven with reconnect, deep sleep is measured on the battery lead, partitions include two OTA slots with rollback, and every panic is decoded against an archived ELF. It also knows which strapping and flash pins to keep away from.

  • Embedded & IoT
  • $esp32-firmware
  • 183 lines
  • 2 files
  • ~2,050 tokens
  • MIT
  • Updated
  • Editor's pick

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- esp32-firmware

Installs to ~/.agents/skills/esp32-firmware/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $esp32-firmware, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Builds ESP32 firmware with ESP-IDF 5.x (or the Arduino-ESP32 3.x core) - Wi-Fi station with reconnect, NVS, deep sleep with timer and GPIO wake, custom partition tables, HTTPS OTA with rollback, and decoding panics and watchdog resets. Use when an ESP32, ESP32-S3, C3 or C6 project will not connect to Wi-Fi, drains its battery, boot-loops with Guru Meditation, needs OTA, or when choosing pins, partitions and sdkconfig options.

Use it when

  • An ESP32 drops off Wi-Fi or boot-loops with Guru Meditation errors
  • A battery-powered ESP32 node drains faster than expected
  • You need a partition table and OTA updates with rollback
  • Choosing GPIO pins, ADC channels or sdkconfig options for a new board

Use something else when

  • You need fleet-wide signing, staged rollouts or anti-rollback design: use firmware-ota
  • You are running MicroPython on the ESP32: use micropython

How to check the result

Power-cycle the access point and confirm reconnection, measure sleep current on the battery lead, and push a deliberately broken OTA image to prove rollback.

The mistake it prevents

Pulling GPIO12 high at boot on a classic ESP32 selects 1.8 V flash and the module will not boot; strapping pins need checking before any PCB layout.

~/.agents/skills/esp32-firmware/SKILL.md

--- name: esp32-firmware description: Builds ESP32 firmware with ESP-IDF 5.x (or the Arduino-ESP32 3.x core) - Wi-Fi station with reconnect, NVS, deep sleep with timer and GPIO wake,… license: MIT ---

Done well means: the device reconnects on its own after any Wi-Fi or power event, sleeps at the current the datasheet promises, can be updated over the air and roll back, and every crash in the field produces a decodable backtrace.

Inspect first

  1. Read AGENTS.md, then find the framework: CMakeLists.txt with include($ENV{IDF_PATH}/tools/cmake/project.cmake) is ESP-IDF; .ino or platformio.ini with framework = arduino is the Arduino core.
  2. Record the chip and IDF version: idf.py --version, CONFIG_IDF_TARGET in sdkconfig. ESP32, S2, S3, C3, C6 and H2 differ in cores, wake sources, ADC and radio (H2 has no Wi-Fi).
  3. Read sdkconfig.defaults (the committed source of truth) and partitions.csv. sdkconfig itself is generated; changes belong in sdkconfig.defaults.
idf.py set-target esp32s3        # wipes build config; do once per target
idf.py menuconfig                # then copy the changed keys into sdkconfig.defaults
idf.py build
idf.py size-components           # who uses flash and DRAM
idf.py -p /dev/ttyUSB0 flash monitor   # the user runs this; Codex rarely has the port

Installing ESP-IDF or components through the component manager (idf.py add-dependency "espressif/led_strip^3.0.0") needs network access.

Pin selection

Avoid on classic ESP32Why
GPIO 6-11Wired to the SPI flash
GPIO 0, 2, 5, 12, 15Strapping pins; GPIO12 high at boot selects 1.8 V flash and bricks boot on most modules
GPIO 34-39 for outputsInput only, no internal pull-ups
ADC2 pins with Wi-Fi onADC2 is unavailable while Wi-Fi runs; use ADC1

S3 and C3 have different strapping pins and USB-JTAG on GPIO 19/20 (S3) or 18/19 (C3); check the chip’s datasheet table rather than reusing ESP32 pin maps.

Wi-Fi station that survives the real world

static EventGroupHandle_t s_net;
#define NET_UP BIT0

static void on_event(void *arg, esp_event_base_t base, int32_t id, void *data) {
    if (base == WIFI_EVENT && id == WIFI_EVENT_STA_START) {
        esp_wifi_connect();
    } else if (base == WIFI_EVENT && id == WIFI_EVENT_STA_DISCONNECTED) {
        xEventGroupClearBits(s_net, NET_UP);
        esp_wifi_connect();                      // add backoff for battery devices
    } else if (base == IP_EVENT && id == IP_EVENT_STA_GOT_IP) {
        xEventGroupSetBits(s_net, NET_UP);
    }
}

void wifi_start(void) {
    esp_err_t err = nvs_flash_init();
    if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        err = nvs_flash_init();
    }
    ESP_ERROR_CHECK(err);
    s_net = xEventGroupCreate();
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());
    esp_netif_create_default_wifi_sta();
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));
    ESP_ERROR_CHECK(esp_event_handler_register(WIFI_EVENT, ESP_EVENT_ANY_ID, on_event, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_STA_GOT_IP, on_event, NULL));
    wifi_config_t wc = { .sta = { .threshold.authmode = WIFI_AUTH_WPA2_PSK } };
    strlcpy((char *)wc.sta.ssid, CONFIG_APP_WIFI_SSID, sizeof wc.sta.ssid);
    strlcpy((char *)wc.sta.password, CONFIG_APP_WIFI_PASS, sizeof wc.sta.password);
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_STA));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_STA, &wc));
    ESP_ERROR_CHECK(esp_wifi_start());
}
  • Code that needs the network waits on NET_UP with a timeout; it never assumes it.
  • Credentials come from Kconfig for development and NVS (provisioning) for products; never commit real SSIDs and passwords in sdkconfig.defaults.
  • Arduino core equivalent: WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(true); plus a WiFi.onEvent handler; still check WiFi.status() before every network call.

Deep sleep

RTC_DATA_ATTR static uint32_t boot_count;           // survives deep sleep, not power loss

void sleep_now(uint64_t seconds) {
    esp_sleep_enable_timer_wakeup(seconds * 1000000ULL);
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_33, 0);  // ext0: ESP32/S2/S3 only, RTC GPIO
    esp_wifi_stop();
    esp_deep_sleep_start();                         // does not return; next run is a reset
}
  • Read esp_sleep_get_wakeup_cause() at boot to branch between timer, button and cold boot.
  • Board current, not chip current, decides battery life: USB-UART bridges, LDO quiescent current and power LEDs often draw 1-10 mA. Measure with a current meter on the battery lead, not the USB port.
  • Hold output states through sleep with gpio_hold_en() plus gpio_deep_sleep_hold_en(), or pins float and external parts wake up.
  • C3 and C6 wake from GPIO through esp_deep_sleep_enable_gpio_wakeup(), not ext0.

Partitions and OTA

A minimal two-slot table for 4 MB flash:

# Name,   Type, SubType, Offset,   Size
nvs,      data, nvs,     0x9000,   0x4000
otadata,  data, ota,     0xd000,   0x2000
phy_init, data, phy,     0xf000,   0x1000
ota_0,    app,  ota_0,   0x10000,  0x1E0000
ota_1,    app,  ota_1,   ,         0x1E0000

Set CONFIG_PARTITION_TABLE_CUSTOM=y, CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" and CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y in sdkconfig.defaults.

esp_http_client_config_t http = { .url = url, .cert_pem = server_ca_pem, .timeout_ms = 15000 };
esp_https_ota_config_t ota = { .http_config = &http };
if (esp_https_ota(&ota) == ESP_OK) esp_restart();

// In the new image, after self-tests pass (network up, sensors read):
esp_ota_mark_app_valid_cancel_rollback();

Changing the partition table on deployed devices requires a serial reflash; get it right before shipping. Signing, staged rollout and anti-rollback live in firmware-ota.

Crashes and resets

OutputMeaningAction
Guru Meditation Error: Core 0 panic'ed (LoadProhibited)Null or wild pointer readDecode the backtrace (below)
***ERROR*** A stack overflow in task XTask stack too smallRaise the stack in xTaskCreate (bytes on ESP-IDF)
Task watchdog got triggeredA task never yieldsAdd vTaskDelay, move work off busy loops
Brownout detector was triggeredSupply sags on Wi-Fi TX burstsBulk capacitor, better regulator; do not disable the detector
rst:0x10 (RTCWDT_RTC_RESET) boot loopBad strapping pin or flash settingsCheck GPIO 0/2/12/15 wiring

idf.py monitor decodes backtraces automatically when given the matching ELF. For field logs, use xtensa-esp32-elf-addr2line -pfiaC -e build/app.elf 0x400d1234 ... (RISC-V chips use riscv32-esp-elf-addr2line). Keep every released ELF; without it a backtrace is noise. Enable CONFIG_ESP_COREDUMP_ENABLE_TO_FLASH for devices you cannot watch over serial.

Gotchas

  • ESP_ERROR_CHECK aborts on failure; in production paths handle the error instead.
  • Do not call blocking or logging functions from ISRs; defer to a task (freertos).
  • idf.py set-target deletes the existing sdkconfig; anything not in sdkconfig.defaults is lost.
  • The Arduino core’s delay() yields to FreeRTOS; a tight while loop without it trips the watchdog on core 0.
  • NVS keys are at most 15 characters.

Verification

  • idf.py build clean; idf.py size within budget with OTA slot headroom (aim for 20 %)
  • Power-cycle the router during operation: device reconnects without reboot
  • Deep sleep current measured on the battery lead and written down
  • OTA to a deliberately broken image rolls back to the previous slot
  • A forced panic produces a decodable backtrace with the archived ELF

Report

**Target:** ESP32-S3, ESP-IDF v5.3, 8 MB flash, custom 2-slot OTA table
**Change:** event-driven Wi-Fi reconnect with backoff; deep sleep 300 s with ext0 button wake
**Measured:** 14 uA in sleep on the battery lead (was 4.1 mA: power LED, now removed)
**OTA:** https OTA with rollback; broken-image test rolled back correctly
**Open:** provisioning still uses Kconfig credentials; NVS-based provisioning not done

What the zip contains

SKILL.md plus the interface file below. Nothing runs on install.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "ESP32 Firmware"
  short_description: "Reconnect, sleep and update without a cable"
  default_prompt: "$esp32-firmware My ESP32 never reconnects after the router reboots, fix the Wi-Fi code"

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem