PlatformIO skill for Codex
Pinned firmware builds with tests that run in seconds. Codex structures platformio.ini with a shared [env] base, extends and exact platform and library pins, so a fresh clone builds the same firmware. It moves logic where the native environment can unit-test it with Unity in seconds, keeps hardware suites separate, adds pio check and compile_commands.json, and wires a cached CI job.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- platformio Installs to ~/.agents/skills/platformio/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install platformio The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project platformio Installs to .agents/skills/platformio/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/platformio.zipunzip -o platformio.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) platformio PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $platformio, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Sets up and maintains PlatformIO firmware projects - platformio.ini environments with pinned platforms and lib_deps, shared [env] sections and extends, build flags and source filters, Unity unit tests on the native host and on hardware, static analysis with pio check, compile_commands.json for editors, and CI caching. Use when creating or cleaning up a platformio.ini, builds differ between machines, adding unit tests to firmware, a library resolves to the wrong version, or running PlatformIO builds and tests in GitHub Actions.
Use it when
- Creating or restructuring a platformio.ini with several boards
- Builds differ between machines or a library jumped a major version
- Adding unit tests to firmware without hardware in the loop
- Running PlatformIO builds and tests in CI
Use something else when
- A single sketch built with the Arduino IDE or arduino-cli: use arduino-sketches
- The project is built with ESP-IDF's own idf.py: use esp32-firmware
How to check the result
Build every environment from a fresh clone with an empty package cache, run pio test -e native, and confirm CI builds all envs with the pinned versions.
The mistake it prevents
Leaving platform unpinned, so the next clean install pulls a new toolchain and framework and the firmware changes without any code change.
Done well means: platformio.ini pins every platform and library version so a fresh clone
builds identically, logic is unit-tested on the host in seconds, hardware tests run with the
same framework, and CI builds every environment from cache.
Inspect first
- Read
AGENTS.md, thenplatformio.ini,src/,include/,lib/,test/and anyextra_scripts. - List environments and resolved versions:
pio project config # parsed config per environment
pio pkg list -e esp32dev # installed platform, toolchain and library versions
pio run -e esp32dev # baseline build; keep the RAM/Flash summary
First runs download platforms and toolchains (hundreds of MB). That needs network; in a
Codex sandbox, ask for approval or work in an environment where ~/.platformio is populated.
A pinned, layered platformio.ini
[platformio]
default_envs = esp32dev
[env] ; inherited by every env, including native
build_flags =
-Wall -Wextra
-DAPP_VERSION=\"1.4.0\"
test_framework = unity
[device] ; plain section shared by board envs via extends
framework = arduino
monitor_speed = 115200
lib_deps =
bblanchon/[email protected]
adafruit/Adafruit BME280 [email protected]
[env:esp32dev]
extends = device
platform = [email protected]
board = esp32dev
board_build.partitions = partitions.csv
monitor_filters = esp32_exception_decoder
[env:esp32dev-debug]
extends = env:esp32dev
build_type = debug
build_flags =
${env.build_flags}
-DLOG_LEVEL=4
[env:uno]
extends = device
platform = [email protected]
board = uno
[env:native] ; host tests: no framework, no device libraries
platform = native
build_flags =
${env.build_flags}
-std=c++17 -DUNIT_TEST
test_build_src = no
test_ignore = test_device_*
Rules:
- Pin
platform = [email protected]. An unpinned platform upgrades toolchain and framework under you on the next clean install. - Pin libraries to exact versions (
@7.2.1) for products;^7.2.1only for hobby code. Use theowner/nameform from the registry to avoid same-name forks. - Overriding
build_flagsin an env replaces the inherited value; reference${env.build_flags}to append. - Keep private, project-specific libraries in
lib/<name>/with their own folder per library; PlatformIO’s dependency finder compiles them only when included. - Arduino-ESP32 3.x is not in the official
espressif32platform; the community pioarduino platform provides it. Decide deliberately and pin whichever you use.
Source and flag control
| Need | Key |
|---|---|
| Exclude files from one env | build_src_filter = +<*> -<drivers/ws2812.cpp> |
| Flags only for project code, not libraries | build_src_flags |
| Strip a default flag | build_unflags = -Os |
| Generated version header, git hash | extra_scripts = pre:scripts/version.py |
| Library compatibility mode issues | lib_ldf_mode = deep+ (slower, finds conditional includes) |
Unit tests
test/
├── test_parser/test_main.cpp # host-safe logic
└── test_device_i2c/test_main.cpp # needs hardware
#include <unity.h>
#include "frame_parser.h"
void setUp(void) {}
void tearDown(void) {}
void test_rejects_bad_crc(void) {
const uint8_t frame[] = {0xAA, 0x03, 0x01, 0x02, 0x03, 0x00};
TEST_ASSERT_EQUAL(PARSE_BAD_CRC, parse_frame(frame, sizeof frame, nullptr));
}
int main(int, char **) { // native env
UNITY_BEGIN();
RUN_TEST(test_rejects_bad_crc);
return UNITY_END();
}
On a board, replace main with setup() that waits about 2 s (so the serial port is
ready), runs UNITY_BEGIN() … UNITY_END(), and an empty loop().
pio test -e native # host, seconds, runs in the sandbox
pio test -e native -f test_parser -v # one suite, verbose
pio test -e esp32dev --without-uploading # build only, when no board is attached
pio test -e esp32dev -f test_device_* # on hardware: user runs this with the board
- Put testable logic in
lib/or behind headers so the native env can compile it without Arduino. If a test needsArduino.h, the code is not separated enough. test_build_src = yescompilessrc/into tests; with it, guardsetup()/loop()ormain()insrcwith#ifndef UNIT_TESTto avoid duplicate symbols.test_ignore = test_device_*in the native env keepspio test -e nativeaway from hardware suites.
Static analysis and editor support
pio check -e esp32dev --skip-packages --severity=medium # cppcheck by default
pio run -e esp32dev -t compiledb # compile_commands.json for clangd
pio run -e esp32dev -t size # section sizes
Set check_tool = cppcheck, clangtidy and check_flags in the env to make the checks
reproducible.
CI (GitHub Actions)
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.platformio/.cache
~/.platformio/packages
~/.platformio/platforms
key: pio-${{ runner.os }}-${{ hashFiles('platformio.ini') }}
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install platformio==6.1.16
- run: pio test -e native
- run: pio run # every env in default_envs; add -e for others
Pin the PlatformIO Core version too. Release builds should upload firmware.bin and
firmware.elf as artifacts; the ELF is needed to decode field crashes.
Gotchas
lib_depsentries with spaces in names need the exact registry name; a near-miss silently resolves a different library. Confirm withpio pkg list..pio/is a build cache: never commit it; delete it (pio run -t cleanor remove the folder) when switching platform versions.- Uploading picks the first serial port it finds; set
upload_portandmonitor_portwhen several boards are attached. - Native tests run on the host’s architecture:
intsize, endianness and alignment may differ from the target. Use fixed-width types in tested code.
Verification
- Fresh clone with an empty
~/.platformiobuilds every env with the pinned versions -
pio test -e nativepasses; hardware suites pass on the board (user-run output) -
pio checkshows no new medium or high defects - CI builds all environments and caches packages
- Size output compared with the previous release
Report
**Envs:** esp32dev, esp32dev-debug, uno, native (espressif32 6.9.0, atmelavr 5.1.0, Core 6.1.16)
**Changes:** pinned 4 libraries (ArduinoJson was floating to 7.x from 6.x); [env] base + extends
**Tests:** 23 native tests (parser, scheduler) in 2.1 s; 3 hardware suites build-only in CI
**CI:** build + native tests, cache hit time 48 s What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "PlatformIO"
short_description: "Pinned firmware builds with tests that run in seconds"
default_prompt: "$platformio Clean up our platformio.ini, builds differ between my laptop and CI" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.