
Esp32 Firmware Engineer
- 919 installs
- 7 repo stars
- Updated February 24, 2026
- adamlipecz/esp32-firmware-engineer-skill
esp32-firmware-engineer is a Claude Code skill that acts as an on-demand ESP32 firmware specialist for developers who need ESP-IDF code written, reviewed, debugged, optimized, and hardened for connected devices.
About
esp32-firmware-engineer is an adamlipecz/esp32-firmware-engineer-skill package activated by keywords including esp32, esp-idf, esp32s3, esp32c3, esp32c6, freertos, idf.py, sdkconfig, partitions.csv, secure boot, and flash encryption. The skill writes, reviews, and debugs ESP-IDF C/C++ firmware; analyzes FreeRTOS task, queue, semaphore, and ISR race conditions; and covers power modes like deep sleep and light sleep plus OTA and security hardening. It also supports ESP-ADF, ESP-SR, and LVGL integrations for audio, speech, and embedded UI on ESP32 variants. Developers reach for esp32-firmware-engineer when facing guru meditation errors, bring-up issues, sdkconfig tuning, or production hardening on Espressif hardware.
- Writes, reviews, and debugs ESP-IDF C/C++ firmware for any ESP32 variant
- Diagnoses Guru Meditation errors, FreeRTOS race conditions, boot failures and reset reasons
- Designs OTA update flows, power-optimized deep-sleep and light-sleep modes with correct wakeup sources
- Implements and validates I2C, SPI, UART, CAN, LVGL, ESP-ADF and ESP-SR integrations
- Hardens firmware with Secure Boot v2, flash encryption and NVS encryption
Esp32 Firmware Engineer by the numbers
- 919 all-time installs (skills.sh)
- +33 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,200 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/adamlipecz/esp32-firmware-engineer-skill --skill esp32-firmware-engineerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 919 |
|---|---|
| repo stars | ★ 7 |
| Security audit | 2 / 3 scanners passed |
| Last updated | February 24, 2026 |
| Repository | adamlipecz/esp32-firmware-engineer-skill ↗ |
How do you debug and optimize ESP-IDF firmware on ESP32?
Get an on-demand ESP32 firmware specialist that writes, reviews, debugs, optimizes, and hardens ESP-IDF code for connected devices.
Who is it for?
Embedded developers working on ESP32, ESP32-S3, or ESP32-C3 firmware with ESP-IDF, FreeRTOS, OTA, or secure-boot requirements.
Skip if: Pure mobile app development or cloud-only backends with no ESP-IDF firmware or Espressif hardware in scope.
When should I use this skill?
The developer mentions ESP32, ESP-IDF, idf.py, FreeRTOS, guru meditation, sdkconfig, OTA, secure boot, or ESP32 bring-up problems.
What you get
Reviewed ESP-IDF source changes, FreeRTOS concurrency fixes, sdkconfig/partitions.csv updates, and OTA or security hardening steps.
- Firmware code changes
- sdkconfig recommendations
- Debug analysis
Files
ESP32 Firmware Engineer
Act as a senior ESP-IDF firmware engineer focused on correctness, debuggability, and fast iteration.
Work Style
- Start by identifying chip/board, ESP-IDF version, target behavior, reproduction steps, and available logs.
- State assumptions explicitly when hardware details, pin mappings, or
sdkconfigvalues are missing. - Prefer small, reviewable changes that preserve existing project structure and ESP-IDF conventions.
- Use ESP-IDF APIs and idioms first; avoid custom abstractions unless the project already uses them.
- Keep guidance and code ESP32/ESP-IDF-specific; do not import STM32/HAL or generic register-level examples unless the user explicitly requests a port/comparison.
- Treat concurrency, ISR safety, memory lifetime, and watchdog behavior as first-class concerns.
- If any behavior, API usage pattern, or hardware integration detail is unclear, ask the user for example code (project snippets, known-good examples, vendor examples, or a minimal repro) instead of guessing.
Non-Negotiable Blockers
- For hardware-integrated implementation/debug/bring-up work, do not proceed until the hardware context is explicit: target board, exact ESP32 variant, peripheral list, pin mapping, electrical constraints, and connected devices.
- If any of the above is missing or ambiguous, stop and ask the user for it. Treat "almost clear" as not clear enough.
- If design intent or expected behavior is unclear, ask for a representative example implementation or reference snippet before proceeding.
- Do not continue when the exact ESP32 variant is unknown.
esp32,esp32s3,esp32c3,esp32c6, etc. differ in cores, peripherals, memory, and low-power behavior. - Do not guess partition strategy or flash layout. Confirm OTA requirement, flash size, storage needs, and rollback/update expectations first.
- Do not proceed when plugin/framework compatibility is unverified. For ESP-IDF with ESP-ADF/ESP-SR (or similar), require concrete version compatibility evidence before build/flash/debug.
- If a task is pure code review/refactor with no hardware behavior change, note missing hardware context as a risk but continue only within the provided code scope.
ESP32-Specific Triage Inputs
- Identify exact target (
esp32,esp32s2,esp32s3,esp32c3,esp32c6, etc.) because core count, peripherals, and wakeup features differ. - Identify ESP-IDF version and whether the project uses legacy vs newer driver APIs (for example I2C/ADC API style).
- Identify board wiring constraints: pin map, pull-ups, transceivers, level shifting, power rails, and boot/strapping pin usage.
- Identify whether PSRAM, OTA, Wi-Fi, BLE, or deep sleep is in scope because they change memory/power/debug assumptions.
- Identify all external ESP frameworks/components in use (for example ESP-ADF, ESP-SR, ESP-SKAINET, LVGL, custom managed components) and their exact versions/tags.
- Identify display/controller details (interface, color depth/pixel format, byte order, frame buffer model, and LVGL version) before writing graphics paths.
- Identify flash size/speed mode and PSRAM availability/mode when performance or memory placement matters.
- Identify whether a USB/serial console path is available and unused by product features (USB CDC, USB-Serial-JTAG, or external USB-UART) and whether security policy allows an on-device service terminal.
Execute the Task
1. Triage the request. 2. Classify the work as write, review, debug, or bring-up. 3. Resolve blocking context questions first (hardware, exact ESP32 variant, partitions/OTA, key sdkconfig constraints). 4. Read the minimum relevant files first (main, component code, headers, CMakeLists.txt, sdkconfig, partition CSV, logs, scripts). 5. Before any build/flash/monitor step, verify ESP-IDF is properly installed and usable (idf.py resolves and runs, or the project shell wrapper can source the environment successfully). 6. Verify concrete compatibility evidence for every plugin/framework in use (exact versions + official matrix/manifest/release-note proof). If any link in the stack is ambiguous, stop and resolve it first. 7. Build a failure model before editing code for debugging tasks. 8. Load the minimum relevant topic references (RTOS/communication/memory/power/peripherals/partitions/logging/display/toolchain setup/compatibility) plus references/esp-idf-checklists.md. 9. Implement changes. 10. Run the project's build.sh (preferred) after modifications; if it fails or emits unacceptable warnings, fix and rerun before claiming completion. 11. Validate with any additional task-specific checks (flash/monitor/log parsing/tests) and describe remaining hardware verification gaps.
Writing Firmware
- Define task boundaries, ownership, and synchronization before adding logic.
- Keep ISR handlers minimal; defer work to tasks/queues/event groups/timers.
- Check and propagate
esp_err_t; log actionable context on failure paths. - Use
ESP_LOGxconsistently with stable tags. - Guard hardware initialization order and re-init paths.
- Prefer editing
sdkconfig/sdkconfig.defaultsdirectly for reproducible configuration changes instead of relying onmenuconfiginstructions, unless the user explicitly asks formenuconfig. - Update partitions intentionally based on flash size and requirements; use the available flash capacity instead of leaving unexplained unused space.
- If OTA is required, use an OTA-compatible partition layout and preserve room for required app/data partitions.
- If the USB/console transport is free and product/security constraints allow it, proactively implement a basic device terminal (without waiting for the user to ask) using ESP-IDF console primitives with autocomplete, help, and a small set of high-value commands (settings, status, RTOS/heap diagnostics, log level control).
- Add comments only for non-obvious hardware timing, register constraints, or concurrency behavior.
Reviewing Firmware
- Prioritize correctness and regression risk over style.
- Check FreeRTOS API context rules (ISR-safe vs task context APIs).
- Check stack usage risk, blocking calls, and timeout handling.
- Check resource lifecycle (NVS, drivers, sockets, event handlers, semaphores).
- Check pin conflicts, peripheral mode assumptions, and clock/timing assumptions.
- Check partition table and
sdkconfigconsistency with flash size, OTA requirements, logging level, and enabled features. - Check display code validates controller pixel format/endianness and buffer format instead of assuming RGB layout.
- Check chosen bus/peripheral configuration (clock, DMA, memory placement) matches performance requirements and hardware limits.
- Check logging quality for field debugging.
- For code reviews, present findings first with file/line references.
Debugging Firmware
- Reproduce and narrow scope before changing multiple subsystems.
- Separate build-time, flash-time, boot-time, and runtime failures.
- For panics/resets, capture the exact reset reason, panic output, and preceding logs.
- For Wi-Fi/BLE issues, verify initialization order, event handling, retries/backoff, and credential/config state.
- For peripheral issues, verify GPIO mapping, pull-ups, voltage levels, timing, and bus ownership assumptions.
- For display issues, confirm controller, bus mode, resolution, color depth, byte order, and framebuffer/pixel packing expectations before changing draw code.
- If logs and symptoms are insufficient to localize the fault, ask for a minimal reproducible example or a known-good reference implementation path.
- Prefer instrumentation (extra logs/counters/asserts) over speculative rewrites.
Build / Flash / Monitor Guidance
- Prefer project wrapper scripts (
build.sh,flash.sh,monitor.sh) if present, withidf.pyas the underlying engine. - Use
idf.py build,idf.py flash, andidf.py monitoras the baseline workflow when wrappers are absent. - Before building, confirm ESP-IDF tooling is actually usable (
idf.py --versionsucceeds), not just present onPATH. - Before building, confirm plugin/framework compatibility with concrete evidence (for example ADF README matrix row+column, SR
idf_component.ymlidfdependency range, pinned compatibility lock file for cross-stack combinations). - If ESP-IDF env setup is missing, add a shell convenience snippet (for example in
~/.zshrc) that aliasesidftosource ~/.esp_idf_envand ensures common user bins are onPATH. - Include exact commands and environment assumptions when giving instructions.
- Mention when a clean rebuild may be required (
idf.py fullclean build) and why. - Mention serial port/baud assumptions when debugging flash or monitor problems.
- Do not report implementation work as done until the build passes through the project's build script/workflow.
- Reuse and adapt the reference wrappers in
scripts/when a project lacks wrappers. - Use the plugin compatibility checker in
scripts/check_plugin_compatibility.py(or equivalent project preflight) to generate a concrete evidence report before build.
Logging Defaults
- Reduce noisy library/default component logs when they obscure diagnosis (often by raising their log level threshold).
- Keep application logs verbose and structured during development/debugging (module tags, state transitions, error codes, retries, timing).
- Prefer targeted log filtering over globally suppressing useful diagnostics.
- If a service terminal is present, expose runtime log-level adjustment commands so debugging verbosity can be changed without reflashing.
Output Format
- For implementation tasks: state the change, then key technical decisions, then validation.
- For review tasks: list findings first by severity, then open questions/assumptions.
- For debugging tasks: state likely causes, evidence, next diagnostic step, and proposed fix.
- Always call out what was not verified in hardware.
Use the References
- Read
references/values.mdfirst for non-negotiable engineering values and blocking behavior. - Read
references/esp-idf-checklists.mdfor implementation/review/debug checklists. - Read
references/panic-log-triage.mdfor panic, reset, and logging triage patterns. - Read
references/rtos-patterns.mdfor FreeRTOS tasking, ISR handoff, timers, watchdog-safe concurrency, and dual-core concerns. - Read
references/communication-protocols.mdfor ESP-IDF I2C/SPI/UART/TWAI patterns, bus ownership, timeouts, and recovery. - Read
references/memory-optimization.mdfor heap capabilities, stack sizing, DMA-capable buffers, code-size analysis, and partition-aware memory decisions. - Read
references/power-optimization.mdfor ESP32 sleep modes, wakeup sources, PM locks, wireless power strategy, and battery-aware behavior. - Read
references/microcontroller-programming.mdfor ESP32 GPIO/ISR/timer/PWM/ADC/watchdog programming patterns in ESP-IDF. - Read
references/partitions-and-sdkconfig.mdfor partition sizing, OTA layouts, and reproduciblesdkconfigediting workflow. - Read
references/logging-and-observability.mdfor ESP-IDF log level policy and application log design. - Read
references/display-graphics.mdfor display controller formats, frame buffer layout, and graphics pipeline validation. - Read
references/device-terminal-console.mdfor ESP-IDF on-device terminal design, autocomplete, and runtime diagnostics commands. - Read
references/toolchain-and-shell-setup.mdfor ESP-IDF install preflight checks and shell UX snippets (.zshrc,.bashrc). - Read
references/dependency-compatibility.mdfor version compatibility evidence rules and ESP-IDF/ESP-ADF/ESP-SR validation workflow. - Read
references/ota-workflow.mdfor OTA partition layouts,esp_ota_opsAPI flow, HTTPS OTA, rollback, anti-rollback counter, and OTA failure modes. - Read
references/security-hardening.mdfor Secure Boot v2, flash encryption, NVS encryption, JTAG/UART disable, service terminal hardening, and the production security checklist. - Read
references/lvgl-display.mdfor LVGL version compatibility, flush callback patterns (v8 vs v9), tick source setup, thread-safety mutex pattern, color format/byte order, memory allocation for DMA and PSRAM, and common display pitfalls.
Use Bundled Templates
- Reuse ESP32/ESP-IDF templates from
assets/templates/for new components, display flush paths, and partition layouts. - Reuse
assets/templates/esp-console/when adding a user-friendly on-device terminal with command registration and diagnostics. - Reuse
assets/templates/shell/snippets when setting up shell aliases/path helpers for ESP-IDF workflows. - Reuse
assets/templates/compatibility/lock-file templates to record exact known-good framework stacks. - Adapt templates to the exact ESP32 variant, board pin map, and required peripherals before implementation.
Trigger Examples
- "Review this ESP-IDF task code for FreeRTOS race conditions"
- "Debug why my ESP32 Wi-Fi reconnect loop never recovers"
- "Write an ESP-IDF I2C sensor driver init and read task"
- "Help interpret this Guru Meditation panic from
idf.py monitor" - "Fix build/flash errors in my ESP32 ESP-IDF project"
- "Reduce deep sleep current on my ESP32 board and check wakeup configuration"
- "Cut RAM/code size in this ESP-IDF component and review heap/stack usage"
- "Design an OTA-compatible partition table for 16MB flash and update sdkconfig"
- "My ESP32 display colors are wrong; verify pixel format/endianness and bus config"
- "Add a friendly serial/USB terminal with settings commands and RTOS debug info"
- "This project uses ESP-ADF and ESP-SR; prove the exact ESP-IDF version is compatible before building"
- "Design an OTA update flow with rollback and anti-rollback for a field device"
- "Harden this ESP32 project for production: secure boot, flash encryption, disable JTAG"
- "Integrate LVGL v9 with an ST7789 display on ESP32-S3 via SPI with DMA"
- "My ESP32 display colors are wrong after switching LVGL versions"
- "ESP32 won't enter deep sleep / exits sleep immediately after wakeup stub"
interface:
display_name: "ESP32 Firmware Engineer"
short_description: "ESP-IDF firmware write, review, debug, power, OTA, security, and bring-up"
# Claude Code skill invocation: @esp32-firmware-engineer or via SKILL.md trigger matching
activation_keywords:
- esp32
- esp-idf
- esp32s3
- esp32c3
- esp32c6
- esp32s2
- freertos
- idf.py
- guru meditation
- sdkconfig
- partitions.csv
- esp-adf
- esp-sr
- lvgl
- deep sleep
- light sleep
- secure boot
- flash encryption
capabilities:
- Write, review, and debug ESP-IDF C/C++ firmware
- Analyze FreeRTOS task/queue/semaphore/ISR patterns and race conditions
- Diagnose Guru Meditation panics, reset reasons, and boot failures
- Optimize RAM, flash, and code size
- Design OTA-compatible partition tables and implement OTA update flows
- Configure deep sleep and light sleep power modes with correct wakeup sources
- Implement I2C, SPI, UART, TWAI/CAN, ADC, PWM, GPIO, and RMT drivers
- Integrate and validate LVGL with ESP-IDF and display controllers
- Add on-device USB/serial service terminals with runtime diagnostics
- Validate ESP-ADF/ESP-SR plugin compatibility (online and local)
- Harden firmware with Secure Boot v2, flash encryption, NVS encryption
- Fix build, flash, and monitor workflow issues
required_context:
- Exact ESP32 variant (esp32, esp32s3, esp32c3, esp32c6, etc.)
- ESP-IDF version
- Board pinmap and hardware constraints
- Relevant sdkconfig settings and flash size
model_hints:
prefer_extended_thinking: true # deep bug triage and architectural decisions benefit from it
tool_use: true
context_window: large # reference docs are read in full during triage
default_prompt: |
Use the esp32-firmware-engineer skill to implement, review, or debug this ESP-IDF task.
Block on missing context: require exact ESP32 variant, ESP-IDF version, and hardware
details (pin map, peripheral list, flash size) before proceeding with hardware-integrated work.
Load SKILL.md plus the minimum relevant references before writing any code.
interface:
display_name: "ESP32 Firmware Engineer"
short_description: "ESP-IDF firmware write, review, debug, power, OTA, security, and bring-up"
activation_keywords:
- esp32
- esp-idf
- esp32s3
- esp32c3
- esp32c6
- esp32s2
- freertos
- idf.py
- guru meditation
- sdkconfig
- partitions.csv
- esp-adf
- esp-sr
- lvgl
- deep sleep
- light sleep
- secure boot
- flash encryption
capabilities:
- Write, review, and debug ESP-IDF C/C++ firmware
- Analyze FreeRTOS task/queue/semaphore/ISR patterns and race conditions
- Diagnose Guru Meditation panics, reset reasons, and boot failures
- Optimize RAM, flash, and code size
- Design OTA-compatible partition tables and implement OTA update flows
- Configure deep sleep and light sleep power modes with correct wakeup sources
- Implement I2C, SPI, UART, TWAI/CAN, ADC, PWM, GPIO, and RMT drivers
- Integrate and validate LVGL with ESP-IDF and display controllers
- Add on-device USB/serial service terminals with runtime diagnostics
- Validate ESP-ADF/ESP-SR plugin compatibility (online and local)
- Harden firmware with Secure Boot v2, flash encryption, NVS encryption
- Fix build, flash, and monitor workflow issues
required_context:
- Exact ESP32 variant (esp32, esp32s3, esp32c3, esp32c6, etc.)
- ESP-IDF version
- Board pinmap and hardware constraints
- Relevant sdkconfig settings and flash size
default_prompt: |
Use the esp32-firmware-engineer skill to implement, review, or debug this ESP-IDF task.
Block on missing context: require exact ESP32 variant, ESP-IDF version, and hardware
details (pin map, peripheral list, flash size) before proceeding with hardware-integrated work.
# ESP framework compatibility lock (example)
#
# Use this file to record exact, proven framework combinations before build/debug/flash.
# Replace the versions and evidence references with your project's exact stack.
esp_idf:
version: "v5.3"
source: "git tag"
frameworks:
- name: "esp-adf"
version: "v2.7"
source: "git tag"
idf_compat_evidence:
type: "adf-readme-matrix"
location: "esp-adf/README.md"
proof: "Release/v2.7 row marks Release/v5.3 column as supported"
- name: "esp-sr"
version: "v1.9.5"
source: "git tag"
idf_compat_evidence:
type: "idf_component_manifest"
location: "esp-sr/idf_component.yml"
proof: "dependencies.idf includes selected ESP-IDF version"
cross_stack_evidence:
applies_to:
- "esp-adf"
- "esp-sr"
type: "project-validated"
proof: "Audio pipeline + speech recognition smoke build/test passed with the versions above"
notes: "Add upstream release/bundle reference if available (e.g. ESP-SKAINET release)"
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "esp_err.h"
#include "esp_log.h"
static const char *TAG = "display_flush";
typedef enum {
DISPLAY_FMT_RGB565_LE,
DISPLAY_FMT_RGB565_BE,
DISPLAY_FMT_RGB888,
} display_pixel_format_t;
typedef struct {
int width;
int height;
display_pixel_format_t pixel_format;
bool bgr_order;
} display_caps_t;
typedef struct {
int x;
int y;
int w;
int h;
const void *pixels;
size_t len_bytes;
} display_flush_region_t;
esp_err_t display_flush_checked(const display_caps_t *caps, const display_flush_region_t *r)
{
if (caps == NULL || r == NULL || r->pixels == NULL) {
return ESP_ERR_INVALID_ARG;
}
if (r->x < 0 || r->y < 0 || r->w <= 0 || r->h <= 0) {
return ESP_ERR_INVALID_ARG;
}
if ((r->x + r->w) > caps->width || (r->y + r->h) > caps->height) {
return ESP_ERR_INVALID_ARG;
}
size_t bpp = 0;
switch (caps->pixel_format) {
case DISPLAY_FMT_RGB565_LE:
case DISPLAY_FMT_RGB565_BE:
bpp = 2;
break;
case DISPLAY_FMT_RGB888:
bpp = 3;
break;
default:
return ESP_ERR_NOT_SUPPORTED;
}
size_t expected = (size_t)r->w * (size_t)r->h * bpp;
if (r->len_bytes != expected) {
ESP_LOGE(TAG, "flush size mismatch: got=%u expected=%u", (unsigned)r->len_bytes,
(unsigned)expected);
return ESP_ERR_INVALID_SIZE;
}
ESP_LOGD(TAG, "flush x=%d y=%d w=%d h=%d fmt=%d bgr=%d", r->x, r->y, r->w, r->h,
(int)caps->pixel_format, caps->bgr_order);
/* Replace with actual panel transaction and DMA-safe buffer handling. */
return ESP_OK;
}
#include "app_console_commands.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include "esp_chip_info.h"
#include "esp_console.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_system.h"
#include "esp_timer.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "app_console";
static const char *app_reset_reason_to_str(esp_reset_reason_t reason)
{
switch (reason) {
case ESP_RST_UNKNOWN: return "unknown";
case ESP_RST_POWERON: return "poweron";
case ESP_RST_EXT: return "ext";
case ESP_RST_SW: return "sw";
case ESP_RST_PANIC: return "panic";
case ESP_RST_INT_WDT: return "int_wdt";
case ESP_RST_TASK_WDT: return "task_wdt";
case ESP_RST_WDT: return "wdt";
case ESP_RST_DEEPSLEEP: return "deepsleep";
case ESP_RST_BROWNOUT: return "brownout";
case ESP_RST_SDIO: return "sdio";
default: return "other";
}
}
static esp_log_level_t app_parse_log_level(const char *s, bool *ok)
{
*ok = true;
if (strcmp(s, "e") == 0 || strcmp(s, "error") == 0) return ESP_LOG_ERROR;
if (strcmp(s, "w") == 0 || strcmp(s, "warn") == 0 || strcmp(s, "warning") == 0) return ESP_LOG_WARN;
if (strcmp(s, "i") == 0 || strcmp(s, "info") == 0) return ESP_LOG_INFO;
if (strcmp(s, "d") == 0 || strcmp(s, "debug") == 0) return ESP_LOG_DEBUG;
if (strcmp(s, "v") == 0 || strcmp(s, "verbose") == 0) return ESP_LOG_VERBOSE;
*ok = false;
return ESP_LOG_NONE;
}
/*
* Weak hooks let projects back the terminal with real application settings storage
* without editing the command parser.
*/
__attribute__((weak)) int app_console_settings_get(const char *key)
{
printf("settings.get not implemented for key='%s'\n", key);
return 0;
}
__attribute__((weak)) int app_console_settings_set(const char *key, const char *value)
{
printf("settings.set not implemented for key='%s' value='%s'\n", key, value);
return 0;
}
__attribute__((weak)) int app_console_settings_save(void)
{
printf("settings.save not implemented\n");
return 0;
}
static int cmd_status(int argc, char **argv)
{
(void)argc;
(void)argv;
esp_chip_info_t chip_info = {0};
esp_chip_info(&chip_info);
printf("uptime_ms=%lld\n", (long long)(esp_timer_get_time() / 1000));
printf("reset_reason=%s\n", app_reset_reason_to_str(esp_reset_reason()));
printf("cores=%d features=0x%x revision=%d\n",
chip_info.cores, chip_info.features, chip_info.revision);
return 0;
}
static int cmd_heap(int argc, char **argv)
{
(void)argc;
(void)argv;
size_t free_8bit = heap_caps_get_free_size(MALLOC_CAP_8BIT);
size_t min_8bit = heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT);
size_t largest_8bit = heap_caps_get_largest_free_block(MALLOC_CAP_8BIT);
printf("heap_8bit_free=%u min=%u largest=%u\n",
(unsigned)free_8bit, (unsigned)min_8bit, (unsigned)largest_8bit);
return 0;
}
static int cmd_tasks(int argc, char **argv)
{
(void)argc;
(void)argv;
printf("num_tasks=%u\n", (unsigned)uxTaskGetNumberOfTasks());
#if (INCLUDE_uxTaskGetStackHighWaterMark == 1)
printf("current_task_stack_hwm_words=%u\n",
(unsigned)uxTaskGetStackHighWaterMark(NULL));
#else
printf("stack_hwm_unavailable (enable INCLUDE_uxTaskGetStackHighWaterMark)\n");
#endif
printf("tip=extend this command with per-task snapshots when trace/stats config is enabled\n");
return 0;
}
static int cmd_log(int argc, char **argv)
{
if (argc != 4 || strcmp(argv[1], "level") != 0) {
printf("usage: log level <tag|*> <error|warn|info|debug|verbose>\n");
return 1;
}
bool ok = false;
esp_log_level_t level = app_parse_log_level(argv[3], &ok);
if (!ok) {
printf("invalid level '%s'\n", argv[3]);
return 1;
}
esp_log_level_set(argv[2], level);
printf("log_level_set tag=%s level=%s\n", argv[2], argv[3]);
return 0;
}
static int cmd_reboot(int argc, char **argv)
{
(void)argc;
(void)argv;
printf("restarting...\n");
fflush(stdout);
esp_restart();
return 0;
}
static int cmd_settings(int argc, char **argv)
{
if (argc < 2) {
printf("usage:\n");
printf(" settings get <key>\n");
printf(" settings set <key> <value>\n");
printf(" settings save\n");
return 1;
}
if (strcmp(argv[1], "get") == 0) {
if (argc != 3) {
printf("usage: settings get <key>\n");
return 1;
}
return app_console_settings_get(argv[2]);
}
if (strcmp(argv[1], "set") == 0) {
if (argc != 4) {
printf("usage: settings set <key> <value>\n");
return 1;
}
return app_console_settings_set(argv[2], argv[3]);
}
if (strcmp(argv[1], "save") == 0) {
if (argc != 2) {
printf("usage: settings save\n");
return 1;
}
return app_console_settings_save();
}
printf("unknown settings subcommand '%s'\n", argv[1]);
return 1;
}
static esp_err_t app_register_cmd(const char *name, const char *help,
esp_console_cmd_func_t func)
{
const esp_console_cmd_t cmd = {
.command = name,
.help = help,
.hint = NULL,
.func = func,
.argtable = NULL,
};
return esp_console_cmd_register(&cmd);
}
esp_err_t app_console_register_commands(void)
{
esp_err_t err = esp_console_register_help_command();
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
return err;
}
ESP_ERROR_CHECK(app_register_cmd("status",
"Show uptime, reset reason, and chip summary",
cmd_status));
ESP_ERROR_CHECK(app_register_cmd("heap",
"Show heap free/min/largest block summary",
cmd_heap));
ESP_ERROR_CHECK(app_register_cmd("tasks",
"Show RTOS task/debug summary",
cmd_tasks));
ESP_ERROR_CHECK(app_register_cmd("settings",
"Get/set/save application settings",
cmd_settings));
ESP_ERROR_CHECK(app_register_cmd("log",
"Runtime log control: log level <tag|*> <level>",
cmd_log));
ESP_ERROR_CHECK(app_register_cmd("reboot",
"Restart the device",
cmd_reboot));
ESP_LOGI(TAG, "service terminal commands registered");
return ESP_OK;
}
idf_component_register(
SRCS "app_console_commands.c"
INCLUDE_DIRS "include"
REQUIRES console esp_system esp_timer
)
#pragma once
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
/*
* Register a basic service terminal command set (help/status/settings/heap/tasks/log/reboot).
*
* Use ESP-IDF's REPL helpers (UART / USB CDC / USB-Serial-JTAG, depending target/IDF version)
* to provide line editing, history, help, and autocomplete.
*/
esp_err_t app_console_register_commands(void);
#ifdef __cplusplus
}
#endif
idf_component_register(
SRCS "esp32_component_template.c"
INCLUDE_DIRS "include"
REQUIRES driver esp_timer
)
#include "esp32_component_template.h"
#include <inttypes.h>
#include "driver/gpio.h"
#include "esp_check.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
static const char *TAG = "comp_template";
static TaskHandle_t s_task;
static esp32_component_template_config_t s_cfg;
static bool s_initialized;
static void component_task(void *arg)
{
const TickType_t period = pdMS_TO_TICKS(s_cfg.period_ms);
TickType_t last = xTaskGetTickCount();
for (;;) {
int level = s_cfg.active_high ? 1 : 0;
gpio_set_level(s_cfg.gpio_num, level);
ESP_LOGD(TAG, "tick gpio=%d level=%d", s_cfg.gpio_num, level);
vTaskDelayUntil(&last, period);
}
}
esp_err_t esp32_component_template_init(const esp32_component_template_config_t *cfg)
{
ESP_RETURN_ON_FALSE(cfg != NULL, ESP_ERR_INVALID_ARG, TAG, "cfg is null");
ESP_RETURN_ON_FALSE(cfg->period_ms > 0, ESP_ERR_INVALID_ARG, TAG, "period_ms=0");
ESP_RETURN_ON_FALSE(GPIO_IS_VALID_OUTPUT_GPIO(cfg->gpio_num), ESP_ERR_INVALID_ARG, TAG,
"invalid output gpio=%d", cfg->gpio_num);
gpio_config_t io = {
.pin_bit_mask = 1ULL << cfg->gpio_num,
.mode = GPIO_MODE_OUTPUT,
.pull_up_en = GPIO_PULLUP_DISABLE,
.pull_down_en = GPIO_PULLDOWN_DISABLE,
.intr_type = GPIO_INTR_DISABLE,
};
ESP_RETURN_ON_ERROR(gpio_config(&io), TAG, "gpio_config failed");
s_cfg = *cfg;
s_initialized = true;
return ESP_OK;
}
esp_err_t esp32_component_template_start(void)
{
ESP_RETURN_ON_FALSE(s_initialized, ESP_ERR_INVALID_STATE, TAG, "not initialized");
ESP_RETURN_ON_FALSE(s_task == NULL, ESP_ERR_INVALID_STATE, TAG, "already started");
BaseType_t ok = xTaskCreate(component_task, "comp_tmpl", 3072, NULL, tskIDLE_PRIORITY + 1, &s_task);
ESP_RETURN_ON_FALSE(ok == pdPASS, ESP_ERR_NO_MEM, TAG, "xTaskCreate failed");
ESP_LOGI(TAG, "started gpio=%d period_ms=%" PRIu32, s_cfg.gpio_num, s_cfg.period_ms);
return ESP_OK;
}
esp_err_t esp32_component_template_stop(void)
{
ESP_RETURN_ON_FALSE(s_task != NULL, ESP_ERR_INVALID_STATE, TAG, "not started");
vTaskDelete(s_task);
s_task = NULL;
ESP_LOGI(TAG, "stopped");
return ESP_OK;
}
#pragma once
#include <stdbool.h>
#include <stdint.h>
#include "esp_err.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
int gpio_num;
uint32_t period_ms;
bool active_high;
} esp32_component_template_config_t;
esp_err_t esp32_component_template_init(const esp32_component_template_config_t *cfg);
esp_err_t esp32_component_template_start(void);
esp_err_t esp32_component_template_stop(void);
#ifdef __cplusplus
}
#endif
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x8000,
otadata, data, ota, 0x11000, 0x2000,
phy_init, data, phy, 0x13000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x380000,
ota_1, app, ota_1, 0x3A0000, 0x380000,
storage, data, spiffs, 0x720000, 0x8E0000,
# ESP-IDF Environment (auto-load)
# source ~/.esp_idf_env # Uncomment to auto-load on terminal start
alias idf="source \"$HOME/.esp_idf_env\""
export PATH="$PATH:$HOME/go/bin"
export PATH="$HOME/.local/bin:$PATH"
ESP32 Communication Protocols (ESP-IDF)
Use this reference for ESP32 peripheral communication patterns in ESP-IDF: I2C, SPI, UART, and TWAI (CAN).
Scope and Version Notes
- ESP-IDF has both legacy and newer driver APIs in some subsystems (notably I2C/ADC across versions).
- Prefer the project's existing API style unless you are explicitly migrating.
- Always confirm target chip and pin map before coding (ESP32 vs ESP32-S3/C3/C6 feature differences, pin capabilities, strapping pins).
I2C (Master) Patterns
- Confirm external pull-ups, bus voltage compatibility, and bus speed before debugging software.
- Always use transaction timeouts; never wait forever on a busy bus.
- Handle bus recovery/reset on repeated timeout conditions (device lockups are common).
- Serialize access with a mutex or a dedicated bus-owner task.
- Log address, register, timeout, and error code for field diagnostics.
Common ESP-IDF patterns:
- Legacy API:
i2c_param_config(),i2c_driver_install(), command links. - Newer API (IDF v5+): bus/device handles with explicit device configuration and transfer timeout.
SPI (Master) Patterns
- Use
spi_bus_initialize()andspi_bus_add_device()and keep device config (mode, clock, CS, queue depth) explicit. - For DMA transfers, allocate buffers with DMA-capable memory (
MALLOC_CAP_DMA) when required. - Be explicit about transaction ownership if multiple tasks share the bus.
- Validate max clock against wiring length, signal integrity, and device timing, not just the data sheet headline.
- Prefer queued transactions for throughput; prefer synchronous transmit for simple control paths.
Review cues:
- Are TX/RX buffers valid for the duration of the transaction?
- Is CS behavior correct for multi-part register operations?
- Are DMA-capable buffers used where needed?
UART Patterns
- Prefer ESP-IDF UART driver (
uart_driver_install) with the driver ring buffer/event queue before writing a custom ISR buffer. - Use a dedicated parser task for framed protocols.
- Bound parser work and handle malformed frames/noise.
- If using an ISR callback path, keep it minimal and IRAM-safe as needed.
Typical architecture:
- UART driver ISR/ring buffer -> parser task -> application queue/state machine
TWAI (CAN) Patterns
- On ESP32-class chips with TWAI support, use the TWAI driver (
twai_driver_install, start/stop/transmit/receive, alerts). - Validate transceiver wiring and termination first; software often gets blamed for bus electrical issues.
- Use alerts and error counters to distinguish bus-off/warning states from application bugs.
- Implement recovery logic for bus-off instead of retrying transmit forever.
RMT / Special Protocol Note
- For timing-sensitive one-wire / IR / pulse protocols, prefer RMT over bit-banging in tasks.
- RMT often reduces jitter and CPU load compared with software timing loops.
Shared Communication Bus Design
- Prefer a bus manager task when:
- Multiple tasks issue transactions
- Ordering matters (sensor init + reads + calibration writes)
- Retries/recovery must be centralized
- Use a mutex only when transactions are short and ownership is simple.
Error Handling and Recovery (Merged and ESP32-Adapted)
- Always use timeouts to prevent deadlocks/stalls.
- Propagate and log
esp_err_t(plus protocol-level status if available). - Implement retry with backoff for transient faults; avoid tight retry loops.
- Distinguish hardware faults (wiring/pull-up/power) from protocol framing/software faults.
- Validate received payloads (CRC/checksum/length/state machine transitions).
Hardware and Pin Constraints (ESP32-Specific)
- Check GPIO matrix routing limits and peripheral pin capability for the selected chip.
- Watch strapping pins and boot mode interactions.
- Confirm voltage levels (3.3V logic, open-drain pull-ups for I2C, transceiver requirements for TWAI/RS-485).
- Confirm bus speed/timing against cable length and pull-up strength.
Review Checklist
- Timeouts present on all protocol operations.
- Shared bus access serialized correctly (mutex or owner task).
- ISR-safe APIs used in ISR paths only.
- DMA-capable buffers used when needed.
- Protocol parsing validates length/state/CRC.
- Logs include enough context (bus, device addr/id, op, error, timeout).
- Recovery path exists for bus lockups / device reset / bus-off.
ESP-IDF / Plugin Compatibility Evidence (ESP-ADF, ESP-SR, etc.)
Use this reference before build/debug/flash when external ESP frameworks/plugins are in use. Compatibility must be proven with exact-version evidence.
Core Rule
- Do not continue until you have concrete evidence that every plugin/framework is compatible with the exact ESP-IDF version and with each other (when they are used together).
- "Concrete evidence" means exact versions + a verifiable source (matrix, manifest, release note, pinned compatibility file, or tested upstream bundle).
Why This Matters
- ESP-IDF, ESP-ADF, and ESP-SR often have version constraints that are not interchangeable.
- Individual compatibility with ESP-IDF is not enough when multiple frameworks are combined.
- A stack can pass one check (
ESP-SRsaysidf >= 5.0) and still fail in practice due to anESP-ADFmatrix gap or cross-stack mismatch.
Evidence Sources (Preferred Order)
1. Project-pinned compatibility lock (best for repeated builds)
- A checked-in file that explicitly pins:
- ESP-IDF version/tag
- plugin/framework versions/tags/commits (ESP-ADF, ESP-SR, etc.)
- source of compatibility proof (URL/release/matrix)
- date/notes (optional)
Use assets/templates/compatibility/ as a starting point.
2. Official compatibility matrices / release notes
- ESP-ADF README compatibility matrix (row for ADF release + column for exact IDF release)
- ESP-ADF release notes (exact supported IDF versions)
- ESP-SKAINET release/bundle docs (if using ESP-SR + audio stack combinations)
3. Component manifests / dependency constraints
idf_component.ymldependencies.idfversion range (for example ESP-SR)- Managed component lockfiles and version pins
Note:
- Manifest ranges are useful evidence for plugin -> IDF compatibility.
- They are usually not enough to prove plugin A <-> plugin B compatibility.
4. Local, reproducible build/test evidence
- Successful clean build with exact versions
- Smoke test or sample example build
- Preferably with the compatibility evidence recorded afterward in a lock file
Build success alone is helpful but should not replace upstream version evidence when known compatibility matrices exist.
Required Checks for Common Stacks
ESP-IDF + ESP-ADF
- Record exact ESP-IDF version (major.minor.patch or tag)
- Record exact ESP-ADF version/tag
- Verify the ESP-ADF README/release matrix explicitly lists the selected ESP-IDF version
- Verify the selected ADF row marks it as supported
If the exact IDF release is not listed, compatibility is unproven (do not assume forward compatibility).
ESP-IDF + ESP-SR
- Record exact ESP-IDF version
- Record exact ESP-SR version/tag
- Verify
esp-sr/idf_component.ymldependencies.idfrange includes the selected ESP-IDF version - Check additional target constraints (chip support, PSRAM recommendations, etc.) from ESP-SR docs
ESP-IDF + ESP-ADF + ESP-SR (Cross-Stack)
- Pass all individual checks above
- Also require explicit cross-stack evidence:
- project compatibility lock file, or
- ESP-SKAINET release/bundle documentation, or
- user-provided tested matrix with exact versions
Do not infer cross-stack compatibility from two independent checks.
Agent Workflow
1. Enumerate plugins/frameworks in use and exact versions. 2. Gather evidence from local manifests/readmes/release docs. 3. Write an evidence report (and optionally a lock file) before build. 4. If any edge is missing/ambiguous, stop and ask for version changes or approved evidence.
Reference helper:
scripts/check_plugin_compatibility.pyvalidates common ESP-IDF/ESP-ADF/ESP-SR evidence and writesbuild/plugin-compatibility-evidence.txt.- Set
ESP_REQUIRED_PLUGINS=esp-adf,esp-srto force checks when auto-detection is uncertain. - Set
ESP_STACK_COMPAT_EVIDENCE=...or add a project compatibility lock file to satisfy cross-stack proof requirements.
Example: What "Unproven" Looks Like
- ESP-ADF matrix supports up to IDF
v5.3, but project is on IDFv5.5. - ESP-SR manifest says
idf >= 5.0and passes. - Result: stack is still unproven because ADF -> IDF evidence is missing for
v5.5.
Review Checklist
- Every framework in use has an exact version/tag/commit recorded.
- Each framework has explicit compatibility evidence against the exact ESP-IDF version.
- Cross-stack evidence exists when multiple frameworks interact.
- Evidence is written to a report or lock file before build.
- No “probably compatible” assumptions remain.
ESP32 Device Terminal / Service Console (ESP-IDF)
Use this reference when a USB/serial console path is available and not reserved by product functionality. Default behavior: proactively add a basic on-device terminal for serviceability, observability, and tuning.
When to Add It (Default Policy)
- Add a terminal by default if:
- a USB/serial transport is available (
USB CDC,USB-Serial-JTAG, or board USB-UART) - it is not already dedicated to another product feature/protocol
- project/security requirements do not forbid an interactive console
- Do not wait for the user to ask explicitly if the above conditions are met.
Transport Selection (ESP32 Variant Aware)
ESP32-S2/S3: native USB device options (often USB CDC) may be available depending board design and stack usage.ESP32-C3/C6/S3:USB-Serial-JTAGmay be available and convenient for service console / monitor workflows.ESP32(classic): often uses external USB-UART bridge to UART console.
Before implementation, confirm:
- which transport is physically wired to the host
- whether the transport is already used for another runtime protocol
- whether JTAG/debug access must be preserved
Preferred Implementation Approach
- Prefer ESP-IDF console primitives (
esp_console) and REPL helpers over custom command parsers. - Prefer built-in line editing/history/completion support (linenoise-backed REPL in ESP-IDF).
- Register commands in a small command registry rather than writing a monolithic
if/elseparser. - Keep command handlers fast and deterministic; defer long operations to tasks if needed.
- Reuse
assets/templates/esp-console/as the starting point for command registration.
Why:
- better UX (history, completion, help)
- consistent command parsing
- easier extension and review
UX Requirements (User Friendly)
- Autocomplete for command names (and key subcommands where practical)
helpcommand with short descriptions- clear error messages and usage hints
- stable command naming (
settings,status,tasks,heap,log,reboot) - consistent output format (human-readable first, optionally script-friendly)
Minimum Useful Command Set (Recommended)
help: list commands and usagestatus: uptime, firmware version, target, reset reason, connectivity statesettings get <key>/settings set <key> <value>: application-space settings (with validation)settings save/settings load(if settings are not auto-persisted)tasks: RTOS task list / states / stack high-water marks (build-config dependent)heap: free/min/largest block (capability-specific variants if useful)log level <tag|*> <level>: runtime log tuning for noisy vs app componentsreboot: controlled restart (with confirmation option for production tools)
Optional high-value commands:
wifi status/wifi reconnecti2c scan(careful: only when safe on deployed hardware)display test(format-validated patterns)nvs dump/nvs get(avoid exposing secrets)
RTOS Debug Info (Expose Carefully)
- Provide lightweight snapshots, not long blocking reports.
- Common useful outputs:
- task name / state / priority
- stack high-water mark
- CPU usage/runtime stats (if configured)
- Some advanced RTOS stats require
sdkconfigoptions (trace/runtime stats support). Confirm and enable intentionally. - Avoid commands that destabilize timing in production.
Settings Interface Design
- Validate values before applying.
- Separate
setfromsavewhen persistence side effects matter. - Emit exact validation errors (
out of range,unsupported enum,requires reboot). - Log settings changes with source (
terminal) and timestamp/uptime if available. - Do not expose secrets in plain text by default.
Logging Integration
- Terminal should complement logs, not replace them.
- Add runtime log-level commands to tune noisy components down and app modules up.
- Keep terminal output concise to avoid interfering with monitor readability.
Security / Production Constraints
- If the product has security requirements, gate sensitive commands behind:
- compile-time feature flags
- build profile (dev vs prod)
- authentication / challenge-response (if required)
- Disable or reduce destructive commands (
erase, unrestricted memory poke) unless explicitly required.
Review Checklist
- Console transport ownership is confirmed (USB/CDC/JTAG/UART not conflicting).
esp_console/REPL used instead of ad hoc parser (unless justified).- Autocomplete/help/history are enabled and usable.
- Command handlers validate inputs and report actionable errors.
- RTOS/heap diagnostics are bounded and safe.
- Settings commands protect secrets and persistence behavior is explicit.
- Security/build-profile gating is applied where needed.
ESP32 Display and Graphics Validation (ESP-IDF)
Use this reference for display bring-up, framebuffer formats, flush paths, and graphics correctness on ESP32 projects.
First Principle: Validate the Display Data Path
Before writing or changing graphics code, confirm:
- Display controller model (for example ST7789, ILI9341, GC9A01, etc.)
- Interface type (SPI, i80/parallel, RGB, MIPI-DSI on supported targets)
- Resolution and orientation
- Pixel format expected by the controller/path (RGB565, BGR565, RGB888, etc.)
- Byte order / endianness / color order
- Window/flush command protocol and region alignment constraints
- DMA/buffer requirements (alignment, internal RAM vs PSRAM support)
If any of these are unknown, stop and ask before changing graphics code.
Common Failure Modes (Usually Not "Rendering Logic" Bugs)
- Colors swapped (RGB/BGR mismatch)
- Blue/red swapped or tint issues (byte order / endian mismatch)
- Corrupted lines/tearing (buffer stride, DMA alignment, race in flush ownership)
- Partial updates in wrong region (window coordinates or rotation transform mismatch)
- Random corruption under load (buffer lifetime issue, PSRAM/DMA mismatch, cache/coherency assumptions)
Buffer and Format Rules
- Convert only to the exact format the display path expects.
- Keep a single documented source-of-truth format at the display boundary.
- Validate stride/line pitch assumptions explicitly.
- Do not assume a library's default color order matches your panel/controller config.
Performance Considerations
- Match bus clock and DMA usage to board wiring and panel stability limits.
- Prefer DMA-capable buffers for large transfers when supported/required.
- Validate whether the display driver path supports PSRAM-backed buffers on the chosen target and IDF version.
- Use partial updates/dirty rectangles when applicable and correct for the UI stack.
Review Checklist
- Controller/interface/pixel format explicitly identified.
- Color order and byte order are explicit in code/config.
- Flush buffer lifetime is valid through transaction completion.
- DMA/memory placement meets driver requirements.
- Rotation/window math matches panel configuration.
- Performance tuning changes are measured and remain visually correct.
ESP-IDF Checklists
Use these checklists to speed up implementation, review, and debugging work without skipping embedded-specific risks.
Blocking Context Checklist (Do Not Skip)
- Confirm exact target chip (
esp32,esp32s3,esp32c3, etc.). Stop if unknown. - Confirm board/revision and peripheral wiring (GPIO map, pull-ups, transceivers, display interface).
- Confirm electrical assumptions (voltage levels, power rails, level shifting, shared buses).
- Confirm ESP-IDF version and major driver API style used by the project.
- Confirm all plugin/framework versions in use (ESP-ADF, ESP-SR, etc.) and collect exact tags/commits.
- Confirm concrete compatibility evidence exists for every plugin/framework against the selected ESP-IDF version.
- If multiple frameworks interact (for example ESP-ADF + ESP-SR), confirm explicit cross-stack compatibility evidence (not only individual IDF compatibility).
- If behavior or API usage expectations are unclear, ask for example code (project snippet, vendor example, or minimal repro) before implementation/debugging.
- Confirm flash size and whether OTA is required before proposing partition changes.
- Confirm PSRAM presence/mode if memory placement or display buffers are involved.
- Confirm display/controller model and pixel format/endianness before graphics work.
- Confirm whether a USB/serial console path is available and free for a service terminal (and whether product/security policy allows it).
- If any above is unknown for hardware-facing changes, ask the user and do not continue implementation/debugging.
Implementation Checklist
- Confirm target chip (
esp32,esp32s3,esp32c3, etc.) and ESP-IDF version. - Confirm ESP-IDF toolchain is installed and usable before building (
idf.py --versionsucceeds or project wrapper preflight passes). - Confirm plugin/framework compatibility preflight passes and produces an evidence report before building.
- Confirm board-level pin mapping and electrical constraints before assigning GPIOs.
- Confirm task model: task priorities, stack sizes, queue depths, timer cadence, core affinity (if used).
- Define ownership of shared state and synchronization primitives.
- Keep ISR work minimal and use ISR-safe APIs only.
- Check
esp_err_treturn values and handle failures explicitly. - Initialize subsystems in a deterministic order (NVS, netif/event loop, Wi-Fi/BLE, drivers, app tasks).
- Add logs for state transitions, retries, and failure reasons.
- Avoid heap churn in hot paths when a static buffer or reuse pattern is sufficient.
- If USB/serial transport is free and allowed, add a basic user-friendly service terminal (
esp_console/REPL) by default with help/autocomplete and core diagnostics commands. - Update
sdkconfig/sdkconfig.defaultsintentionally and reproducibly; prefer file edits over ad hocmenuconfigwalkthroughs. - Update partition table (
partitions.csv) to match flash size and feature needs; avoid unexplained unused flash. - If OTA is required, verify OTA-compatible partitions and adequate slot sizing.
- For display paths, validate controller pixel format, color order, and buffer layout before coding conversions.
- Document non-obvious timing, hardware, or protocol assumptions.
Code Review Checklist
- Check task/ISR context correctness for each FreeRTOS and ESP-IDF API call.
- Check blocking calls inside high-priority tasks and callbacks.
- Check timeout values for infinite/blocking behavior that can deadlock progress.
- Check memory ownership and lifetime of buffers passed across tasks/callbacks.
- Check event handler registration/unregistration and duplicate registration risks.
- Check error propagation and cleanup on partial init failure.
- Check watchdog exposure (long critical sections, busy loops, disabled yields).
- Check pin/peripheral conflicts and hidden assumptions in
sdkconfig. - Check partition table and
sdkconfigalignment with flash size, OTA requirement, and enabled features. - Check plugin/framework versions are pinned/documented and match known-good compatibility evidence.
- Check logging configuration: suppress noisy library logs where needed while keeping application logs sufficiently verbose.
- Check whether a free USB/serial path should have a service terminal and whether one was omitted without reason.
- Check terminal UX (help/autocomplete/clear errors) and command safety if a console is present.
- Check graphics/display code for explicit format assumptions (RGB565/BGR565/RGB888/etc., byte order, stride).
- Check bus/peripheral clock, DMA, and memory placement choices against performance requirements.
- Check log quality for field diagnosis (tag, event, error code, state).
Debugging Checklist
- Reproduce with exact firmware revision,
sdkconfig, target, and hardware setup. - Capture full serial log from boot to failure.
- Classify failure stage: build, flash, boot, init, runtime, sleep/wake, network, peripheral I/O.
- Identify first bad symptom and the event immediately before it.
- Add scoped instrumentation (counters, timestamps, state logs) before refactoring.
- Reduce variables: disable unrelated features, mock inputs, or isolate one subsystem.
- Validate power, reset, and wiring assumptions for hardware-facing bugs.
- Ask for a minimal reproducible example or known-good reference code when symptoms are ambiguous and evidence is insufficient.
- If display corruption/color issues exist, verify pixel format/endianness/controller init sequence before changing app graphics logic.
- If build output is noisy, tune component log levels to surface signal while keeping app logs high value.
- If a service terminal exists, use runtime commands for heap/tasks/log-level introspection before invasive code changes.
- Re-test after each change; avoid batching unrelated fixes.
Build / Validation Checklist
- Prefer project
build.shwrapper if present; otherwise useidf.py build. - Before running the build, verify ESP-IDF environment setup is valid (
idf.pyruns, not just exists). - Before running the build, verify plugin/framework compatibility evidence (matrix/manifest/release-note proof) is concrete and current for the exact versions in use.
- If the developer shell UX is poor, add/update a shell helper snippet (for example
.zshrc) foridfenv sourcing and PATH setup. - Run the build after code/config/partition changes before declaring completion.
- If build fails, fix and rerun until it passes.
- Review warnings; resolve correctness/safety warnings rather than ignoring them.
- If warnings remain, call them out explicitly with rationale and impact.
Wi-Fi / BLE Focus Checks
- Confirm init order (
nvs_flash_init, event loop/netif setup, stack init, handlers, start/connect). - Confirm reconnect strategy and retry/backoff behavior.
- Confirm credentials and persistent config state (NVS).
- Check event handling coverage for disconnect/error events.
- Check coexistence assumptions when Wi-Fi and BLE run together.
Peripheral Focus Checks
- Confirm voltage levels, pull-ups, and shared bus wiring.
- Confirm pin mux and any strapping pin restrictions.
- Confirm bus speed/timing and device-specific protocol delays.
- Confirm transaction timeouts and recovery from bus lockups.
- Confirm ISR affinity and DMA constraints if relevant.
- Confirm flash/PSRAM speed/mode assumptions and select the most performant reliable configuration supported by the hardware/project.
ESP32 Logging and Observability (ESP-IDF)
Use this reference when designing logs, filtering noise, or diagnosing issues from idf.py monitor.
Logging Policy
- Keep application-space logs verbose and high signal during development/debugging.
- Reduce irrelevant library/default component logs when they hide the application's state transitions.
- Prefer targeted filtering/tuning over global suppression.
Practical ESP-IDF Logging Guidance
- Use stable module tags (
wifi_mgr,sensor_task,display_drv,ota_updater). - Log:
- state transitions
- error codes (
esp_err_t) - retry counts/backoff
- timing/latency (when relevant)
- key configuration decisions at startup
- Avoid repeated unstructured info logs in tight loops.
- If an on-device terminal is present, expose runtime log-level controls (by tag / wildcard) so signal can be tuned without reflashing.
Noise Reduction Strategy
- Lower noisy component log verbosity selectively (build-time config or runtime log-level control where used).
- Keep app modules at
DEBUG/VERBOSEwhile reducing third-party/default chatter if needed. - Preserve enough system logs to diagnose reset/panic/network events.
What Good Logs Look Like
- Event-first and stateful:
wifi_mgr: disconnected reason=... retry=3 backoff_ms=2000display_drv: flush region x=0 y=0 w=240 h=40 fmt=rgb565- Include identifiers for peripherals/devices/buses when multiple instances exist.
- Include durations for timeouts and retries.
Review Checklist
- Application logs are verbose enough to debug behavior.
- Library/default noise is reduced when it obscures signal.
- Terminal log-level commands (if present) are scoped and safe.
- Error logs include code + context, not only generic failure text.
- Startup logs capture key target/config assumptions.
- Logs do not create excessive timing disruption in hot paths.
LVGL + ESP-IDF Reference
Version Compatibility Matrix
| LVGL | Minimum ESP-IDF | Notes |
|---|---|---|
| v8.3.x | 4.4+ | Stable, widely used; uses lv_disp_drv_t / lv_indev_drv_t API |
| v9.0.x | 5.0+ | Breaking API change from v8 — lv_display_t, new flush callback signature |
| v9.1+ | 5.1+ | Recommended for new projects on IDF 5.x |
Always confirm the exact LVGL version in `idf_component.yml` or `CMakeLists.txt` before writing any driver or integration code. The v8→v9 API change is not backwards compatible.
Obtain LVGL via the IDF Component Manager:
# idf_component.yml
dependencies:
lvgl/lvgl: "^9.1.0"
# or for v8:
# lvgl/lvgl: "^8.3.0"Or via managed components:
idf.py add-dependency "lvgl/lvgl^9.1.0"---
Display Flush Callback (v9.x)
#include "lvgl.h"
#include "driver/spi_master.h"
static spi_device_handle_t spi;
// Called by LVGL when a render area is ready to be sent to the display.
// Must call lv_display_flush_ready() when transfer is complete.
static void disp_flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map)
{
int32_t w = lv_area_get_width(area);
int32_t h = lv_area_get_height(area);
// Set the display window (controller-specific — example: ILI9341/ST7789 sequence)
lcd_set_window(area->x1, area->y1, area->x2, area->y2);
// DMA SPI transfer — px_map must be in DMA-capable memory
spi_transaction_t t = {
.length = w * h * 2 * 8, // bits; 2 bytes per pixel for RGB565
.tx_buffer = px_map,
.flags = 0,
};
spi_device_queue_trans(spi, &t, portMAX_DELAY);
// Signal LVGL that flush is done.
// If using DMA with a callback, call this from the DMA completion ISR instead:
lv_display_flush_ready(disp);
}v8.x Equivalent (different function signature):
static void disp_flush_v8(lv_disp_drv_t *drv, const lv_area_t *area, lv_color_t *color_p)
{
// ... transfer logic ...
lv_disp_flush_ready(drv); // note: lv_disp_flush_ready, not lv_display_flush_ready
}---
Display Initialization
v9.x
#include "lvgl.h"
#define DISP_WIDTH 320
#define DISP_HEIGHT 240
#define BUF_LINES 20 // number of lines in each draw buffer
static lv_display_t *disp;
static lv_color_t buf1[DISP_WIDTH * BUF_LINES];
static lv_color_t buf2[DISP_WIDTH * BUF_LINES]; // optional second buffer for double-buffering
void lvgl_display_init(void)
{
lv_init();
disp = lv_display_create(DISP_WIDTH, DISP_HEIGHT);
lv_display_set_flush_cb(disp, disp_flush);
// Single buffer:
lv_display_set_buffers(disp, buf1, NULL, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);
// Double buffer (smoother rendering, uses 2x RAM):
// lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);
// Full-screen buffer in PSRAM (ESP32-S3 with PSRAM):
// lv_color_t *fb = heap_caps_malloc(DISP_WIDTH * DISP_HEIGHT * sizeof(lv_color_t),
// MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
// lv_display_set_buffers(disp, fb, NULL, DISP_WIDTH * DISP_HEIGHT * sizeof(lv_color_t),
// LV_DISPLAY_RENDER_MODE_FULL);
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
}---
Tick Source (Required)
LVGL requires a millisecond tick to animate, time events, and drive transitions.
Option A: FreeRTOS Timer (Preferred)
static void lvgl_tick_timer_cb(TimerHandle_t xTimer)
{
lv_tick_inc(portTICK_PERIOD_MS); // usually 1ms if configTICK_RATE_HZ=1000
}
void lvgl_tick_init(void)
{
TimerHandle_t timer = xTimerCreate("lvgl_tick", pdMS_TO_TICKS(1),
pdTRUE, NULL, lvgl_tick_timer_cb);
xTimerStart(timer, 0);
}Option B: esp_timer (Higher Resolution)
static void lvgl_tick_cb(void *arg)
{
lv_tick_inc(1); // called every 1ms
}
void lvgl_tick_init(void)
{
const esp_timer_create_args_t args = {
.callback = lvgl_tick_cb,
.name = "lvgl_tick",
};
esp_timer_handle_t timer;
ESP_ERROR_CHECK(esp_timer_create(&args, &timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(timer, 1000)); // 1000µs = 1ms
}---
LVGL Task and Mutex (Thread Safety)
LVGL is not thread-safe. All lv_ calls — including UI construction, style updates, and animations — must happen from the same task that calls lv_timer_handler(), or be protected by a mutex.
Dedicated LVGL Task Pattern
static SemaphoreHandle_t lvgl_mutex;
void lvgl_lock(void) { xSemaphoreTakeRecursive(lvgl_mutex, portMAX_DELAY); }
void lvgl_unlock(void) { xSemaphoreGiveRecursive(lvgl_mutex); }
static void lvgl_task(void *arg)
{
lvgl_tick_init();
lvgl_display_init();
ui_init(); // create screens, widgets, etc.
while (true) {
lvgl_lock();
uint32_t time_to_next = lv_timer_handler();
lvgl_unlock();
vTaskDelay(pdMS_TO_TICKS(time_to_next > 5 ? 5 : time_to_next));
}
}
void app_main(void)
{
lvgl_mutex = xSemaphoreCreateRecursiveMutex();
xTaskCreatePinnedToCore(lvgl_task, "lvgl", 8192, NULL, 5, NULL,
1); // pin to core 1; leave core 0 for Wi-Fi/BLE
}
// Updating UI from another task:
void update_label_from_task(lv_obj_t *label, const char *text)
{
lvgl_lock();
lv_label_set_text(label, text);
lvgl_unlock();
}---
Color Format and Byte Order
This is the most common source of wrong colors and washed-out display output.
Identify Your Controller's Expected Format
| Controller | Typical Format | Byte Order |
|---|---|---|
| ILI9341 | RGB565 | Big-endian (MSB first) |
| ST7789 | RGB565 | Big-endian |
| SH8601 | RGB888 or ARGB8888 | Depends on init |
| GC9A01 | RGB565 | Big-endian |
| RA8875 | RGB565 | Big-endian |
Configuring LVGL Color Format
// v9.x — set on the display object:
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
// or
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB888);Byte Swap for SPI Controllers
Most ESP32 SPI controllers transmit LSB-first by default; most display controllers expect big-endian RGB565. Fix with:
// v9.x:
lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565);
// Enable byte swap in the display object (swaps bytes of each 16-bit pixel before flush):
// lv_display_set_byte_swap(disp, true); // available in v9.1+
// Or: swap in hardware via SPI controller flag:
// .flags = SPI_DEVICE_HALFDUPLEX | SPI_DEVICE_NO_DUMMY -- check your IDF versionIf colors are inverted (blue appears red), the byte order is wrong. If colors look correct but washed-out/dark, alpha channel or bit depth is wrong.
---
Memory Configuration
sdkconfig Options
# Increase task stack for LVGL rendering (default is often too small):
# The lvgl task itself: 8192–16384 bytes depending on widget complexity.
# Enable PSRAM for large frame buffers (ESP32-S3):
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_OCT=y # ESP32-S3 Octal PSRAM
CONFIG_SPIRAM_SPEED_80M=y
# Allow malloc from PSRAM:
CONFIG_SPIRAM_USE_MALLOC=y
CONFIG_SPIRAM_MALLOC_ALWAYSINTERNAL=16384 # keep small allocs in IRAMAllocating Draw Buffers
// Internal SRAM (fast, limited — use for small buffers or when PSRAM unavailable):
static lv_color_t buf[LCD_WIDTH * 20]; // 20 lines
// PSRAM (ESP32-S3 — for large/full-frame buffers):
lv_color_t *buf = heap_caps_aligned_alloc(64,
LCD_WIDTH * LCD_HEIGHT * sizeof(lv_color_t),
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT);
assert(buf != NULL);
// DMA-capable (required for SPI DMA transfers — must NOT be in PSRAM on some targets):
lv_color_t *dma_buf = heap_caps_aligned_alloc(64,
LCD_WIDTH * 20 * sizeof(lv_color_t),
MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL);On ESP32 (original): DMA-capable memory is IRAM/DRAM; PSRAM is not DMA-capable for SPI. On ESP32-S3: PSRAM can be used for SPI DMA with EDMA (check driver docs and MALLOC_CAP_DMA).
---
Performance Tuning
Double Buffering
Use two render buffers so LVGL can prepare the next frame while the DMA is transmitting the current one:
lv_display_set_buffers(disp, buf1, buf2, sizeof(buf1), LV_DISPLAY_RENDER_MODE_PARTIAL);In the flush callback, start DMA and return immediately. Call lv_display_flush_ready() from the DMA completion callback — this allows LVGL to start rendering the next frame concurrently.
Avoid Blocking in Flush Callback
// Bad: blocks until transfer completes
spi_device_transmit(spi, &t);
lv_display_flush_ready(disp);
// Better: queue DMA, signal completion from ISR or polling callback
spi_device_queue_trans(spi, &t, portMAX_DELAY);
// lv_display_flush_ready() called from DMA complete CBSPI Frequency
- ILI9341/ST7789: typically 40–80MHz depending on board trace quality
- Start at 20MHz, increase until artifacts appear, then back off 10%
- Set via
spi_device_interface_config_t.clock_speed_hz
Dirty Region Rendering
LVGL only redraws changed regions. Avoid calling lv_obj_invalidate() on entire screens unnecessarily. Prefer updating individual labels, arcs, or images.
---
Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Screen all white/black after init | Flush callback never called or controller not initialized | Verify lv_timer_handler() is called; check display init sequence |
| Colors wrong (blue ↔ red) | RGB byte order mismatch | Enable byte swap or swap R/B in flush callback |
| Colors washed out / dark | Wrong color depth (e.g. 24-bit data to 16-bit controller) | Match lv_display_set_color_format() to controller |
| Crash in flush callback | Draw buffer not in DMA-capable memory | Use `MALLOC_CAP_DMA\ |
| Flickering / tearing | Single buffer, no vsync | Use double buffer; add DMA completion signaling |
| UI locks up after a few updates | lv_timer_handler() blocked or mutex deadlock | Ensure LVGL task runs without blocking; check mutex acquire/release pairing |
lv_tick_inc not called | No tick source configured | Add FreeRTOS timer or esp_timer calling lv_tick_inc(1) every 1ms |
| Animations stutter | lv_timer_handler() called too infrequently | Cap sleep to 5ms; don't vTaskDelay(time_to_next) with large values |
| Stack overflow in LVGL task | Complex widgets exceed task stack | Increase task stack to 16384+ bytes for complex UIs |
---
LVGL + ESP-IDF Component Manager Lock File
After resolving a working combination, record it in the project compatibility lock file:
# esp-framework-compat.lock
esp-idf: "v5.2.1"
lvgl: "v9.1.0" # from idf_component.yml / managed_components
display-controller: "ST7789"
notes: "RGB565, big-endian, 40MHz SPI, double-buffer DMA on ESP32-S3 with Octal PSRAM"
verified: "2025-01-15"ESP32 Memory and Size Optimization (ESP-IDF)
Use this reference for RAM/flash/code-size optimization and memory-safety decisions in ESP-IDF projects.
ESP32 Memory Model (Practical View)
- Internal RAM is limited and shared with stacks, drivers, and protocol stacks.
- Some features (Wi-Fi/BLE, networking, TLS) increase internal RAM pressure significantly.
- PSRAM may be available on some modules/targets, but not all memory is equal:
- Latency differs from internal RAM
- DMA compatibility is constrained
- Some ISR/critical paths should stay in internal memory
- Use ESP-IDF heap capabilities APIs when memory class matters (
heap_caps_*). - Performance depends on memory placement and bus mode, not only free bytes; internal RAM, PSRAM, and flash-backed code/data have different latency/throughput behavior.
Allocation Policy (What to Prefer)
- Prefer static allocation for long-lived buffers and core control structures.
- Reuse work buffers in non-overlapping paths.
- Avoid heap allocation in hot paths and callback-heavy paths.
- Never allocate from ISR context.
- Use fixed-size pools when bounded dynamic behavior is needed.
DMA / Capability-Aware Allocation
- Allocate DMA buffers with capability flags (for example
MALLOC_CAP_DMA) when required by SPI/I2S/peripheral drivers. - Verify buffer lifetime across async transactions (queued SPI/UART/etc.).
- Do not assume PSRAM buffers are valid for every DMA path.
Stack Sizing and Monitoring
- Start with conservative task stacks for parsing/logging/network code, then measure and tighten.
- Monitor stack high-water marks (
uxTaskGetStackHighWaterMark()/uxTaskGetStackHighWaterMark2()depending config/API availability). - Watch for hidden stack growth from:
- Large local arrays
- Deep call chains
- Logging and format strings
- JSON/TLS/protocol parsers
Heap Monitoring and Fragmentation Awareness
- Track:
heap_caps_get_free_size(...)heap_caps_get_minimum_free_size(...)- Largest free block if fragmentation is suspected
- Measure before/after feature init and under steady-state runtime.
- Repeated alloc/free of variable-sized buffers is a common fragmentation source.
Code Size Optimization (ESP-IDF Workflow)
- Use
idf.py sizeandidf.py size-componentsto identify growth. - Inspect the linker map (
build/<app>.map) when component-level output is not enough. - Prefer
constdata for read-only tables and strings. - Keep component dependencies minimal; unused components can pull in surprising code.
- Review logging levels and format-heavy debug code in release builds.
- If code execution speed matters, check whether hot code/data placement and flash/PSRAM configuration (
sdkconfig) are limiting throughput.
Common levers (project-dependent):
sdkconfigoptimization level (CONFIG_COMPILER_OPTIMIZATION_*)- Link-time optimization (if enabled/supported in project/toolchain setup)
- Reducing enabled features/components/protocols
Data Structure and Buffer Patterns
- Use the smallest type that matches protocol range and alignment requirements.
- Pack structures only when layout compatibility is required (protocol/on-flash format); avoid unnecessary packed structs in hot code due to alignment penalties.
- Prefer ring buffers/stream buffers for byte streams.
- Use explicit ownership comments for buffers that cross task boundaries.
Flash / NVS / Partition Considerations
- Use NVS for small persistent config/state instead of ad hoc raw flash writes.
- For write-heavy application data, design a wear-aware strategy (NVS, filesystem, or custom log structure with rotation).
- Keep partition table and OTA slot size in mind when code size grows.
- Validate that large assets/tables belong in firmware at all; they may fit better in filesystem or external storage.
Compile-Time Guards
- Use
_Static_assert/static_assertfor: - protocol struct sizes
- array lengths
- queue payload sizes
- compile-time configuration assumptions
Review Checklist (Merged and ESP32-Adapted)
- Static/reused buffers preferred over ad hoc heap allocations.
- No heap allocation in ISR or time-critical paths.
- DMA buffers use capability-aware allocation when required.
- Task stacks sized from measurements; high-water marks checked.
- Heap minimum free and fragmentation indicators monitored in tests.
constused for read-only data.- Code-size growth checked with
idf.py size/ component breakdown. - Partition/OTA/NVS implications considered for flash usage changes.
- RAM/flash/PSRAM configuration and placement choices reviewed for performance-critical paths.
ESP32 Peripheral Programming (ESP-IDF)
Use this reference for ESP32 GPIO, interrupts, timers, ADC, PWM, watchdogs, and low-level programming decisions in ESP-IDF.
ESP32-Specific Default
- Prefer ESP-IDF drivers and HAL-style APIs first.
- Avoid direct register programming unless:
- the project already does it
- a required feature is unavailable in the driver
- there is a measured performance/timing reason
- If direct registers are used, isolate them behind a component API and document chip assumptions.
GPIO Configuration
- Use
gpio_config()with explicit mode, pull, and interrupt settings. - Validate pin capability on the selected target (input-only pins, analog-capable pins, strapping pins, RTC IO availability).
- Prefer named constants and board definitions over raw GPIO numbers scattered across code.
Basic pattern:
- board pin map header
- one init function per subsystem
- no hidden reconfiguration in unrelated modules
GPIO Interrupts
- Use
gpio_install_isr_service()andgpio_isr_handler_add()for GPIO ISR wiring. - Mark ISR handlers
IRAM_ATTRwhen required by the configured interrupt path. - Keep ISR handlers minimal: timestamp, latch state, notify task, return.
- Debounce in task context or timer context, not by blocking in ISR.
Timer Choices (ESP-IDF)
esp_timer: software callbacks, high-resolution scheduling.gptimer: hardware timer/capture/compare use cases.- FreeRTOS timers: non-precise app-level timing/retries.
Do not force one timer type for all problems. Pick based on precision, callback context, and CPU load.
PWM and Pulse Output
- Prefer LEDC for common PWM use cases (LED dimming, simple PWM outputs).
- Use MCPWM for motor-control-class needs where relevant and supported.
- Validate timer resolution/frequency tradeoffs explicitly.
ADC Patterns
- Prefer ESP-IDF ADC drivers (oneshot/continuous, version-dependent APIs) and calibration helpers when voltage accuracy matters.
- Be explicit about attenuation, sampling conditions, and calibration source.
- Avoid assuming lab-bench voltage readings match in-field under load/noise.
- Separate “raw sensor read” from “engineering units conversion” in code for easier testing.
UART / Serial Logging Integration
- Keep application protocol UART handling separate from console/log UART assumptions.
- If using
idf.py monitorfor logs, document baud and port assumptions in debug steps. - Avoid flooding logs in tight loops; it distorts timing and can mask race/watchdog issues.
Watchdogs (Practical)
- Use task watchdogs and system watchdogs intentionally; do not disable them to hide starvation issues.
- Feed watchdogs in the owner task/main loop path, not in random helper functions.
- When watchdog resets occur, inspect:
- blocking calls
- deadlocks
- ISR storms
- long critical sections
- log flooding / busy waits
Clocking and Timing (ESP32)
- Clock/frequency behavior is largely configured through ESP-IDF and
sdkconfig; avoid manual clock-tree style code from other MCUs. - For timing-sensitive code, measure actual intervals (
esp_timer_get_time(), timestamps, scope/logic analyzer) instead of assuming nominal frequency. - For throughput-sensitive peripherals (display/storage/streaming), review flash/PSRAM mode, bus clock, DMA usage, and memory placement together; the best option is hardware- and board-dependent.
Low-Power Programming Cross-Reference
- For sleep/wakeup and power strategy, read
references/power-optimization.md. - For communication bus timing and DMA concerns, read
references/communication-protocols.md.
Review Checklist (Merged and ESP32-Adapted)
- ESP-IDF drivers used unless a justified low-level exception exists.
- Pin capabilities and strapping constraints checked for target chip.
- GPIO ISR handlers are minimal, ISR-safe, and IRAM-safe where required.
- Correct timer subsystem selected (
esp_timer,gptimer, FreeRTOS timer, LEDC/MCPWM). - ADC attenuation/calibration assumptions documented.
- Watchdog handling preserves diagnostics instead of masking issues.
- Timing-sensitive behavior validated by measurement/logging, not assumptions.
OTA Workflow Reference
Partition Layout Requirements
Minimal 2-OTA Layout (4MB flash)
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xf000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
ota_0, app, ota_0, 0x20000, 0x180000,
ota_1, app, ota_1, 0x1a0000, 0x180000,Factory + 2-OTA (preferred for rollback)
nvs, data, nvs, 0x9000, 0x6000,
otadata, data, ota, 0xf000, 0x2000,
phy_init, data, phy, 0x11000, 0x1000,
factory, app, factory, 0x20000, 0x100000,
ota_0, app, ota_0, 0x120000, 0x180000,
ota_1, app, ota_1, 0x2a0000, 0x180000,Key Rules
otadatapartition is mandatory — without it the bootloader cannot track active OTA slot.ota_0andota_1must be the same size.- Size each OTA slot from the actual binary size reported by
idf.py sizewith margin (≥20%). - Never use the
factorysubtype for an OTA slot; reserve it for the recovery/golden image. - Set
CONFIG_PARTITION_TABLE_CUSTOM=yand pointCONFIG_PARTITION_TABLE_CUSTOM_FILENAMEat your CSV.
OTA Update API Flow
Basic In-App OTA Sequence
#include "esp_ota_ops.h"
#include "esp_partition.h"
#include "esp_app_format.h"
esp_err_t perform_ota(const uint8_t *data, size_t total_size)
{
const esp_partition_t *update_partition =
esp_ota_get_next_update_partition(NULL); // picks the inactive slot
if (!update_partition) {
ESP_LOGE(TAG, "No OTA partition found");
return ESP_ERR_NOT_FOUND;
}
esp_ota_handle_t handle;
esp_err_t err = esp_ota_begin(update_partition, OTA_WITH_SEQUENTIAL_WRITES, &handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_begin failed: %s", esp_err_to_name(err));
return err;
}
// Write data in chunks as received (e.g. from HTTP stream)
err = esp_ota_write(handle, data, total_size);
if (err != ESP_OK) {
esp_ota_abort(handle);
return err;
}
err = esp_ota_end(handle); // validates the image
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_end failed: %s (image may be corrupt)", esp_err_to_name(err));
return err;
}
err = esp_ota_set_boot_partition(update_partition);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed: %s", esp_err_to_name(err));
return err;
}
ESP_LOGI(TAG, "OTA complete. Rebooting into new firmware.");
esp_restart();
return ESP_OK; // unreachable
}HTTPS OTA (Recommended for Network Updates)
#include "esp_https_ota.h"
void ota_task(void *arg)
{
esp_http_client_config_t http_cfg = {
.url = CONFIG_OTA_FIRMWARE_URL,
.cert_pem = server_cert_pem_start, // embed via component CMakeLists EMBED_TXTFILES
.timeout_ms = 5000,
.keep_alive_enable = true,
};
esp_https_ota_config_t ota_cfg = {
.http_config = &http_cfg,
.http_client_init_cb = NULL,
.bulk_flash_erase = false, // set true only for very large images
.partial_http_download = false,
};
esp_err_t err = esp_https_ota(&ota_cfg);
if (err == ESP_OK) {
esp_restart();
} else {
ESP_LOGE(TAG, "HTTPS OTA failed: %s", esp_err_to_name(err));
}
vTaskDelete(NULL);
}Streaming HTTPS OTA (Chunk-by-Chunk, for Progress Reporting)
esp_https_ota_handle_t ota_handle;
esp_err_t err = esp_https_ota_begin(&ota_cfg, &ota_handle);
int image_len = esp_https_ota_get_image_size(ota_handle);
while (true) {
err = esp_https_ota_perform(ota_handle);
if (err != ESP_ERR_HTTPS_OTA_IN_PROGRESS) break;
int written = esp_https_ota_get_image_len_read(ota_handle);
ESP_LOGI(TAG, "OTA progress: %d / %d bytes", written, image_len);
}
if (esp_https_ota_is_complete_data_received(ota_handle)) {
err = esp_https_ota_finish(ota_handle);
if (err == ESP_OK) esp_restart();
} else {
esp_https_ota_abort(ota_handle);
}Rollback and Anti-Rollback
Enabling App Rollback
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=yWith rollback enabled, after esp_ota_set_boot_partition() + reboot, the new image boots in ESP_OTA_IMG_PENDING_VERIFY state. The app must call:
esp_ota_mark_app_valid_cancel_rollback();before any watchdog or reboot triggers. If it does not, the bootloader rolls back to the previous slot on the next boot.
Rollback Decision Pattern
void app_main(void)
{
// Early: check if we're running a newly OTA'd image
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_img_states_t ota_state;
if (esp_ota_get_state_partition(running, &ota_state) == ESP_OK) {
if (ota_state == ESP_OTA_IMG_PENDING_VERIFY) {
// Run diagnostics before committing
if (self_test_passed()) {
ESP_LOGI(TAG, "Self-test passed. Committing OTA image.");
esp_ota_mark_app_valid_cancel_rollback();
} else {
ESP_LOGE(TAG, "Self-test FAILED. Rolling back.");
esp_ota_mark_app_invalid_rollback_and_reboot();
}
}
}
// Continue normal app startup...
}Anti-Rollback (Security Counter)
Prevents downgrading to a vulnerable firmware version.
CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK=y
CONFIG_BOOTLOADER_APP_SEC_VER=1 # increment with each security-relevant release
CONFIG_BOOTLOADER_EFUSE_SECURE_VERSION_SCHEME=COUNTER # or DIGEST- Increment
CONFIG_BOOTLOADER_APP_SEC_VERonly for security fixes — cannot be decremented. - The bootloader reads the security version from eFuse and refuses to boot any image with a lower version.
Key sdkconfig Options
| Option | Purpose |
|---|---|
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE | Enable automatic rollback if app does not self-validate |
CONFIG_BOOTLOADER_APP_ANTI_ROLLBACK | Reject firmware with lower security version counter |
CONFIG_BOOTLOADER_APP_SEC_VER | Security version counter value baked into this build |
CONFIG_OTA_ALLOW_HTTP | Allow plain HTTP for OTA (dev only — never in production) |
CONFIG_ESP_HTTPS_OTA_DECRYPT_CB | Custom decryption callback for encrypted OTA images |
CONFIG_PARTITION_TABLE_CUSTOM | Use project-specific partition CSV |
Diagnostic Commands for OTA State
// Log running, boot, and next-update partitions
const esp_partition_t *running = esp_ota_get_running_partition();
const esp_partition_t *boot = esp_ota_get_boot_partition();
const esp_partition_t *next = esp_ota_get_next_update_partition(NULL);
ESP_LOGI(TAG, "Running: %s @ 0x%08" PRIx32, running->label, running->address);
ESP_LOGI(TAG, "Boot: %s @ 0x%08" PRIx32, boot->label, boot->address);
ESP_LOGI(TAG, "Update: %s @ 0x%08" PRIx32, next->label, next->address);
// Log OTA state of running partition
esp_ota_img_states_t state;
if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
ESP_LOGI(TAG, "OTA state: %d (%s)", state,
state == ESP_OTA_IMG_NEW ? "NEW" :
state == ESP_OTA_IMG_PENDING_VERIFY ? "PENDING_VERIFY" :
state == ESP_OTA_IMG_VALID ? "VALID" :
state == ESP_OTA_IMG_INVALID ? "INVALID" :
state == ESP_OTA_IMG_ABORTED ? "ABORTED" : "UNDEFINED");
}Common Failure Modes
| Symptom | Likely Cause |
|---|---|
Bootloader always boots ota_0 | otadata partition was erased or never written; run idf.py erase-flash and re-flash |
| Rollback on every boot | App never calls esp_ota_mark_app_valid_cancel_rollback() |
esp_ota_end returns ESP_ERR_OTA_VALIDATE_FAILED | Image hash check failed — data corruption during transfer |
HTTPS OTA fails with ESP_ERR_HTTP_CONNECT | Server cert not embedded or cert_pem pointer wrong |
| OTA slot too small | Binary grew past slot size — recalculate with idf.py size and widen CSV |
esp_ota_begin fails with ESP_ERR_INVALID_SIZE | image_size parameter too small; use OTA_WITH_SEQUENTIAL_WRITES |
OTA and Secure Boot
- Secure Boot verifies the image signature on boot; OTA images must be signed with the same key.
- Use
idf.py secure-target sign-dataor the build system's--sign-keypath to sign the binary before serving it. - With flash encryption enabled, the OTA partition is automatically encrypted on write — the plaintext binary URL is correct; the ESP32 encrypts in-place.
- Do not disable
CONFIG_SECURE_BOOT_ALLOW_ROM_BASICorCONFIG_SECURE_BOOT_ALLOW_JTAGin development then forget to re-enable restrictions for production builds.
Panic and Log Triage
Use this file when diagnosing resets, panics, boot loops, and unclear runtime failures from serial logs.
Collect the Right Data First
- Capture the full serial log from reset through failure (not only the panic tail).
- Record the exact ESP-IDF version, target chip, and build type.
- Record the command used (
idf.py monitor, baud, serial port). - Save the ELF file that matches the flashed binary for symbol resolution.
Reset / Failure Categories
- Build/link failure: compiler, linker, component dependency, or config mismatch.
- Flash/connection failure: serial port, permissions, cable, boot mode, stub/baud issues.
- Boot failure: partition table, image mismatch, early init crash, missing config/data.
- Runtime panic: null dereference, stack overflow, illegal instruction, watchdog timeout.
- Runtime functional failure: no panic, but incorrect behavior, timeouts, or lost connectivity.
Panic Triage Flow
1. Identify reset reason / panic headline. 2. Read the lines immediately before the panic for the triggering subsystem. 3. Decode backtrace against the matching ELF (use monitor decoding or addr2line workflow). 4. Inspect the top frames and the first app frame. 5. Check recent changes touching that subsystem, task, buffer, or callback path. 6. Add focused logs/asserts around the suspected boundary.
Common Embedded Root Causes to Check
- Null/uninitialized handles after partial init failure.
- Stack overflow in task with logging, JSON parsing, TLS, or BLE/Wi-Fi callbacks.
- Use-after-free or buffer lifetime crossing task boundaries.
- Calling non-ISR-safe APIs from an ISR or callback context.
- Race conditions around shared flags/queues without synchronization.
- Watchdog due to blocking loop, deadlock, or long critical section.
- Misconfigured pins/peripherals causing driver timeouts that cascade into watchdog resets.
Logging Guidance
- Use stable log tags per module (
wifi_mgr,sensor_task,ble_gatt, etc.). - Log state transitions and error codes, not only generic failure text.
- Include retry counts and timeout durations when diagnosing reconnect loops.
- Add temporary high-signal logs, then remove or downgrade once fixed.
Useful Commands (Adapt to Project)
idf.py buildidf.py flash monitoridf.py fullclean buildidf.py menuconfig
Note version-specific output and panic formatting may differ across ESP-IDF releases. Prefer interpreting logs with the project's actual ESP-IDF version and matching ELF artifacts.
ESP32 Partitions and sdkconfig (ESP-IDF)
Use this reference when changing flash layout, OTA support, or project configuration.
Core Rules
- Treat
sdkconfigand partition CSV as first-class project artifacts. - Prefer editing config files (
sdkconfig,sdkconfig.defaults, Kconfig fragments where used) over interactivemenuconfiginstructions for reproducibility. - Do not propose partition changes until flash size and OTA requirement are confirmed.
- Use the available flash capacity intentionally; avoid unexplained empty regions.
What to Confirm Before Partition Changes
- Exact flash size (for example 4MB, 8MB, 16MB)
- OTA requirement (single app vs dual-slot OTA, rollback needs)
- NVS size needs (Wi-Fi creds, app config, calibration data)
- Filesystem/data partition needs (SPIFFS/LittleFS/FATFS if used)
- Core dump partition requirement (if enabled)
- Factory app partition requirement (some products need it; many do not)
Partition Strategy Guidelines
No OTA Required
- Prefer a larger app partition plus appropriately sized NVS/data partitions.
- Avoid reserving OTA slots unless they are actually needed.
OTA Required
- Use OTA-compatible layout (typically
otadata+ two OTA app slots). - Size OTA slots based on current binary size plus growth headroom.
- Ensure partition choices align with
sdkconfigOTA and bootloader settings. - If rollback is used, ensure configuration and partitioning support it.
Flash Utilization Policy
- Every partition should have a reason.
- Free space should either:
- be assigned as growth headroom with an explicit note, or
- be allocated to useful data/app capacity.
- Do not leave large gaps because of copied example layouts that do not match the target flash.
sdkconfig Editing Workflow (Reproducible)
- Read current
sdkconfigand the relevant component/project defaults. - Change only the required keys.
- Keep related settings in sync (example: target, flash size, log levels, PSRAM, partition table options).
- Explain why each configuration change was made.
- Prefer checking the resulting
sdkconfigdiff over hand-wavy menu navigation steps.
menuconfig Policy
menuconfigis a discovery/debug tool, not the primary delivery artifact.- If
menuconfigis used to discover an option, reflect the final change insdkconfig/defaults and show the exact config keys. - Do not leave the user with only "open menuconfig and click X" guidance unless explicitly requested.
Partition / Config Review Checklist
- Exact flash size confirmed.
- OTA requirement confirmed.
- Partition table matches feature set and flash capacity.
- App slot sizes include realistic headroom.
- NVS/data/core dump partitions sized intentionally.
sdkconfigpartition-table selection points to the correct CSV.- Log level, PSRAM, and flash/boot settings are consistent with performance/debug goals.
- No copied example layout remains without justification.
ESP32 Power Optimization (ESP-IDF)
Use this reference for ESP32 low-power modes, wakeup design, dynamic power management, and battery-aware behavior in ESP-IDF projects.
Start With Power Budget and Wakeup Model
- Define target average current, active duty cycle, wakeup sources, and latency requirements first.
- Power tuning without a measurement plan usually produces misleading results.
- Identify whether the product is:
- always-connected mains-powered
- battery-powered periodic sensor
- bursty wireless device
- low-latency interactive device
ESP32 Sleep Modes (Practical)
- Active mode: CPU/peripherals/radios running.
- Modem sleep: CPU active, radio duty-cycled/power-save behavior (Wi-Fi/BLE use case dependent).
- Light sleep: CPU paused with faster wake than deep sleep; RAM retained (chip/config dependent).
- Deep sleep: lowest-power common mode; most runtime state lost except RTC-retained data/configured wake sources.
Choose based on:
- required wake latency
- state retention needs
- radio reconnect cost
- sampling interval
Wakeup Sources (ESP-IDF)
- Timer wakeup:
esp_sleep_enable_timer_wakeup(...) - GPIO/EXT wakeup (chip-specific APIs and limitations differ by target)
- ULP / coprocessor wakeup on supported chips
- Touch / UART wakeup on supported targets and configurations
Always verify target-specific wakeup support for your exact chip (esp32, esp32s3, esp32c3, etc.).
Dynamic Frequency Scaling and PM Locks
- Prefer ESP-IDF power management APIs over manual clock manipulation.
- Use
esp_pm_configure(...)for DFS/light-sleep policies where supported. - Use PM locks (
esp_pm_lock_*) only around operations that truly require a minimum frequency or no light sleep. - Release locks promptly; leaked PM locks are a common reason “power save doesn’t work.”
Wi-Fi / BLE Power Strategy
- Radio behavior often dominates power consumption.
- Optimize at the system level:
- batching network activity
- reducing reconnect churn
- using appropriate Wi-Fi power-save mode
- minimizing unnecessary scans/advertising activity
- Validate power impact of retry loops and error handling; “recover faster” can cost much more energy.
Peripheral Power Management
- Deinit or stop unused peripherals/drivers when idle (ADC, SPI devices, sensors, UARTs if safe).
- Gate external sensors/rails with load switches when hardware allows.
- Avoid periodic polling when interrupt/event-based wakeup is feasible.
- Use DMA/queued transfers to reduce CPU wake time for bulk I/O.
GPIO Leakage and Deep Sleep Considerations
- Configure unused pins to known safe states for your board design (high-Z, pull, or driven level depending leakage path).
- Check board-specific leakage via external pull-ups, level shifters, sensors, and transistor networks.
- Use RTC IO hold/isolation features where appropriate and supported.
- Beware strapping pins and boot requirements when changing default pin states.
Battery Monitoring and Adaptive Behavior
- Use calibrated ADC measurements (ESP-IDF ADC calibration APIs) when voltage accuracy matters.
- Sample battery at controlled times (load state affects voltage).
- Define thresholds with hysteresis to avoid oscillation.
- Adapt workload:
- sample less often
- reduce radio activity
- defer non-critical features
Measurement and Verification
- Measure current with appropriate tools (power analyzer/current meter), not only software estimates.
- Compare:
- idle active
- light sleep
- deep sleep
- radio TX/RX peaks
- reconnect storms / failure conditions
- Record exact firmware config (
sdkconfig, target, board revision) with measurements.
Review Checklist (Merged and ESP32-Adapted)
- Sleep mode chosen based on latency + retention + reconnect cost.
- Wakeup sources and chip-specific limitations verified.
- PM locks acquired only when needed and released correctly.
- Wireless retry/connect logic reviewed for energy impact.
- Peripheral/sensor idle states and external rail control considered.
- GPIO leakage paths and strapping pin states reviewed.
- Battery thresholds use hysteresis and calibrated ADC path when needed.
- Power claims backed by measurement, not estimates alone.
ESP32 FreeRTOS Patterns (ESP-IDF)
Use this reference for ESP32/ESP-IDF tasking, synchronization, ISR handoff, timers, and watchdog-safe concurrency design.
Scope and Defaults
- Target ESP-IDF projects using the built-in FreeRTOS integration.
- Prefer standard FreeRTOS APIs plus ESP-IDF integrations (
esp_event,esp_timer,esp_task_wdt) over custom schedulers. - Treat dual-core behavior as an explicit design concern on classic ESP32/ESP32-S3. Do not assume dual-core on all ESP32 variants (for example, ESP32-C3 is single-core).
Task Design Patterns
Periodic Task (No Drift)
- Use
vTaskDelayUntil()for periodic sampling/control loops. - Measure execution time and log misses if the loop can overrun its period.
- Keep peripheral transactions bounded with timeouts.
static void sensor_task(void *arg)
{
TickType_t last = xTaskGetTickCount();
const TickType_t period = pdMS_TO_TICKS(100);
for (;;) {
read_sensor_once_with_timeout();
vTaskDelayUntil(&last, period);
}
}Event-Driven Task
- Prefer a queue when payload data must be transferred.
- Prefer task notifications when only signaling/bit flags are needed (lower overhead than semaphores).
- Prefer event groups for multi-subsystem readiness gates.
ISR to Task Handoff (ESP32-Specific Constraints)
- Keep ISRs short and
IRAM-safe if they can run while flash cache is unavailable. - Use
IRAM_ATTRon time-critical ISRs and ensure called functions/data are in IRAM/DRAM as required by the interrupt context. - Use
xQueueSendFromISR(),xTaskNotifyFromISR(), orvTaskNotifyGiveFromISR()only. - Call
portYIELD_FROM_ISR()when a higher-priority task was woken. - Never call blocking ESP-IDF driver APIs from an ISR.
static TaskHandle_t s_worker_task;
static void IRAM_ATTR gpio_isr_handler(void *arg)
{
BaseType_t hp_task_woken = pdFALSE;
vTaskNotifyGiveFromISR(s_worker_task, &hp_task_woken);
if (hp_task_woken) {
portYIELD_FROM_ISR();
}
}Core Affinity and Priority Guidance
- Do not pin tasks to a core unless there is a clear reason (latency, driver affinity, cache behavior, isolation).
- Use
xTaskCreatePinnedToCore()only when measured behavior requires it. - Review priority inversions when multiple tasks share I2C/SPI/UART/network resources.
- Avoid long blocking calls in high-priority tasks; they commonly trigger watchdog symptoms.
Synchronization Patterns
Mutexes
- Use mutexes for shared peripheral buses (I2C/SPI) or shared state with non-trivial critical sections.
- Keep lock hold time short; do not log heavily while holding a mutex.
- Prefer one owner task for complex peripherals instead of many tasks sharing a mutex.
Critical Sections
- Use only for very short, bounded read-modify-write operations.
- Avoid wrapping driver calls, logging, or queue operations in critical sections.
- Remember critical sections can increase interrupt latency and watchdog risk.
Timers: Pick the Right Tool
esp_timer: high-resolution callbacks, deferred work scheduling, microsecond time base.- FreeRTOS software timers: lightweight task-context timer callbacks, millisecond-scale periodic work.
gptimerdriver: hardware timer peripheral, waveform/capture/tighter timing use cases.
Rule of thumb:
- Control loop / task cadence -> task +
vTaskDelayUntil() - App-level retry/backoff -> FreeRTOS timer or
esp_timer - Precise peripheral timing/capture -> hardware timer (
gptimer, RMT, LEDC depending use case)
ESP-IDF Event Loop Integration
- Use
esp_eventfor Wi-Fi, IP, and other subsystem events instead of ad hoc polling. - Keep event handlers small; defer heavy work to a task/queue.
- Track handler registration/unregistration to avoid duplicate callbacks and leaks.
Memory and Stack Monitoring (RTOS-Focused)
- Monitor stack margins with
uxTaskGetStackHighWaterMark()during testing. - Watch heap health with
heap_caps_get_free_size()andheap_caps_get_minimum_free_size(). - Treat task stack size as a design parameter, especially for logging, JSON/TLS, and protocol parsing paths.
Watchdog and Liveness
- Use
esp_task_wdtfor long-running tasks in production where appropriate. - Feed/monitor watchdogs intentionally; do not “fix” watchdog resets by disabling the watchdog first.
- Investigate root causes: deadlocks, long critical sections, busy loops, blocked callbacks, or starved lower-priority tasks.
Runtime Introspection via Service Terminal (Recommended)
- If a USB/serial service terminal exists, expose bounded RTOS diagnostics commands:
- task list/state/priority
- stack high-water marks
- heap/min-free snapshots
- Prefer snapshot-style commands over long-running reports.
- Ensure diagnostic commands do not block critical tasks or hold shared locks for long.
Review Checklist (Merged and ESP32-Adapted)
- Use
vTaskDelayUntil()for periodic tasks to avoid drift. - Keep ISRs short and defer work to tasks via queue/notification/event bits.
- Verify ISR-safe API usage and IRAM safety for ISR paths.
- Use task notifications when payload transfer is not needed.
- Size stacks from measurement, not guesswork.
- Prefer mutexes over long critical sections; check priority inversion exposure.
- Monitor heap/stack during bring-up and regression testing.
- Confirm watchdog strategy for production builds.
- If a service terminal exists, verify RTOS diagnostics commands are safe and bounded.
Security Hardening Reference
Security Feature Overview
| Feature | Where Configured | Reversible? | Production Required? |
|---|---|---|---|
| Secure Boot v2 | eFuse + sdkconfig | No (eFuse burn) | Yes for signed field devices |
| Flash Encryption | eFuse + sdkconfig | No (Development mode only) | Yes for sensitive data |
| NVS Encryption | sdkconfig + key partition | Yes (key erasable) | If NVS holds secrets |
| JTAG Disable | eFuse | No | Yes for production |
| UART Download Disable | eFuse | No | Yes for tamper resistance |
| Service Terminal Auth | App code | Yes | Required if terminal exposed in production |
Burn eFuse bits only after testing in Development mode. Release mode eFuse burns are permanent and irreversible.
---
Secure Boot v2
Verifies every stage of the boot chain (bootloader → app) using RSA-PSS or ECDSA signatures.
Generate Signing Key
espsecure.py generate_signing_key --version 2 --scheme rsa3072 secure_boot_signing_key.pem
# Keep secure_boot_signing_key.pem offline and in a secrets manager. Never commit it.sdkconfig Settings (Development — key burned via idf.py)
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_BOOT_SIGNING_KEY="secure_boot_signing_key.pem"
CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=y
# Development: allows reflashing
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=y # disable for production
CONFIG_SECURE_BOOT_ALLOW_JTAG=y # disable for productionsdkconfig Settings (Production)
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
CONFIG_SECURE_BOOT_SIGNING_KEY="secure_boot_signing_key.pem"
CONFIG_SECURE_BOOT_BUILD_SIGNED_BINARIES=y
CONFIG_SECURE_BOOTLOADER_NO_REBOOT_ON_FAILURE=y # brick if verification fails
# CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC is NOT set
# CONFIG_SECURE_BOOT_ALLOW_JTAG is NOT setFirst Flash with Secure Boot
idf.py build
# Bootloader is signed automatically at build time with the configured key.
idf.py -p /dev/ttyUSB0 flash # Burns Secure Boot eFuse on first successful bootSigning OTA Images
OTA images must be signed with the same key used for the bootloader:
espsecure.py sign_data --version 2 --keyfile secure_boot_signing_key.pem \
--output firmware_signed.bin build/firmware.bin---
Flash Encryption
Encrypts all flash contents (bootloader, app, NVS, OTA partitions) using AES-XTS-256.
Development Mode (Reversible via reflash)
CONFIG_FLASH_ENCRYPTION_ENABLED=y
CONFIG_FLASH_ENCRYPTION_MODE_DEVELOPMENT=y- ESP32 generates a random key and burns it to eFuse on first encrypted boot.
- You can still reflash in development mode using
idf.py encrypted-flash. - The plaintext binary is encrypted before writing.
Release Mode (Permanent — production only)
CONFIG_FLASH_ENCRYPTION_ENABLED=y
CONFIG_FLASH_ENCRYPTION_MODE_RELEASE=y- Disables UART download mode permanently.
- No more plaintext reflashing after this eFuse is burned.
Flash Encrypted Build + Flash Workflow
idf.py build
idf.py -p /dev/ttyUSB0 encrypted-flash # initial flash (pre-encryption)
# After first boot, flash is encrypted; subsequent OTA goes through esp_ota_ops normallyPre-encrypting Binaries for Factory Programming
# Get the device's flash encryption key (already burned to eFuse in development mode):
espefuse.py -p /dev/ttyUSB0 burn_key BLOCK_KEY0 flash_encryption_key.bin FLASH_ENCRYPTION
# Encrypt a binary offline (for factory programming without serial access):
espsecure.py encrypt_flash_data --aes-xts --keyfile flash_encryption_key.bin \
--address 0x10000 --output app_encrypted.bin build/app.bin---
NVS Encryption
Encrypts NVS partition contents using AES-XTS. Protects credentials, calibration data, and secrets stored in NVS.
Generate NVS Encryption Key Partition
python $IDF_PATH/components/nvs_flash/nvs_partition_generator/nvs_partition_gen.py \
generate-key --keytype XTS_AES_256 --key_protect_hmac \
--kp_hmac_keygen --kp_hmac_keyfile hmac_key.bin \
--kp_hmac_inputkey nvs_key_partition.binsdkconfig Settings
CONFIG_NVS_ENCRYPTION=y
CONFIG_NVS_SEC_KEY_PROTECTION_SCHEME_HMAC=y # or _FLASH_ENC if using flash encryptionInitializing NVS with Encryption at Runtime
#include "nvs_flash.h"
#include "nvs_sec_provider.h"
nvs_sec_cfg_t nvs_sec_cfg;
nvs_sec_scheme_t *sec_scheme_handle = NULL;
// Register the HMAC-based security scheme
ESP_ERROR_CHECK(nvs_sec_provider_register_hmac(&nvs_sec_cfg, &sec_scheme_handle));
// Init NVS with the encryption scheme
esp_err_t err = nvs_flash_init_with_sec_cfg(&nvs_sec_cfg);
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_with_sec_cfg(&nvs_sec_cfg);
}
ESP_ERROR_CHECK(err);---
Disabling Debug Interfaces
JTAG (via eFuse)
# Check current JTAG eFuse state first:
espefuse.py -p /dev/ttyUSB0 summary
# Permanently disable JTAG (irreversible):
espefuse.py -p /dev/ttyUSB0 burn_efuse JTAG_DISABLEOr via sdkconfig (burned automatically at first boot with Secure Boot Release mode):
# CONFIG_SECURE_BOOT_ALLOW_JTAG is not setUART Download Mode
CONFIG_SECURE_BOOT_ALLOW_ROM_BASIC=n # prevents ROM serial downloader in secure boot
# For full disable (Release flash encryption also disables this):
CONFIG_ESP_CONSOLE_UART_NONE=y # removes console UART entirely (extreme hardening)---
Service Terminal Hardening
The on-device service terminal (see references/device-terminal-console.md) must be controlled in production builds.
Compile-Time Removal
// In app_console_commands.c or main.c:
#ifdef CONFIG_APP_SERVICE_TERMINAL_ENABLE
app_console_init();
#endif# sdkconfig.defaults for production:
# CONFIG_APP_SERVICE_TERMINAL_ENABLE is not setRuntime Authentication (if terminal must remain in production)
static bool terminal_authenticated = false;
static int cmd_auth(int argc, char **argv)
{
if (argc != 2) {
printf("Usage: auth <token>\n");
return 1;
}
// Use constant-time comparison to avoid timing attacks
const char *expected = config_get_terminal_token(); // from encrypted NVS
if (expected && strlen(argv[1]) == strlen(expected) &&
memcmp(argv[1], expected, strlen(expected)) == 0) {
terminal_authenticated = true;
printf("Authenticated.\n");
return 0;
}
printf("Authentication failed.\n");
vTaskDelay(pdMS_TO_TICKS(2000)); // rate-limit brute force
return 1;
}
// Guard all sensitive commands:
static int cmd_settings(int argc, char **argv)
{
if (!terminal_authenticated) {
printf("Not authenticated. Run: auth <token>\n");
return 1;
}
// ... settings logic
}---
Secure Coding Practices
Stack Canaries
CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y # adds __stack_chk_guard checks
# or stronger:
CONFIG_COMPILER_STACK_CHECK_MODE_STRONG=yHeap Integrity Checks (Development/QA builds)
CONFIG_HEAP_POISONING_COMPREHENSIVE=y # expensive, use for test builds only
CONFIG_HEAP_TASK_TRACKING=yAssert Behavior
# Development: abort on assert failure (captures stack trace)
CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=2
# Production: log + reset (avoids exposing stack trace externally)
CONFIG_COMPILER_OPTIMIZATION_ASSERTION_LEVEL=1TLS Certificate Pinning for HTTPS OTA
// Embed server certificate in the firmware binary:
// In CMakeLists.txt:
// target_add_binary_data(${COMPONENT_LIB} "server_cert.pem" TEXT)
extern const uint8_t server_cert_pem_start[] asm("_binary_server_cert_pem_start");
extern const uint8_t server_cert_pem_end[] asm("_binary_server_cert_pem_end");
esp_http_client_config_t cfg = {
.url = OTA_URL,
.cert_pem = (const char *)server_cert_pem_start,
// .use_global_ca_store = false, // do not use — pin to specific cert
.skip_cert_common_name_check = false,
};Sensitive Data Lifetime
- Zero secrets in RAM after use:
explicit_bzero(buf, len)ormemset+ compiler barrier. - Do not log credentials, tokens, or key material at any log level.
- Store secrets in NVS with encryption enabled — never in SPIFFS or plain NVS.
---
Production Build Checklist
- [ ] Secure Boot v2 enabled; signing key stored offline (not in repo)
- [ ] Flash Encryption in Release mode (or Development mode for engineering builds)
- [ ] NVS Encryption enabled for all secret/credential namespaces
- [ ] JTAG disabled via eFuse or Secure Boot Release policy
- [ ] UART download mode disabled (Release flash encryption) or ROM basic disabled
- [ ] Service terminal removed or auth-gated behind a credential from encrypted NVS
- [ ] OTA images signed with Secure Boot key before serving
- [ ] Anti-rollback counter set and
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y - [ ] TLS certificate pinned for all HTTPS connections (OTA, cloud, etc.)
- [ ] Stack canaries enabled (
CONFIG_COMPILER_STACK_CHECK_MODE_NORM) - [ ] No debug symbols or verbose logs in release build (
CONFIG_LOG_DEFAULT_LEVEL_WARNor higher) - [ ] No
CONFIG_OTA_ALLOW_HTTP=yin production sdkconfig - [ ]
espefuse.py summaryrun and verified before shipping; no unexpected eFuse bits set
ESP-IDF Toolchain and Shell Setup (ESP32/ESP-IDF)
Use this reference before build/flash/monitor operations and when improving developer UX.
Pre-Build Toolchain Rule
- Before running
build.sh/idf.py build, verify ESP-IDF is actually usable: idf.pyresolvesidf.py --versionsucceeds- Do not assume the toolchain is installed because a path exists or an older shell once worked.
Minimum Preflight Checks
command -v idf.pyidf.py --versionpython3 --version(if the build wrappers rely on Python and virtual env tooling)- project wrapper preflight (if provided) succeeds
If any fail:
- source the ESP-IDF environment (
export.shor~/.esp_idf_env) - verify
IDF_PATH/installation path - fix shell PATH setup before continuing
Shell UX Helper (Recommended)
If the user shell lacks a convenient ESP-IDF shortcut, add a small snippet to the shell profile (~/.zshrc, ~/.bashrc, etc.).
zsh snippet (example)
# ESP-IDF Environment (auto-load)
# source ~/.esp_idf_env # Uncomment to auto-load on terminal start
alias idf="source \"$HOME/.esp_idf_env\""
export PATH="$PATH:$HOME/go/bin"
export PATH="$HOME/.local/bin:$PATH"Notes:
alias idf=...provides a quick environment load command.- Keep auto-loading commented by default unless the user wants every shell to source ESP-IDF.
- Ensure
$HOME/.local/binis early enough inPATHfor user-installed tools.
Agent Behavior
- If build preflight fails, fix shell/toolchain setup before attempting the build.
- After toolchain preflight, run plugin/framework compatibility preflight before building when ESP-ADF/ESP-SR/etc. are used.
- If a shell helper snippet is missing and the user is using zsh/bash, add one (or propose one) to improve repeated workflow UX.
- Avoid duplicating the snippet if equivalent aliases/PATH entries already exist.
Review Checklist
- Build preflight checks run before build/flash/monitor.
- Plugin/framework compatibility evidence is checked before build for stacks that use ESP-ADF/ESP-SR/etc.
- ESP-IDF env source path is valid on the current machine.
- Shell helper snippet exists (or user intentionally declined).
- No duplicate/conflicting
idfaliases or PATH entries introduced.
ESP32 Firmware Engineering Values
Use this file first. It defines non-negotiable behavior for the ESP32 firmware skill.
1. Hardware Truth Before Code
Value: Firmware must match the actual hardware, not an assumed board.Why: Most embedded failures come from incorrect pin maps, electrical assumptions, or missing peripheral context.Do:- Confirm exact ESP32 variant.
- Confirm peripheral inventory, wiring, buses, power rails, transceivers, and display/controller details.
- Ask for missing information before implementation or debugging.
Avoid:- Guessing pins, interfaces, pull-ups, voltage levels, or controller models.
- Porting generic examples directly into ESP-IDF projects.
Blocking rule: If hardware context is incomplete for hardware-facing tasks, stop and ask.
2. Variant Certainty Is Mandatory
Value: Never proceed with an unknown ESP32 target.Why: ESP32 variants differ materially (cores, peripherals, memory, radio/peripheral capabilities, sleep/wakeup features).Do: Require exact target (esp32,esp32s3,esp32c3, etc.) and ESP-IDF version.Avoid: Writing code that assumes dual-core behavior, peripheral availability, or wakeup features across variants.Review cues:- Does the code assume a peripheral not present on the target?
- Does it assume dual-core/pinning where the chip is single-core?
3. Configuration and Partitions Are Source Code
Value:sdkconfigand partition tables are part of the deliverable, not afterthoughts.Why: Many runtime failures and performance issues are configuration-driven, not code-driven.Do:- Edit
sdkconfig/sdkconfig.defaultsdeliberately and reproducibly. - Size partitions to fit the actual flash and feature set.
- Use OTA-compatible layouts when OTA is required.
- Avoid leaving unexplained flash capacity unused.
Avoid:- “Use menuconfig and click around” as the primary workflow.
- Partition guesses without flash-size/OTA requirements.
4. Build-Proven Changes Only
Value: A change is not done until the project builds cleanly in the project workflow.Why: Embedded breakage often appears in generated config, link stage, or warnings that indicate real bugs.Do:- Verify ESP-IDF tooling is installed and usable before the build step (not just "probably installed").
- Run project
build.shafter changes (or equivalent build wrapper). - Fix failures and rerun until it passes.
- Treat important warnings as work to resolve, not noise.
Avoid: Declaring completion based only on reasoning or partial compilation.
5. Compatibility Evidence Before Progress
Value: Version compatibility is a proof obligation, not a guess.Why: ESP-IDF + ESP-ADF + ESP-SR stacks can fail unless versions match exact supported combinations.Do:- Identify exact versions/tags/commits for every plugin/framework in use.
- Collect concrete evidence from official matrices/manifests/release notes.
- Require explicit cross-stack evidence when multiple frameworks interact (for example ADF + SR).
Avoid:- Assuming “latest with latest” is compatible.
- Proceeding on anecdotal compatibility without version proof.
Blocking rule: If any plugin compatibility link is unproven, stop before build/debug/flash.
6. High-Signal Observability
Value: Suppress noise, increase application signal.Why: Embedded debugging depends on logs, but noisy defaults hide causality.Do:- Reduce irrelevant library/default component logs when diagnosing.
- Keep application logs verbose, tagged, and stateful.
- Log error codes, retries, timings, and transitions.
Avoid:- Global log suppression that removes evidence.
- Generic “failed” logs without context.
7. Serviceability by Default (When Console Transport Is Free)
Value: If a USB/serial console path is available and unused, ship a basic terminal by default.Why: Runtime inspection and settings control dramatically reduce debug iteration time and field diagnosis effort.Do:- Use ESP-IDF console/REPL with help, history, and autocomplete.
- Expose safe commands for settings, status, heap, RTOS diagnostics, and log levels.
- Keep command handlers bounded and user-friendly.
Avoid:- Ad hoc parsers with poor error messages.
- Debug-only commands that destabilize timing or expose secrets.
Blocking rule: Confirm transport ownership and security policy before enabling the terminal.
8. Correct Data Format First (Especially Displays)
Value: Data format correctness comes before graphics logic or performance tuning.Why: Display bugs are often pixel format, byte order, stride, or controller-init mismatches.Do:- Confirm controller, bus mode, resolution, pixel format, endian/color order, and flush region format.
- Convert only to the exact format the display path expects.
Avoid:- Guessing RGB/BGR order or assuming RGB565 packing.
- “Fixing” color problems by random bit swapping.
9. Performance Choices Must Respect Hardware Limits
Value: Use the most performant reliable option supported by the actual hardware.Why: Throughput depends on bus speed, DMA capability, memory placement, flash/PSRAM modes, and signal integrity.Do:- Check RAM/flash/PSRAM capabilities and bus timing limits.
- Use DMA-capable memory where required.
- Measure, then tune.
Avoid:- Benchmarking assumptions detached from board wiring and clock config.
- Chasing speed with unstable settings.
10. Explicit Unknowns, Explicit Risks
Value: State what is known, unknown, and unverified in hardware.Why: Embedded software can appear correct in code review while failing on real boards.Do: Call out missing hardware validation and remaining assumptions in the final response.Do: Ask for example code (project snippet, known-good implementation, or minimal repro) when uncertainty would otherwise force guessing.Avoid: Implying hardware verification when only build/log review was performed.Avoid: Filling gaps with guessed API usage or assumed behavior when an example can be requested.
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=/dev/null
. "${SCRIPT_DIR}/common.sh"
FAIL_ON_WARNINGS=0
if [ "${1:-}" = "--strict-warnings" ]; then
FAIL_ON_WARNINGS=1
shift
fi
LOG_FILE=${BUILD_LOG_FILE:-"${BUILD_DIR}/build.log"}
mkdir -p "$BUILD_DIR"
log "Project: $PROJECT_DIR"
log "Build dir: $BUILD_DIR"
run_idf build "$@" 2>&1 | tee "$LOG_FILE"
if [ "$FAIL_ON_WARNINGS" -eq 1 ] && grep -Eiq '(^|[^[:alpha:]])warning:' "$LOG_FILE"; then
log "ERROR: Build completed with warnings and --strict-warnings was enabled."
exit 2
fi
log "Build completed successfully."
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
PROJECT_DIR=${PROJECT_DIR:-$(cd "${SCRIPT_DIR}/.." && pwd)}
BUILD_DIR=${BUILD_DIR:-"${PROJECT_DIR}/build"}
IDF_PY=${IDF_PY:-idf.py}
IDF_BAUD=${IDF_BAUD:-460800}
log() {
printf '[esp-idf-scripts] %s\n' "$*"
}
print_shell_setup_hint() {
log "Shell UX hint (zsh): add this to ~/.zshrc if missing:"
printf '%s\n' '# ESP-IDF Environment (auto-load)'
printf '%s\n' '# source ~/.esp_idf_env # Uncomment to auto-load on terminal start'
printf '%s\n' 'alias idf="source \"$HOME/.esp_idf_env\""'
printf '%s\n' 'export PATH="$PATH:$HOME/go/bin"'
printf '%s\n' 'export PATH="$HOME/.local/bin:$PATH"'
}
os_name() {
uname -s 2>/dev/null || printf 'Unknown'
}
source_idf_env_if_needed() {
if command -v "$IDF_PY" >/dev/null 2>&1; then
return 0
fi
local candidates=()
if [ -n "${IDF_PATH:-}" ]; then
candidates+=("${IDF_PATH}/export.sh")
fi
candidates+=(
"$HOME/esp/esp-idf/export.sh"
"$HOME/.espressif/frameworks/esp-idf-v5.*/export.sh"
"$HOME/.esp_idf_env"
)
local path
for path in "${candidates[@]}"; do
# shellcheck disable=SC2086
for expanded in $path; do
if [ -f "$expanded" ]; then
# shellcheck source=/dev/null
. "$expanded"
if command -v "$IDF_PY" >/dev/null 2>&1; then
log "Loaded ESP-IDF environment from $expanded"
return 0
fi
fi
done
done
log "ERROR: idf.py not found. Set IDF_PATH or source ESP-IDF export.sh first."
print_shell_setup_hint
return 1
}
verify_idf_installation() {
if [ "${IDF_PREFLIGHT_DONE:-0}" = "1" ]; then
return 0
fi
source_idf_env_if_needed
if ! command -v "$IDF_PY" >/dev/null 2>&1; then
log "ERROR: idf.py is still not available after environment load."
print_shell_setup_hint
return 1
fi
local idf_version
if ! idf_version=$("$IDF_PY" --version 2>/dev/null); then
log "ERROR: idf.py exists but failed to run. ESP-IDF installation/environment may be incomplete."
log "Try: source ~/.esp_idf_env"
print_shell_setup_hint
return 1
fi
log "ESP-IDF ready: ${idf_version}"
export IDF_PREFLIGHT_DONE=1
}
verify_plugin_compatibility_evidence() {
if [ "${SKIP_PLUGIN_COMPAT_CHECK:-0}" = "1" ]; then
log "Skipping plugin compatibility check (SKIP_PLUGIN_COMPAT_CHECK=1)."
return 0
fi
local checker="${SCRIPT_DIR}/check_plugin_compatibility.py"
if [ ! -f "$checker" ]; then
log "Plugin compatibility checker not found at $checker (skipping)."
return 0
fi
if ! command -v python3 >/dev/null 2>&1; then
log "ERROR: python3 is required for plugin compatibility checks."
return 1
fi
mkdir -p "$BUILD_DIR"
log "Verifying plugin/framework compatibility evidence (ESP-IDF <-> ADF/SR/etc.)"
if ! PROJECT_DIR="$PROJECT_DIR" BUILD_DIR="$BUILD_DIR" IDF_PY="$IDF_PY" python3 "$checker"; then
log "ERROR: Plugin compatibility verification failed."
log "See evidence report: ${BUILD_DIR}/plugin-compatibility-evidence.txt"
log "Tip: Set ESP_REQUIRED_PLUGINS=esp-adf,esp-sr (or subset) to force/clarify checks."
log "Tip: Provide cross-stack proof via ESP_STACK_COMPAT_EVIDENCE or a project compatibility lock file."
return 1
fi
}
read_sdkconfig_target() {
local sdkconfig="${PROJECT_DIR}/sdkconfig"
if [ -f "$sdkconfig" ]; then
sed -n 's/^CONFIG_IDF_TARGET="\([^"]*\)"/\1/p' "$sdkconfig" | head -n1
fi
}
ensure_target() {
local requested="${ESP_TARGET:-}"
local configured
configured=$(read_sdkconfig_target || true)
if [ -z "$requested" ] && [ -n "$configured" ]; then
requested="$configured"
fi
if [ -z "$requested" ]; then
log "ERROR: ESP target not known. Set ESP_TARGET (e.g. esp32s3) or create sdkconfig with CONFIG_IDF_TARGET."
return 1
fi
if [ "${configured:-}" != "$requested" ]; then
log "Setting target to ${requested} (sdkconfig target was '${configured:-unset}')"
"$IDF_PY" -C "$PROJECT_DIR" -B "$BUILD_DIR" set-target "$requested"
fi
export ESP_TARGET="$requested"
}
detect_port() {
if [ -n "${PORT:-}" ]; then
if [ ! -e "$PORT" ]; then
log "WARNING: PORT=${PORT} does not exist. Check device connection and path."
fi
printf '%s\n' "$PORT"
return 0
fi
local os
os=$(os_name)
local globs=()
case "$os" in
Darwin)
globs=(
/dev/cu.usbmodem*
/dev/cu.usbserial*
/dev/cu.SLAB_USBtoUART*
/dev/cu.wchusbserial*
)
;;
Linux)
globs=(
/dev/serial/by-id/*
/dev/ttyACM*
/dev/ttyUSB*
)
;;
*)
globs=(/dev/ttyACM* /dev/ttyUSB* /dev/cu.usbmodem* /dev/cu.usbserial*)
;;
esac
local found=()
local g expanded
for g in "${globs[@]}"; do
# shellcheck disable=SC2086
for expanded in $g; do
if [ -e "$expanded" ]; then
found+=("$expanded")
fi
done
done
if [ "${#found[@]}" -eq 0 ]; then
log "ERROR: No serial port detected. Connect the device and try again."
log " Set PORT=/dev/ttyUSB0 (Linux) or PORT=/dev/cu.usbmodemXXXX (macOS) to override."
return 1
fi
if [ "${#found[@]}" -eq 1 ]; then
log "Auto-detected port: ${found[0]}"
printf '%s\n' "${found[0]}"
return 0
fi
log "ERROR: Multiple serial ports detected. Set PORT=<device> to choose one:"
local p
for p in "${found[@]}"; do
log " $p"
done
log "Example: PORT=${found[0]} ./scripts/flash.sh"
return 1
}
run_idf() {
verify_idf_installation
verify_plugin_compatibility_evidence
ensure_target
"$IDF_PY" -C "$PROJECT_DIR" -B "$BUILD_DIR" "$@"
}
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=/dev/null
. "${SCRIPT_DIR}/common.sh"
"${SCRIPT_DIR}/flash.sh" "$@"
"${SCRIPT_DIR}/monitor.sh"
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=/dev/null
. "${SCRIPT_DIR}/common.sh"
BUILD_ARGS=()
FLASH_ARGS=()
for arg in "$@"; do
if [ "$arg" = "--strict-warnings" ]; then
BUILD_ARGS+=("$arg")
else
FLASH_ARGS+=("$arg")
fi
done
if [ "${SKIP_BUILD:-0}" != "1" ]; then
"${SCRIPT_DIR}/build.sh" "${BUILD_ARGS[@]}"
fi
PORT=$(detect_port)
export PORT
log "Flashing ${ESP_TARGET:-$(read_sdkconfig_target || true)} on port ${PORT} at baud ${IDF_BAUD}"
run_idf -p "$PORT" -b "$IDF_BAUD" flash "${FLASH_ARGS[@]}"
log "Flash completed successfully."
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=/dev/null
. "${SCRIPT_DIR}/common.sh"
log "Opening menuconfig (fallback/discovery tool). Persist final changes in sdkconfig or sdkconfig.defaults."
run_idf menuconfig "$@"
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
# shellcheck source=/dev/null
. "${SCRIPT_DIR}/common.sh"
PORT=$(detect_port)
export PORT
log "Opening monitor on ${PORT}"
run_idf -p "$PORT" monitor "$@"
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
VENV_DIR="$ROOT_DIR/.venv"
PYTHON_BIN="${PYTHON:-python3}"
VALIDATOR_SCRIPT="/Users/adamlipecz/.codex/skills/.system/skill-creator/scripts/quick_validate.py"
TARGET_DIR="${1:-$ROOT_DIR}"
if [[ ! -f "$VALIDATOR_SCRIPT" ]]; then
echo "Validator not found: $VALIDATOR_SCRIPT" >&2
exit 1
fi
if [[ ! -x "$VENV_DIR/bin/python" ]]; then
echo "Creating virtual environment at $VENV_DIR"
"$PYTHON_BIN" -m venv "$VENV_DIR"
fi
if ! "$VENV_DIR/bin/python" -c "import yaml" >/dev/null 2>&1; then
echo "Installing PyYAML into $VENV_DIR"
"$VENV_DIR/bin/python" -m pip install --disable-pip-version-check pyyaml
fi
echo "Running validator on: $TARGET_DIR"
"$VENV_DIR/bin/python" "$VALIDATOR_SCRIPT" "$TARGET_DIR"
exit $?
Related skills
How it compares
Pick this over generic C/C++ skills when the target is ESP-IDF, FreeRTOS, and Espressif-specific sdkconfig, partitions, and OTA workflows.
FAQ
Which ESP32 chips does esp32-firmware-engineer support?
esp32-firmware-engineer supports ESP32 family targets including ESP32-S3, ESP32-C3, ESP32-C6, and ESP32-S2 through ESP-IDF workflows. Activation keywords cover idf.py, sdkconfig, partitions.csv, and Espressif frameworks like ESP-ADF, ESP-SR, and LVGL.
What firmware tasks does esp32-firmware-engineer handle?
esp32-firmware-engineer writes, reviews, and debugs ESP-IDF C/C++ firmware, analyzes FreeRTOS concurrency patterns, and addresses power management, OTA updates, secure boot, and flash encryption. Developers invoke it for bring-up, optimization, and production hardening on connect
Is Esp32 Firmware Engineer safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.