Everything needed to build, flash, test and maintain the firmware locally. CI runs the same scripts, so what works here works there.
| Tool | Version | Notes |
|---|---|---|
| ESP-IDF | v6.1 | The version CI pins (.github/actions/setup-esp-idf). Install with the Espressif installer (EIM); the scripts and CMake presets find that install on their own. |
| Python | 3.13 (.python-version) |
For the repository scripts. ESP-IDF brings its own venv. |
| Node | 26 (frontend/.nvmrc) |
Captive-portal frontend. |
| pnpm | 12 (packageManager in frontend/package.json) |
corepack enable installs it. |
| flatc | matches the flatbuffers submodule | Only when editing schemas; scripts/fetch_flatc.py downloads it. |
Clone with submodules. The flatbuffers runtime is a submodule and the build fails without it:
git clone --recurse-submodules https://github.com/OpenShock/Firmware.git
# or, in an existing clone
git submodule update --initscripts/build.py finds an ESP-IDF installed by the Espressif installer (EIM) on its own: it reads
the installer's eim_idf.json and applies that install's activation environment before running
idf.py. So the build script, and the VS Code tasks that call it, work from a plain terminal.
Direct idf.py calls, or an ESP-IDF installed some other way, still need the environment in the
shell. The activation only lasts for that shell. Two helpers do the same discovery for a shell:
. scripts/idfenv.sh # bash, zsh, sh on Linux and macOS
call scripts\idfenv.bat # cmd.exeOn Windows use PowerShell or cmd. ESP-IDF refuses MSYS shells such as Git Bash, and the installer's environment is in Windows path form, which those shells cannot use.
Windows, PowerShell. The installer writes a profile under the tools directory. Use it rather than
the generic export.ps1, which looks for a venv under your home folder and fails:
. C:\Espressif\tools\Microsoft.v6.1.PowerShell_profile.ps1Linux and macOS:
. ~/esp/v6.1/esp-idf/export.sh # path depends on where you installed itESP-IDF prints non-ASCII glyphs. Python on Windows encodes console output with the legacy code
page unless UTF-8 mode is on, and crashes with UnicodeEncodeError otherwise. scripts/build.py
sets PYTHONUTF8=1 for the idf.py it launches. For direct idf.py calls, set it once for your
user:
[Environment]::SetEnvironmentVariable("PYTHONUTF8", "1", "User")The project's scripts, both the ones you run and the ones the build runs (gen_staticfs.py,
stamp_board.py), need requirements.txt. Install it into a venv of your own, not into ESP-IDF's:
requirements.txt pins a newer cryptography than ESP-IDF's constraints allow, and installing it
there breaks the ESP-IDF activation.
python -m venv .venv
. .venv/bin/activate # PowerShell or cmd: .venv\Scripts\activate
pip install --require-hashes -r requirements.txtRun scripts/build.py from that venv: it checks that its own interpreter has every pin of
requirements.txt before idf.py starts, then hands the interpreter to CMake as OPENSHOCK_PYTHON
for the scripts the build runs. A bare cmake --preset <board> (CLion, the command line) uses
.venv/ in the repository when it exists, or the interpreter you set OPENSHOCK_PYTHON to, and
runs the same check at configure time. Nothing installs packages for you: a missing pin fails fast
with the list.
scripts/build.py is the build driver. It applies the board's defaults on top of
sdkconfig.defaults, builds into build/<board>/, and stamps the board descriptor into the app
image after linking. Anything after the board name is passed to idf.py.
python scripts/build.py OpenShock-Core-V2 # full build, incl. the www image
python scripts/build.py OpenShock-Core-V2 app # app only
python scripts/build.py OpenShock-Core-V2 flash -p COM4 # build and flash
python scripts/build.py OpenShock-Core-V2 monitor -p COM4 # serial monitor
python scripts/build.py OpenShock-Core-V2 fullclean # wipe build/<board>
python scripts/build.py OpenShock-Core-V2 menuconfig # tweak Kconfig for this build dirBoard names are the file names under boards/ without .defaults.
OPENSHOCK_BUILD_DIR overrides the build directory.
Boards on the same chip with the same Kconfig compile identical objects. CI compiles once per variant and stamps each board into a copy afterwards:
python scripts/build.py --variant esp32s3-6832dbd5 app bootloader partition-table
python scripts/build.py --variant ? # any unknown id lists the variants and their boardsboards/<board>.defaults holds two kinds of lines:
CONFIG_*Kconfig lines: chip target, flash size, console, PSRAM. Applied on top ofsdkconfig.defaultsat configure time.OPENSHOCK_*pin lines: not Kconfig. Stripped from the fragment and stamped into the image byscripts/stamp_board.py, so the same compiled objects serve every board of a chip.
Shared config lives in sdkconfig.defaults, including the 4 MB OTA partition table
(partitions/ota_4mb.csv) every board uses. sdkconfig and build/ are generated and
gitignored. If a sdkconfig is stale after a defaults change, delete it or run fullclean.
Each component defines its own CONFIG_OPENSHOCK_* options in components/<name>/Kconfig; they
show up in menuconfig under Component config, in the "OpenShock ..." menus. A component may
read only its own options and those of the components it requires, which
scripts/check_kconfig_ownership.py checks in CI. An option more than one component reads goes
in the lowest component they all depend on: the default domains and hostname, and every task's
stack and priority, live in components/common/Kconfig, where src/TaskUtils.cpp checks the
priority order between the tasks.
python scripts/stamp_board.py read --image build/OpenShock-Core-V2/firmware.binThe repository carries its own VS Code setup in .vscode/. Open the folder, install the
recommended extensions when prompted, and:
- Tasks (Terminal → Run Task, or the default build task): build, build app only, flash,
monitor, menuconfig, full clean and update managed components, each asking for a board and,
where relevant, a serial port. They call
scripts/build.py, which activates ESP-IDF itself. - IntelliSense comes from clangd via
.clangd, which readsbuild/compile_commands.json. Every build, through the script or a CMake preset, refreshes that file from whichever board or variant it built last, so build a board once and clangd follows. Stock clangd does not know the Xtensa targets; install Espressif's esp-clangd (the installer puts it under the tools directory, e.g.C:\Espressif\tools\esp-clangd\<version>\esp-clangd\bin\clangd.exe) and pointclangd.pathat it in your user settings, since the path is machine specific. launch.jsonandc_cpp_properties.jsonare not tracked; a debugger setup with OpenOCD is per machine.
CMakePresets.json has a configure preset and a build preset per board. A preset passes only the
board name; the root CMakeLists.txt derives the chip and the sdkconfig layers from the board
file and activates an installer-registered ESP-IDF itself, so no exported shell is needed:
cmake --preset OpenShock-Core-V2
cmake --build --preset OpenShock-Core-V2 # everything, incl. the www image
cmake --build --preset OpenShock-Core-V2 --target app
ESPPORT=COM4 cmake --build --preset OpenShock-Core-V2 --target flash
ESPPORT=COM4 cmake --build --preset OpenShock-Core-V2 --target monitorThe inline ESPPORT=COM4 form is POSIX shell syntax. In PowerShell set it first with
$env:ESPPORT = "COM4", in cmd with set ESPPORT=COM4, and then run the command without it.
The presets build into build/<board>/, the same directories scripts/build.py uses, so the
two can be mixed. Flash and monitor read the port from ESPPORT; menuconfig is a target too.
A preset build stamps the board like the script does, since the stamp is a CMake post-build step.
The file also has a default preset, which configures OpenShock-Core-V2 (DEFAULT_BOARD in
scripts/gen_cmake_presets.py). idf.py configures with the preset of that name once a
CMakePresets.json is present, so a bare idf.py menuconfig or idf.py build works on that board;
idf.py --preset <board> menuconfig picks another. scripts/build.py runs under it too, and the
script's own -B and -D arguments take precedence, so it changes nothing there.
Differences from the script: a tree configured by a preset has ccache off (idf.py turns it on),
and the bootloader's nested configure prints a warning about ESP_ROM_ELF_DIR when the shell is
not exported; it is harmless. A tree the script configured keeps ccache when CLion or a bare
shell builds it: the compile rules carry ccache's absolute path, so nothing needs to be on PATH.
- Open the repository root (File → Open, pick the folder with
CMakeLists.txt). Choose "Open as CMake project" if asked. CLion findsCMakePresets.jsonand shows the boards as disabled profiles; it may also create a "Debug" profile of its own. Delete that one, since it has no board and will fail to configure. - Enable the boards you build under Settings → Build, Execution, Deployment → CMake. Each preset is a profile; tick "Enable profile" on the ones you want. Leave the toolchain at the default. The preset supplies the generator, and CMake caches the compiler paths from the ESP-IDF install, so CLion's own toolchain is not used for compiling.
- Let the first configure run. It takes about half a minute per profile and prints
ESP-IDF activated from ...early in the CMake tool window. If it instead stops with "no ESP-IDF install was found", ESP-IDF was not installed with the Espressif installer: add the environment to the toolchain (Settings → Build, Execution, Deployment → Toolchains, "Add environment" → "From file") pointing atscripts/idfenv.baton Windows orscripts/idfenv.shelsewhere, or run CLion from a shell where the ESP-IDF export script has been sourced. - Pick a target. The run/build configuration dropdown lists the CMake targets of the active
profile:
appcompiles the firmware,flashwrites it,monitoropens the serial console,menuconfigedits the configuration. ForflashandmonitorsetESPPORT(e.g.COM4) in the configuration's environment variables. - Build with Ctrl+F9 or the hammer icon. Output lands in
build/<board>/, shared withscripts/build.py.
Code insight comes from CLion's own CMake integration, so no clangd setup is needed. The
.idea/ directory and CMakeUserPresets.json are ignored, so machine-specific profiles and
settings stay local. The presets file is generated from boards/; after adding a board run:
python scripts/gen_cmake_presets.pyThe web UI in frontend/ is built with Vite, gzipped, and packed into the flat www image
(STORAGE.md). The CMake www target does this automatically during a full build and
only rebuilds the frontend when its sources changed. To work on the frontend itself:
cd frontend
pnpm install
pnpm run dev # dev server
pnpm run check # svelte-check, what CI runs
pnpm run lint # prettier + eslint
pnpm test:unitTo build only the image, as CI does:
python scripts/gen_staticfs.py --frontend-dir frontend --output build/www.bin \
--partition-csv partitions/ota_4mb.csvReleases ship one binary per board, flashable at offset 0. CI gets there in three steps, which
run locally too. package_boards.py expects each variant directory to hold app.bin,
bootloader.bin and partition-table.bin, the names CI uploads; a local variant build writes
firmware.bin, bootloader/bootloader.bin and partition_table/partition-table.bin, so copy
them first.
python scripts/build.py --variant esp32s3-6832dbd5 app bootloader partition-table
mkdir -p out/variants/esp32s3-6832dbd5
cp build/esp32s3-6832dbd5/firmware.bin out/variants/esp32s3-6832dbd5/app.bin
cp build/esp32s3-6832dbd5/bootloader/bootloader.bin out/variants/esp32s3-6832dbd5/
cp build/esp32s3-6832dbd5/partition_table/partition-table.bin out/variants/esp32s3-6832dbd5/
python scripts/package_boards.py --variants out/variants --board "OpenShock-Core-V2" --out out/boards
python scripts/merge_image.py --board OpenShock-Core-V2 --bindir 'out/boards/{board}' \
--www build/www.bin --output 'out/merged/{board}.bin'
esptool write_flash 0x0 out/merged/OpenShock-Core-V2.binmerge_image.py needs only esptool, not ESP-IDF.
Each components/<name>/host_test/ is its own ESP-IDF project for the linux target with a Unity
suite. The ELF exits with the number of failures. Linux only; needs libbsd-dev and Ruby (ESP-IDF
generates CMock mocks with it).
idf.py -C components/config/host_test --preview set-target linux build
components/config/host_test/build/*_host_test.elfThe loop CI runs over all of them is in .github/workflows/host-tests.yml.
pip install --require-hashes -r requirements.txt -r requirements-dev.txt
ruff check .
mypy
python -m pytestConfig for all three is in pyproject.toml.
C++ formatting is .clang-format, enforced by the cpp-linter workflow. Run
clang-format -i on changed files before pushing. clang-tidy is configured but not run in CI.
Components declare registry dependencies in their own idf_component.yml (today components/core
and components/osjson); dependencies.lock at the root pins them and is committed.
Update through the board script so the lock is solved for the same target CI uses:
python scripts/build.py OpenShock-Core-V2 update-dependenciesCommit the resulting dependencies.lock. A plain idf.py update-dependencies works too but
also flips the lock's target line to whatever the plain sdkconfig targets.
Change the idf-version default in .github/actions/setup-esp-idf/action.yml, install the same
version locally, rebuild every variant, and run the host tests. Expect Kconfig renames; a
reconfigure prints what it ignored.
requirements.txt is compiled from requirements.in with hashes:
uv pip compile requirements.in --universal --python-version 3.10 --generate-hashes -o requirements.txtrequirements-dev.txt adds the test and lint tools on top of it; the refresh command is in its header.
cd frontend
pnpm update --latest # or edit package.json
pnpm install
pnpm run check && pnpm run lintCommit package.json and pnpm-lock.yaml. Dependabot also opens bumps for these.
schemas/ is a submodule of the shared flatbuffers-schemas repository. Generated code is
committed in two places and must match the vendored flatbuffers runtime exactly:
git -C schemas pull origin <branch> # move the schema submodule
python scripts/fetch_flatc.py # flatc matching components/flatbuffers/flatbuffers
python scripts/generate_schemas.py # regenerates C++ and TypeScriptTo move the flatbuffers runtime itself to a newer release, run fetch_flatc.py --upgrade. It
advances the submodule and fetches the matching flatc; then regenerate.
certificates/cacert-merged.pem is compiled into the firmware. Regenerating it requires a manual
checksum check against curl's site. Follow certificates/README.md exactly.
- Component dependency graph:
python scripts/gen_dep_graph.pyemits Mermaid. build/compile_commands.jsonis the compile database of the last build, by script or preset;.clangdpoints at it.- CHANGELOG.md and RELEASE.md hold release notes and the flashing instructions shipped with a release.