| name | esp32-arduino-development |
| description | Complete ESP32 firmware development workflow using Arduino CLI (no Arduino IDE required).
Use this skill whenever the user mentions ESP32, Arduino CLI, compiling or flashing
firmware, serial monitoring, partition schemes (including custom partitions.csv files),
arduino-esp32 core version management, or managing ESP32 projects from VS Code.
Also trigger on errors like "Board not found", "Failed to connect to ESP32",
"Timed out waiting for packet header", partition overflow, or when the user asks
to create, configure, compile, upload, monitor, or check for core updates on an
ESP32 project. Supports all ESP32 variants (ESP32, S2, S3, C3, C6, H2, etc.) and
pins the arduino-esp32 core version per project for reproducible builds. This skill
replaces the Arduino IDE entirely and drives arduino-cli directly, reading project
settings from a project.json file in each project.
|
| author | esp32-arduino-development contributors |
| version | 1.2.4 |
| date | "2026-06-02T00:00:00.000Z" |
ESP32 Arduino Development (Arduino CLI Workflow)
Purpose
Replaces the Arduino IDE with a fully CLI-driven workflow for ESP32 firmware
development. Each project contains a project.json file that defines the board,
port, framework version, build options, and metadata. Claude reads this file and
drives arduino-cli directly to compile, flash, and monitor.
No custom build scripts required — the skill itself orchestrates arduino-cli.
An optional .vscode/tasks.json template is provided as a Claude-independent
fallback for builds from within VS Code.
Core actions
| Action | Trigger phrases | What it does |
|---|
| Compile | "compile", "build" | Builds the .ino with FQBN + options from project.json |
| Flash | "flash", "upload", "téléverse" | Compiles + uploads to ESP32 via serial |
| Monitor | "monitor", "serial", "moniteur" | Opens serial monitor at project baudrate |
| Create project | "new project", "create project.json" | Interactive project setup |
| Check updates | "check updates", "check framework updates" | Scans for arduino-esp32 core updates |
| Update skill | "update the skill", "pull skill updates" | Updates this skill itself from its Git remote |
| Diagnose | Automatic on error | Parses errors, proposes fixes |
project.json schema
{
"metadata": {
"name": "project-name",
"description": "What this firmware does",
"author": "Your name",
"client": "Internal / customer name (optional)",
"firmware_version": "see main .ino file"
},
"framework": {
"core": "esp32:esp32",
"core_version": "<X.Y.Z>"
},
"board": {
"fqbn": "esp32:esp32:esp32",
"cpu_freq_mhz": 240,
"flash_size": "4MB",
"flash_mode": "dio",
"flash_freq": "80",
"partition_scheme": "default"
},
"upload": {
"port": "auto",
"upload_speed": 921600,
"verify": true
},
"monitor": {
"baudrate": 115200
},
"build": {
"debug_level": "none",
"extra_flags": [],
"libraries_path": "",
"output_dir": "build",
"bin_name": ""
}
}
Field reference
| Field | Values | Notes |
|---|
framework.core | esp32:esp32 | Always this for ESP32 work |
framework.core_version | Exact version string in X.Y.Z format | Pinned — enforced before every compile. Never invent a value; always fetch from arduino-cli core search esp32 |
board.fqbn | See references/fqbn-reference.md | Fully-qualified board name |
board.cpu_freq_mhz | 80, 160, 240 | Lower = less power |
board.flash_size | 2MB, 4MB, 8MB, 16MB | Must match hardware |
board.flash_mode | qio, qout, dio, dout | dio is safest default |
board.flash_freq | 40, 80 | MHz |
board.partition_scheme | See references/partition-schemes.md; use "custom" when partitions.csv is present | When "custom", PartitionScheme= is omitted from the FQBN entirely and --build-property build.partitions=partitions is used instead — ensuring correct sketch-size reporting. Set to "custom" with user confirmation when partitions.csv is found and value ≠ "custom". |
upload.port | "auto", "COM5", "/dev/ttyUSB0" | auto = Claude detects |
upload.upload_speed | 115200, 230400, 460800, 921600 | Drop to 115200 if uploads are unreliable |
monitor.baudrate | Usually 115200 | Must match Serial.begin() in code |
build.debug_level | none, error, warn, info, debug, verbose | Core debug output |
build.extra_flags | Array of -D defines | e.g. ["-DDEBUG_MODE=1"] |
build.libraries_path | Relative or absolute path string | Optional. Path to a folder containing additional libraries (passed as --libraries to arduino-cli). Use a path relative to the project root, e.g. "../libraries". Omit or leave empty if all libraries are installed globally via arduino-cli lib install. |
build.output_dir | Relative or absolute path string | Optional. Where arduino-cli writes build artifacts (--output-dir). Recommended: "build" to keep the project root clean. Leave empty to use the OS temp dir. |
build.bin_name | Filename string | Optional. When set, the compiled .bin is copied from <output_dir>/<sketch>.ino.bin to <project_root>/<bin_name> after every successful compile. Use the Arduino IDE naming convention: <sketch>.ino.<board_short_name>.bin. Required if you have symlinks or update scripts pointing to a fixed filename. |
Framework version enforcement — CRITICAL BEHAVIOR
Before every compile, Claude MUST verify the installed core version matches
framework.core_version from project.json.
Never fabricate version numbers
Version numbers change over time. Claude must NEVER suggest, propose, or write a
core version from memory. Always obtain real version strings by running:
arduino-cli core update-index
arduino-cli core search esp32
arduino-cli core list
The values like <X.Y.Z> shown throughout this document are placeholders only —
replace them with the actual output of these commands every time.
Empty core_version field
If framework.core_version is empty or missing in an existing project.json:
- This means the project has not been fully initialized
- Run
arduino-cli core update-index then arduino-cli core search esp32
- Propose the latest stable version to the user
- Once confirmed, write it to
project.json and proceed
Check sequence
arduino-cli core list
Parse the output, find the line for esp32:esp32, compare with project.json.
Decision tree
Installed version matches pinned version:
- Proceed silently with compile
Installed version is different:
- STOP the compile
- Report clearly:
"Project pins esp32:esp32 v<pinned> but v<installed> is installed."
- Offer three options:
- Install the pinned version:
arduino-cli core install esp32:esp32@<pinned>
- Update
project.json to match the installed version (if user confirms this is intentional)
- Cancel the compile
Pinned version not installed at all:
- Report:
"esp32:esp32 v<pinned> is not installed."
- Propose:
arduino-cli core install esp32:esp32@<pinned>
- Wait for confirmation before running
NEVER auto-install or auto-update
Always require explicit user confirmation before changing the installed core version.
The pinned version is a contract — silently changing it would defeat the purpose.
Reading project.json — MANDATORY BEHAVIOR
Before every compile, flash, or any action that uses board options,
Claude MUST read project.json and copy its values LITERALLY into commands.
The whole point of project.json is to be the single source of truth for the
project. The exact string the user wrote is the exact string that must end up
on the arduino-cli command line. Substituting a value — even a "more standard
looking" one — defeats the purpose of pinning and has caused real compile
failures.
Non-negotiable rules
-
Never substitute values from memory or from examples in this skill.
If project.json says "flash_size": "4M", pass FlashSize=4M, never
FlashSize=4MB. If it says "flash_freq": "80", pass FlashFreq=80,
never FlashFreq=80MHz. The values in this skill's examples (like
FlashSize=4MB) are illustrative placeholders — the authority is always
the project's own project.json.
-
Never guess option keys either. Only pass FQBN options that are present
in project.json (via fqbn_extra_options or the structured fields).
Never add options like FlashMode=dio unless they are explicitly written
in the file.
-
Validate against the board's actual menu when uncertain. If the user
reports an invalid option 'X' error, run:
arduino-cli board details -b <fqbn>
to see what options the board actually exposes. Then present the mismatch
to the user: "project.json declares X=Y but board <fqbn> doesn't
expose X. Remove it from project.json, or omit from the FQBN this
time?" — let the user decide. Never silently drop or rename options.
-
If a mandatory field is missing from project.json, ASK the user —
don't fill in a plausible default silently.
Pre-compile read checklist
Before constructing the FQBN, the skill MUST have loaded and used:
| Field | Usage |
|---|
framework.core_version | Enforce installed core matches (see Framework version enforcement) |
board.fqbn | Base FQBN prefix |
board.fqbn_extra_options | Appended verbatim to FQBN (if present) |
board.cpu_freq_mhz | → CPUFreq=<value> |
board.flash_size | → FlashSize=<value> literally (e.g. 4M, not 4MB) |
board.flash_mode | → FlashMode=<value> only if the board exposes this option |
board.flash_freq | → FlashFreq=<value> literally |
board.partition_scheme | → PartitionScheme=<value>, OR omit entirely when "custom" |
build.debug_level | → DebugLevel=<value> |
build.libraries_path | → --libraries <resolved_path> |
build.output_dir | → --output-dir <path> |
build.extra_flags | → --build-property "compiler.cpp.extra_flags=..." |
upload.port, upload.upload_speed, upload.verify | For flash action |
monitor.baudrate | For monitor action |
If the skill constructs a command without having read project.json in the
current session, that is a bug — stop and read the file first.
Compilation logic
Step 0: Validate multi-file sketch prototypes
If the project uses multiple .ino files (001_.ino, 002_.ino, etc.), the main .ino file (same name as the directory) MUST declare explicit prototypes for void setup(); and void loop(); at file scope. Without them, arduino-cli's preprocessor auto-generates prototypes and corrupts the first #line directive into ##line, causing a stray '##' in program compile error (arduino-cli 1.5.0 + esp32:esp32 core 3.3.8).
Check: Before compiling any multi-file sketch, verify the main .ino contains both void setup(); and void loop(); as prototype declarations. If missing, add them — do NOT rely on the preprocessor to fill them in. This bug applies to ALL multi-file sketches, not just ESP32.
Example fix:
void setup();
void loop();
Alternative: prefer proper .h/.cpp structure instead of split .ino files. Create a single .ino for setup()/loop() plus separate .h/.cpp modules:
project/
├── project.ino # setup() + loop() only
├── config.h # Pins, defines
├── module.h/.cpp # One pair per module
└── project.json
This avoids the preprocessor bug entirely (no auto-prototype generation) and is cleaner for projects beyond trivial size. Note: the .ino file MUST still include setup() and loop() definitions — the preprocessor only looks for them in .ino files, not .cpp.
Step 1: Framework version check
Before every compile, verify the installed core version matches framework.core_version from project.json (see Framework version enforcement section above).
PITFALL — replace_all on patch: The string "Before every compile, Claude MUST verify the installed core version matches" appears in multiple sections of this document. Never use replace_all=true with this pattern — it will duplicate content across unrelated sections.
Step 2: Check for custom partition file
Before every compile, check if partitions.csv exists in the project root:
project/
├── project-name.ino
├── project.json
└── partitions.csv ← if present, overrides project.json partition_scheme
If partitions.csv IS present:
- Check
board.partition_scheme in project.json:
- If it is already
"custom": proceed silently (user already acknowledged this once)
- If it is anything else (including empty,
"default", etc.): stop and ask the user:
"I detected partitions.csv in this project but project.json says
partition_scheme = '<current_value>'. To ensure correct sketch-size
reporting, I should update it to "custom". Proceed with the update? (y/n)"
- If the user confirms: update
project.json → "partition_scheme": "custom", then continue
- If the user declines: continue with the current value but warn that sketch-size
percentages may be reported against the wrong partition size
- Build the compile command using the custom partition override (see Step 3)
If partitions.csv is NOT present:
- Read
board.partition_scheme from project.json
- If the field is missing or empty: ASK the user which scheme to use, then save the
answer back into
project.json so the question is asked only once per project
Step 3: Build the compile command
⚠️ The FQBN examples below contain placeholder values (FlashSize=4MB,
FlashMode=dio, etc.). These are illustrative only. The actual command must
use the values from project.json verbatim — see the "Reading project.json
— MANDATORY BEHAVIOR" section above. Not every option shown below applies to
every board: omit any option that is not present in project.json or not
exposed by the board's menu.
Standard compile (no custom partitions.csv):
arduino-cli compile \
--output-dir "<output_dir>" \
--fqbn "esp32:esp32:esp32:PartitionScheme=default,CPUFreq=240,FlashSize=4MB,FlashMode=dio,FlashFreq=80,DebugLevel=none" \
"<project_directory>"
With custom partitions.csv (i.e. partition_scheme is "custom"):
PartitionScheme= is intentionally omitted from the FQBN. Including it (even as
PartitionScheme=default) would cause arduino-cli to report sketch-size percentages
against the built-in scheme's app-partition size rather than the actual layout defined
in partitions.csv, producing misleading output.
arduino-cli compile \
--output-dir "<output_dir>" \
--fqbn "esp32:esp32:esp32:CPUFreq=240,FlashSize=4MB,FlashMode=dio,FlashFreq=80,DebugLevel=none" \
--build-property "build.custom_partitions=partitions" \
--build-property "build.partitions=partitions" \
"<project_directory>"
With a local libraries folder (build.libraries_path is set):
If build.libraries_path is non-empty, append --libraries "<resolved_path>" to
the compile command. The path is resolved relative to the project root:
arduino-cli compile \
--output-dir "<output_dir>" \
--fqbn "<fqbn>" \
--libraries "<project_directory>/../libraries" \
"<project_directory>"
Use a relative path in project.json (e.g. "../libraries") so the project works
across operating systems. Resolve it to an absolute path at compile time by joining
with the project directory.
With extra build flags:
If build.extra_flags is non-empty, append:
--build-property "compiler.cpp.extra_flags=-DDEBUG_MODE=1 -DFEATURE_X"
Step 4: Post-compile binary export
IMPORTANT — stale __DATE__/__TIME__: arduino-cli caches compiled .o
files globally under ~/.cache/arduino/sketches/. If a source file containing
__DATE__ or __TIME__ (e.g. globals.cpp with compileDate) hasn't changed,
the cached .o is reused — producing a binary with an old compile date even after
a clean rm -rf build. Always run both clean steps before a recompile where
the date matters:
rm -rf ~/.cache/arduino/sketches/*
rm -rf <project>/build
arduino-cli compile ...
Verify the date landed in the binary with:
strings <project>/<bin_name> | grep "$(date +%b) $(date +%d) $(date +%Y)"
If build.bin_name is non-empty, copy the compiled application firmware binary to the project root after a successful compile:
source: <output_dir>/<fqbn_short>/<sketch>.ino.bin (custom partitions; OTA/app image)
source: <output_dir>/<sketch>.ino.bin (built-in partitions; OTA/app image)
destination: <project_root>/<bin_name>
CRITICAL — OTA uses the app .bin, not .merged.bin: *.merged.bin is a full flash image (often the whole flash size, e.g. 4 MB) containing bootloader + partitions + app + data layout. It is for serial/full-flash workflows, not OTA. Exporting *.merged.bin to the fixed firmware filename will make OTA fail with "not enough space" even when the application fits the OTA slot.
Never use find build -name "*.merged.bin" as the export fallback for bin_name. If the fixed filename is used by OTA/server symlinks, copying a merged image there creates a broken OTA artifact. For OTA-ready artifacts, the source file must end with .ino.bin and must NOT end with .merged.bin.
With custom partitions, arduino-cli writes both app and merged binaries under a subdirectory named after the FQBN short form (e.g. build/esp32.esp32.featheresp32/0503.ino.bin and 0503.ino.merged.bin). Always export the app binary for OTA and fixed firmware links. Use find build -name "*.ino.bin" to locate the app image if unsure.
Example — bin_name is "0503.ino.feather_esp32.bin", output_dir is "build":
cp build/esp32.esp32.featheresp32/0503.ino.bin 0503.ino.feather_esp32.bin
Robust export pattern:
APP_BIN=$(find build -path "*/<sketch>.ino.bin" ! -name "*.merged.bin" | head -n 1)
test -n "$APP_BIN"
cp "$APP_BIN" "<bin_name>"
After exporting, verify size against the OTA slot from partitions.csv:
stat -c %s 0503.ino.feather_esp32.bin
This keeps the project root clean (build artifacts stay in build/) while maintaining a fixed OTA-ready filename that symlinks or update scripts can always reference.
In VS Code tasks.json, implement this as a separate _esp32_export_bin task
and chain it via "dependsOn" + "dependsOrder": "sequence" on the main compile
task (see templates/.vscode/tasks.json).
Step 5: Report results
Always show:
- Sketch size and percentage of available program storage
- Global variables size and percentage of dynamic memory
- Warnings (if any)
- Path to generated
.bin file
If compile fails, capture the error output and proceed to diagnostic mode (see
Troubleshooting section).
Flash logic
Port detection
If upload.port is "auto" or missing:
arduino-cli board list
Parse output. Scenarios:
- One ESP32 detected → use it silently, mention the port in confirmation
- Multiple ESP32 detected → ask user which one (offer tappable options)
- No ESP32 detected → report: "No ESP32 detected. Check USB cable and drivers (CP210x or CH340 typically required)."
Upload command
arduino-cli upload \
--fqbn "<fqbn>" \
-p <port> \
--input-dir <build_dir>
Add --verify if upload.verify is true in project.json.
Upload failures
If upload fails with "Failed to connect to ESP32" or "Timed out waiting for packet header":
- Tell user: "Hold BOOT, tap EN (reset), release BOOT, then confirm to retry."
- Wait for confirmation before retrying
- If still failing after retry, suggest lowering
upload.upload_speed to 115200 in project.json
Monitor logic
arduino-cli monitor -p <port> --config baudrate=<baudrate>
Notes:
- If a flash just happened, wait ~1 second before opening monitor (device needs to boot)
- Warn the user that monitor blocks the terminal until they stop it (Ctrl+C)
- If port from flash is known, reuse it without re-detecting
Check framework updates (on-demand)
Trigger: user asks "check updates", "check framework updates", "any core updates",
or similar.
Sequence
arduino-cli core update-index
arduino-cli core outdated
arduino-cli core search esp32
Report format
Present results clearly using the actual values returned by arduino-cli:
Currently installed: esp32:esp32 v<installed>
Latest available: esp32:esp32 v<latest>
Recent versions available:
<version> (latest)
<version>
<version>
<version> ← currently installed (matches project.json pin)
<version>
...
Important: never fabricate version numbers. Always run the commands and use
their real output — version numbers shown above are placeholders only.
Then offer next actions:
- Install a newer version (does not change existing projects, since they pin their own)
- Update a specific project's pin (requires editing
project.json + testing)
- Nothing, just informational
NEVER auto-install or modify any project.json during a check.
Creating a new project (interactive)
When the user asks to create a new ESP32 project or project.json, ask these
questions in order using tappable options when possible:
- Project name (used for directory and
.ino filename)
- Board model — top picks: ESP32 Dev Module, ESP32-S3, ESP32-C3, ESP32-C6, NodeMCU-32S, ESP32-CAM, custom FQBN
- Flash size — 4MB (default), 8MB, 16MB, 2MB
- Framework version — determine the default by running
arduino-cli core update-index then arduino-cli core search esp32 to find the latest stable release of esp32:esp32. Propose it as default with the line "Dernière version stable: X.Y.Z — utiliser celle-là ?" and allow override. Once chosen, this value is frozen in project.json and never auto-changes.
- Partition scheme — show options from
references/partition-schemes.md, or "I'll add a custom partitions.csv later"
- Monitor baudrate — default 115200, confirm
- Metadata — description, client (optional but useful)
Generate:
new-project/
├── new-project.ino ← skeleton with firmware version comment
├── project.json ← populated from answers
├── .vscode/
│ └── tasks.json ← copied from templates/.vscode/tasks.json
└── .gitignore ← standard Arduino/build ignores
Do NOT create partitions.csv automatically — only mention that it can be added
later if the default scheme is insufficient.
Debugging approach — CRITICAL MINDSET
When a compile error occurs:
-
Start from your own code, not external assumptions. The most likely cause is a bug or missing declaration in your source files — not a tool bug, not a library issue, not a preprocessor glitch. Always assume the fault is in your code until proven otherwise.
-
Reproduce minimally. Before blaming the toolchain, strip the project down to the smallest reproduction case. Create a clean directory with only the files needed to trigger the error. If the error disappears during reduction, the cause is in whatever you removed.
-
Trace data flow. Read the generated intermediate files (.ino.cpp, .ino.cpp.merged in ~/.cache/arduino/sketches/<hash>/sketch/). Compare the correct merged output against the corrupted compiler input. The diff will show you exactly where the problem starts.
-
One hypothesis at a time. Change one variable between tests. "Maybe it's the comment block" → remove it and test. "Maybe it's CLAUDE.md" → remove it and test. Never change three things and retry.
-
If stuck, load the superpowers skill and follow its systematic-debugging reference — four phases: root cause investigation, pattern analysis, hypothesis + testing, fix + verification. Do not skip to "it's a tool bug" without completing phase 1.
Common trap: Arduino-cli does have bugs, but 9 times out of 10 the issue is in the sketch — a missing prototype, a rogue #line directive, or an unexpected file being scanned by the preprocessor. Verify your own code exhaustively before escalating to tool assumptions.
ErabliTEK project conventions
Martin's ESP32 firmware projects follow this layout:
~/Desktop/Dropbox/ErabliTEK/Martin/Arduino/
├── libraries/ ← ALL shared libraries (80+ libs: MB7389, AsyncIoTManager, etc.)
│ ├── MB7389/
│ ├── ErabliTEKAnalogReading/
│ ├── AsyncIoTManager/
│ ├── async-mqtt-client-develop/
│ ├── AsyncTCP-3.4.8/
│ └── ...
├── 0503/ ← NB300 liquid level sensor
├── 0603/ ← VAC300 vacuum pump controller
└── ...
Key rules:
- Firmware lives in Dropbox, NEVER in the EraIoT Django repo.
Do not search for .ino/.cpp files inside
~/EraIoT/.
- Libraries are shared from
../libraries relative to each project.
project.json → build.libraries_path: "../libraries" — do NOT copy libs one-by-one to ~/Arduino/libraries/.
Use --libraries "../libraries" in the arduino-cli command.
- Custom
partitions.csv is only for devices that need SPIFFS (e.g. VAC300).
NB300 uses NVS (Preferences), not SPIFFS — default partition scheme is correct.
Do NOT copy VAC300's partitions.csv to NB300 without explicit user confirmation.
- All devices share the same libraries folder — no per-project copies.
| Error substring | Likely cause | Action |
|---|
stray '##' in program / ##line | Multi-file sketch missing setup()/loop() prototypes in main .ino | Add void setup(); void loop(); to the main .ino prototypes section. Or migrate to .h/.cpp structure (see Compilation Logic → Step 0). |
Invalid FQBN: ... invalid option 'X' | project.json declares an option the board doesn't expose (common: FlashMode on featheresp32, DebugLevel on some boards) | Run arduino-cli board details -b <fqbn> to list real options. Then ASK the user: remove X from project.json, or omit from FQBN for this build only? Never silently drop. |
Failed to connect to ESP32 | Device not in download mode | Instruct BOOT+EN sequence, retry after user confirms |
Invalid head of packet | Port busy or baudrate mismatch | Close monitors, lower upload_speed |
No board selected | Core missing or wrong FQBN | Verify arduino-cli core list, check FQBN |
esp32:esp32 not installed | Core not installed | Run arduino-cli core install esp32:esp32@<version> |
Library <X>.h: No such file | Missing lib | For ErabliTEK projects, check ~/Desktop/Dropbox/ErabliTEK/Martin/Arduino/libraries/ first — most deps are there. Copy needed libraries to ~/Arduino/libraries/ or use --libraries flag. |
AsyncMqttClient.h: No such file | Missing async-mqtt-client | cp -r ~/Desktop/Dropbox/ErabliTEK/Martin/Arduino/libraries/async-mqtt-client-develop ~/Arduino/libraries/ |
AsyncTCP.h: No such file | Missing AsyncTCP | cp -r ~/Desktop/Dropbox/ErabliTEK/Martin/Arduino/libraries/AsyncTCP-* ~/Arduino/libraries/AsyncTCP |
MB7389.h: No such file / ErabliTEKAnalogReading.h | Missing ErabliTEK sensor libs | cp -r ~/Desktop/Dropbox/ErabliTEK/Martin/Arduino/libraries/{MB7389,ErabliTEKAnalogReading} ~/Arduino/libraries/ |
region \iram0_0_seg' overflowed` | Code too large for RAM | Review code size, reduce features |
region \flash`/`app0` overflowed` | Partition too small | Suggest huge_app scheme or custom partitions.csv |
stray '##' in program / ##line in merged .cpp | Missing setup/loop prototypes in multi-file sketch | Add void setup(); void loop(); to main .ino. See references/troubleshooting.md |
Permission denied on /dev/ttyUSB* | Linux dialout group missing | sudo usermod -a -G dialout $USER then re-login |
xtensa-esp32-elf-gcc: not found | Toolchain corrupt | Reinstall core with arduino-cli core install esp32:esp32@<version> --force |
__DATE__ / __TIME__ stale after recompile | Arduino CLI global cache (~/.cache/arduino/sketches/) reuses old .o files when source hasn't changed | rm -rf ~/.cache/arduino/sketches/* build && arduino-cli compile ... — see Step 4 |
expected constructor ... before 'pour' | */ inside a /* */ comment prematurely closes the block | Reword: pending/sendToCloud instead of pending*/sendToCloud() |
cp: cannot stat '.../tools/partitions/partitions.csv' | custom_build_properties references partitions but no partitions.csv exists in project | Remove custom_build_properties from project.json — this field was likely copy-pasted from a SPIFFS project (e.g. VAC300). NB300 uses NVS/Preferences and does NOT need custom partitions. Check the project's storage type first. |
See references/troubleshooting.md for the detailed version.
Global library management
Libraries are NOT declared in project.json (user preference). They are managed
globally via:
arduino-cli lib install "LibraryName"
arduino-cli lib list
arduino-cli lib update
When Claude sees a compile error about a missing library, it should propose the
install command but NEVER auto-run it without user confirmation.
Prerequisite check (run once per session silently)
Step 1 — Check if arduino-cli is installed
arduino-cli version
If found and version >= 1.0: proceed silently.
If not found: do NOT fail silently — offer to install it (see Step 2).
Step 2 — Install arduino-cli (with user confirmation)
Detect the OS, present the install plan, and wait for explicit confirmation
before running anything.
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | BINDIR=/usr/local/bin sh
/usr/local/bin is already in PATH on most systems. If not, inform the user
and provide the exact export PATH line to add to their shell profile.
Windows (PowerShell — no sh required):
$dest = "$env:LOCALAPPDATA\Programs\arduino-cli"
New-Item -ItemType Directory -Force -Path $dest | Out-Null
Invoke-WebRequest -Uri "https://downloads.arduino.cc/arduino-cli/arduino-cli_latest_Windows_64bit.zip" -OutFile "$env:TEMP\arduino-cli.zip"
Expand-Archive -Path "$env:TEMP\arduino-cli.zip" -DestinationPath $dest -Force
[Environment]::SetEnvironmentVariable("Path", [Environment]::GetEnvironmentVariable("Path","User") + ";$dest", "User")
After install, inform the user they must restart their terminal (or VS Code)
for the PATH change to take effect.
Step 3 — Verify
arduino-cli version
If this still fails after install, report the exact output and help the user
diagnose (PATH not refreshed, download failed, architecture mismatch, etc.).
Step 4 — Core check
arduino-cli core list
If the esp32 core is missing, offer to install it (requires separate
confirmation — see Framework version enforcement section).
Skill self-update — CRITICAL BEHAVIOR
This skill is maintained in a Git repository. It can check for and apply its
own updates, but ONLY under strict safety rules.
Session-start silent check (hybrid mode)
At the beginning of a new ESP32-related conversation, run this check ONCE per
session. Do it silently — the user should not see tool output unless something
needs their attention.
Locate the skill directory: Typically one of:
- Linux / macOS:
~/.claude/skills/esp32-arduino-development/
- Windows:
%USERPROFILE%\.claude\skills\esp32-arduino-development\
If the directory is not a Git repository (no .git/ inside), skip the check
entirely — the skill was installed via a method other than git clone.
Check sequence:
cd <skill_dir>
git fetch --quiet
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse @{u} 2>/dev/null)
BASE=$(git merge-base @ @{u} 2>/dev/null)
Interpretation:
LOCAL == REMOTE → up to date, say nothing, continue
LOCAL == BASE (local is behind remote) → updates are available, notify user
REMOTE == BASE (local is ahead of remote) → user has unpushed commits, say nothing
- Neither matches → diverged history, say nothing (user handles it manually)
If updates are available, add a short, ONE-LINE note at the start of the
first response:
Note: X new commit(s) available for the esp32-arduino-development skill. Say "update the skill" to apply them.
Then answer the user's actual request as normal. Never delay the response to
perform the update check — if git fetch hangs or fails (no network, etc.),
silently skip and move on.
Explicit update command
When the user says "update the skill", "pull skill updates", "update the esp32 skill",
or similar, run the update procedure.
Step 1 — Safety check: uncommitted local changes
cd <skill_dir>
git status --porcelain
If this returns ANY output (even a single modified file), STOP. Do not pull.
Report exactly:
Il y a des modifications locales non-commitées dans le skill. Je ne peux pas faire git pull sans risquer de les écraser.
Options:
- Commit tes changements:
git add . && git commit -m "votre message"
- Ou stash-les temporairement:
git stash
- Ou si les changements ne sont pas importants:
git checkout . (⚠️ perte définitive)
Dis-moi laquelle quand tu es prêt et je retenterai l'update.
(Respond in English if the conversation is in English, adjust accordingly.)
Step 2 — Check for divergence
git fetch --quiet
LOCAL=$(git rev-parse @)
REMOTE=$(git rev-parse @{u})
BASE=$(git merge-base @ @{u})
If REMOTE == BASE (user's branch is ahead of remote), say:
Ton skill local est en avance sur le remote — rien à pull. Tu as peut-être des commits à push.
If LOCAL == REMOTE, say:
Le skill est déjà à jour.
If diverged (neither matches), STOP and warn the user — they need to resolve
the divergence manually.
Step 3 — Pull
Only if safe (working tree clean AND local is strictly behind remote):
git pull --ff-only
Using --ff-only prevents merge commits — if a fast-forward is not possible,
the pull fails cleanly and the user is told to resolve manually.
Step 4 — Report
Show the user a short summary of what changed:
git log --oneline <OLD_LOCAL>..HEAD
Then remind them: "Redémarre Claude Code pour charger la nouvelle version du skill."
(Claude Code needs a session restart to re-read SKILL.md content.)
NEVER do the following
- Never run
git pull silently without user confirmation
- Never run
git reset, git checkout ., or any destructive command automatically
- Never modify files in the skill directory outside of the formal update procedure
- Never push from within the skill directory based on automatic behavior
Versioning and changelog
This skill follows Semantic Versioning. The current
version is in the frontmatter at the top of this file (version: field).
Files to keep in sync when modifying the skill:
SKILL.md frontmatter version: and date: fields
CHANGELOG.md at the repo root
- Git tag matching the version (at release time)
See CONTRIBUTING.md for detailed versioning rules, changelog discipline, and
the release process.
When asked to make changes to this skill:
- Apply the requested changes
- Determine the version bump level (patch / minor / major) per SemVer rules
- Update
version: in the frontmatter
- Add an entry under
## [Unreleased] in CHANGELOG.md with the correct category (Added / Changed / Fixed / etc.)
- Do NOT create a release tag unless the user explicitly asks ("release this as vX.Y.Z")
When the user asks "what version is the skill" or "what changed recently",
read CHANGELOG.md and summarize the most recent entries.
File references
CHANGELOG.md — version history in Keep a Changelog format
CONTRIBUTING.md — versioning rules (SemVer) and release process
LICENSE — MIT license
templates/project.json — blank template with all fields
templates/partitions.csv.example — example of a custom 4MB partition with OTA slots
templates/.vscode/tasks.json — VS Code tasks that call arduino-cli directly (fallback)
templates/gitignore.txt — standard .gitignore for ESP32 Arduino projects
references/fqbn-reference.md — full list of ESP32 FQBN strings
references/partition-schemes.md — all built-in partition schemes with sizes
references/troubleshooting.md — detailed error diagnosis
references/ino-preprocessor-hashhash.md — ##line preprocessor bug reproduction and fix
references/installation.md — arduino-cli installation per OS
Style and tone
- If the conversation is in French, respond in French. If in English, respond in English
- Be concise and technical — avoid hand-holding
- Never auto-run destructive or state-changing commands without confirmation:
- Compile: OK to auto-run once project.json is set up
- Flash: OK to auto-run once project.json is set up
- Monitor: OK to auto-run
lib install, core install, core update: always require confirmation
- When two configurations are plausible, ask rather than guess
- Never modify
project.json without telling the user exactly what changed and why