Build and development environment¶
Toolchain and dependencies¶
Pinned ESP-IDF v6.1, tag commit
fff9895c82d744c7237be8847347bdd1b07c6643, pinned from
the v6.1 release
and the v6.1 ESP32 documentation.
Use ESP-IDF Installation Manager (EIM) to install v6.1 with toolchains for the
required esp32 and/or esp32s3 targets;
select that installation in the ESP-IDF VS Code extension. Build from its activated
ESP-IDF terminal. No project-local SDK, PlatformIO, Arduino, or M5Unified is needed.
.idf-version records the SDK release, the component manifest constrains IDF to
6.1.0, and CMake checks the exact release string before compiling. A compile-time
check also rejects other semantic IDF versions.
Direct managed dependencies are pinned in main/idf_component.yml:
| Component | Version | Purpose |
|---|---|---|
Espressif led_strip |
3.0.1 | Boot matrix output |
Espressif button |
4.2.1 | GPIO debouncing and click events |
Espressif cjson |
1.7.19~2 | Bounded settings/status JSON |
Espressif esp-dsp |
1.8.2 | Optional PDM spectral features |
Espressif ble_conn_mgr |
1.3.4 | Single BLE host and secured CoC |
Espressif ble_services |
1.5.0~1 | Standard Object Transfer Service |
Commit both dependencies.lock (ESP32) and dependencies.esp32s3.lock: they record
resolved direct/transitive versions, hashes and target, following
Espressif's recommendation.
BLE local control, catalogue transfer and authenticated OTA share one IDF NimBLE
peripheral host owned by conn_mgr; there are no application scan calls. LE Secure
Connections with numeric comparison and bonding replaces the generated verifier
requirement. No Arduino or RemoteXY component remains. The iOS client in ios/ pins
SwiftProtobuf and uses native OS bonding; see the app notes.
Do not edit managed manifests or remove transitive dependencies manually. managed_components/, build output and
generated sdkconfig remain ignored. Review the lock during deliberate upgrades.
Upgrades require updating the SDK pin, manifest, compile guard and documentation together, reviewing the official migration guide, rebuilding affected targets and LED backends, and repeating hardware checks. See BLE integration; there is no longer an upstream OTA profile patch to maintain.
led_strip 3.0.1's SPI source uses heap-capability APIs without directly including
esp_heap_caps.h, which fails to compile under IDF v6.1. Top-level CMake force-includes
that header for only led_strip_spi_dev.c. The managed component is unmodified.
No warnings are suppressed; the workaround covers a vendor source compiled even
when ear output uses the project-owned packed SPI DMA backend or RMT. Recheck/remove this
small workaround when upgrading the component.
ble_conn_mgr 1.3.4 also references a hidden bond-capacity Kconfig symbol in an
unused whitelist helper when SMP is disabled. Root CMake defines zero capacity
only for that component in no-SMP builds; managed sources remain unmodified.
The control transport uses the pinned IDF private transport ABI and two narrowly
scoped registration/store hooks; see the integration contract.
Build, flash, monitor¶
From the EIM / VS Code v6.1 ESP-IDF terminal:
# Activate your EIM-managed ESP-IDF v6.1 environment; run from the repository root.
idf.py --version
idf.py --preset normal build
idf.py --version must report ESP-IDF v6.1. Select a preset for the board; use
--preset atoms3r-normal for the AtomS3R v1 upgrade,
--preset s3-normal for AtomS3 Lite, --preset custom-s3-normal for custom legacy S3 boards, and
--preset v2-normal for the experimental WROOM-1-N16R8 integration build, or
--preset v2-8mb for v2 with 8 MB flash, PDM enabled, contact capture/PSRAM disabled
by default. V2
currently keeps lighting black until the source/current service
has fresh status and an explicitly qualified set of operating limits/reserves;
the default v2 configuration deliberately leaves those limits unqualified. Also
see its status and limitations.
Presets isolate
configuration/build output without set-target or regenerating an existing config.
The project defaults to classic ESP32 at 240 MHz, DIO flash at 40 MHz, 4 MB flash, and IDF's built-in
Two large size OTA partitions layout (CONFIG_PARTITION_TABLE_TWO_OTA_LARGE).
It provides ota_0 and ota_1, 1700 KiB each, plus 24 KiB NVS, 8 KiB OTA metadata,
and 4 KiB PHY data. Legacy S3 profiles assume at least 8 MB and use a custom layout with
two 3072 KiB OTA slots plus a reserved 1920 KiB recovery test-app slot. The recovery
image is built separately with s3-recovery, custom-s3-recovery, v2-recovery or
v2-8mb-recovery. Normal native flash/OTA plans exclude it. The S3 web flasher
uses recovery-only USB bootstraps with the native five-second GPIO hold enabled;
its optional preserve plan keeps existing A/B, OTA selection and NVS. See the flash layout and migration guide.
NVS keeps its original offset and size. With erased OTA metadata,
the bootloader selects ota_0; future OTA writes the inactive slot. No separate factory
image is needed, and USB remains the recovery path. Native rollback is enabled. On a
pending OTA boot, input/render health must pass within five seconds; missing IMU input
alone is an allowed degraded boot. Flash the
whole project over USB once to install this bootloader and partition table before
using app-only OTA. No secure-boot/anti-rollback eFuses are programmed.
The ESP32 CPU frequency is selected in sdkconfig.defaults.esp32, which IDF loads
alongside shared defaults. Existing sdkconfig* files retain their choice: select
Component config → ESP System Settings → CPU
frequency → 240 MHz in each preset's menuconfig. Do not delete those files just to
change clocks; they can contain private AP/control credentials. With dynamic frequency
scaling disabled, IDF's boot log should report cpu freq: 240000000 Hz.
240 MHz is the PICO-D4's supported maximum, rather than an overclock. It gives CPU work more headroom for rendering and radio traffic; it does not shorten WS2812 transmission time or guarantee 500 Hz output during Wi-Fi association. Expect increased controller power consumption and check mounted temperature/battery runtime during hardware tuning. This CPU setting does not change flash/LED clocks, brightness/current limits or dither fallback.
Find the Atom's USB serial device (on macOS, typically /dev/cu.usbserial-*):
ls /dev/cu.*
idf.py --preset normal -p /dev/cu.YOUR_ATOM_PORT -b 1500000 flash monitor
Use the same preset you built and replace the port with the actual device (for
example /dev/ttyUSB0 on Linux). Don't select an unrelated serial device.
Flashing replaces firmware. Monitor runs at 115200;
exit with Ctrl-]. For monitoring without flashing:
idf.py --preset normal -p /dev/cu.YOUR_ATOM_PORT monitor
Use 115200 as a fallback if the USB adapter can't sustain 1500000 baud.
Pass -b explicitly: the preset's flash baud
setting controls the VS Code extension, not idf.py's own default.
Boot logs include firmware/build/IDF versions, reset reason, both LED GPIOs, counts/directions/colour orders, build-config source, brightness/current budget, and mode. The asynchronous IMU task logs readiness for the selected chip and sensor failures; its driver rejects an unexpected chip identity.
CMake presets and formatting¶
In VS Code, run ESP-IDF: Select Project Configuration and choose normal,
s3-normal, custom-s3-normal or experimental v2-normal, then build using the extension.
Firmware presets build under build/firmware/<preset>, retaining their existing
root-level SDK configuration files.
See hardware profiles for board defaults and configuration.
Existing menuconfig changes remain in those files; selecting a preset doesn't reset
them to defaults. LED diagnostics are available in the ordinary firmware.
For a terminal build, activate IDF v6.1 first and run from the project root:
cmake --preset normal
cmake --build --preset normal
ESP-IDF v6.1 selects the first (normal) preset when none is specified, so plain
idf.py build uses the same nested directory. Select S3/custom presets explicitly.
Build outputs are grouped by type:
build/
firmware/<preset>/
tests/host/
tests/hardware-profiles/<target>/<case>/
artifacts/legacy/<old-build-directory>/
The artifacts directory preserves firmware/NVS backups, logs, screenshots and scratch
baselines from older builds. CMake caches contain absolute paths; configure/rebuild
in the new directories instead of moving a configured build tree. Root sdkconfig*
and private credentials stay outside build directories. Swift/Gradle caches and Xcode
DerivedData retain their native project locations.
The independent host-test project has its own presets under tests/. Resolve managed
dependencies with one idf.py reconfigure first; host compilation then needs only
CMake, native C/C++ compilers and Python, without an activated IDF environment.
The spectral tests compile the managed ESP-DSP ANSI FFT/window sources unchanged
and compare them with a separate Python direct DFT:
cd tests # from the repository root
cmake --preset host-tests
cmake --build --preset host-tests
ctest --preset host-tests
Commit CMakePresets.json and tests/CMakePresets.json. Personal overrides belong in
ignored CMakeUserPresets.json files. Point clangd at the selected preset's build
directory, for example build/firmware/normal/compile_commands.json for the original Atom.
The live .vscode/settings.json is local and ignored. ESP-IDF writes installation
paths into it, and VS Code has no native settings.local.json overlay. Keep the
extension's generated configuration local rather than managing partial-file commits
or Git index flags. Portable preferences live in .vscode/settings.example.json.
For a new checkout, copy the example once (without replacing existing local settings):
cp -n .vscode/settings.example.json .vscode/settings.json
The example includes clangd formatting, format-on-save for C/C++, and eight-space Kconfig indentation. Select the v6.1 installation with ESP-IDF: Select Current ESP-IDF Version; EIM discovers the local installations. Add these machine-specific clangd settings to the ignored file, using your own installation paths:
{
"clangd.path": "/path/to/esp-clangd/bin/clangd",
"clangd.arguments": [
"--background-index",
"--resource-dir=/path/to/esp-clang/lib/clang/21",
"--query-driver=/path/to/xtensa-esp-elf/bin/xtensa-esp32-elf-*",
"--compile-commands-dir=/absolute/path/to/espurr/build/firmware/normal"
]
}
The EIM-installed esp-clangd package lacks Clang's builtin headers. Pointing
--resource-dir at the matching esp-clang installation (lib/clang/21) fixes missing
standard headers such as float.h; --query-driver supplies the Xtensa GCC system
includes. Update local paths when upgrading tools and run clangd: Restart language
server after changing them. The .clangd file removes GCC-specific -f*/-m* flags
only for editor analysis; it doesn't affect firmware builds.
The example limits Swift's SourceKit-LSP to Swift and Objective-C languages for the
iOS app. Its default also claims C/C++ files, starting Apple's clangd alongside
esp-clangd and producing spurious ESP32 header errors such as missing
sys/features.h. Keep c and cpp out of swift.sourcekit-lsp.supported-languages
in local workspace settings. Run Swift: Restart LSP Server after changing
this setting if existing diagnostics remain.
.clang-format uses LLVM-based style: attached braces,
120 columns, four-space indentation, aligned declarations/assignments, and regrouped
includes (generated config, project headers, standard library, then IDF/components).
Settings apply to both C and C++; Standard: Latest is a formatter parsing setting,
not a change to compiler language flags. SeparateDefinitionBlocks: Always separates
definitions; a blank line after other }; terminators remains a style convention.
VS Code's C/C++ default formatter is clangd, which
reads this file for Format Document (Shift+Option+F on macOS) and format-on-save
when using the example settings. Use IDF's bundled clang-format 21.1.3 as the formatting
reference, matching VS Code's clangd. Homebrew 23.1.2 accepts the configuration but
produces some different alignment. From the activated IDF terminal, format project-owned code:
clang-format --version
rg --files main components preview tests -g '*.c' -g '*.cpp' -g '*.h' \
-g '!**/vendor/**' -g '!**/Generated/**' -0 | xargs -0 clang-format --style=file -i