Building the firmware

July 8, 2026 · View on GitHub

This firmware builds with PlatformIO. Most envs build out of the box with pio run -e <env>. The one wrinkle is that the tree spans two incompatible Arduino-ESP32 cores, which need a little care for local builds — see Two-core tree below.

Bootstrap (first time only)

The web GUI is a git submodule and must be built before the firmware — the build embeds its compiled assets into the binary (see scripts/extra_script.py).

git submodule update --init --recursive
cd gui-nightshift && npm install && npm run build && cd ..

Node.js 20+ and npm are required. The default UI is the gui-nightshift submodule; setting GUI_NAME=gui-v2 (or any other checkout) selects an alternate GUI directory.

Quick start

pip install -U platformio          # or use the VS Code PlatformIO extension

# Build the default 4MB WiFi gateway
pio run -e openevse_wifi_v1

# Build + flash a connected board
pio run -e openevse_wifi_v1 -t upload

Common alternative envs (see platformio.ini for the full list):

pio run -e adafruit_huzzah32
pio run -e nodemcu-32s
pio run -e olimex_esp32-gateway-e      # wired Ethernet
pio run -e openevse_wifi_tft_v1        # TFT touchscreen
pio run -e wt32-eth01                  # Ethernet (WT32)
pio run -e olimex_esp32-poe-iso        # Ethernet + PoE

Build artifacts land in .pio/build/<env>/firmware.bin. The first build downloads the ESP32 toolchain (~500 MB) and can take 15–45 minutes — do not cancel it; incremental builds take 2–5 minutes.

If "GUI files not found" appears, the submodule is missing or unbuilt — run the bootstrap steps above. If HTTPClientError appears, PlatformIO cannot reach its package registry (network restriction).

Two-core tree

The boards split across two PlatformIO platforms:

BoardsPlatformArduino / IDFWhy
4MB (openevse_wifi_v1, gateways, huzzah, …) — the defaultespressif32@6.12.0core 2.x / IDF4Keeps the IDF4 image small enough for dual-slot OTA on 4MB flash.
16MB (openevse_wifi_v1_16mb)${common.platform_core3} (pioarduino)core 3.x / IDF5Larger flash / newer silicon needs the core-3 toolchain; this env is openevse_wifi_v1 rebuilt on core-3.

Shared src/ code that touches APIs which changed between cores (e.g. LEDC, mbedTLS cert parsing) is version-guarded on ESP_ARDUINO_VERSION / MBEDTLS_VERSION_NUMBER so it compiles on both.

Local builds: use scripts/pio

The two platforms declare ~13 package names in common (the Arduino framework, esp-idf, the toolchains, several tool-*) but pin different versions. They all install into one PLATFORMIO_CORE_DIR (~/.platformio), so building a core-2 env right after a core-3 env — or vice versa — evicts and re-downloads those packages every time you switch. (CI is immune: each job runs on a fresh, isolated runner.)

To avoid the thrash locally, build through the wrapper, which routes each core to its own PLATFORMIO_CORE_DIR:

scripts/pio run -e openevse_wifi_v1_16mb   # core-3  -> ~/.platformio-core3
scripts/pio run -e openevse_wifi_v1        # core-2  -> ~/.platformio (default)
scripts/pio device monitor                 # any other pio subcommand, passed through

scripts/pio is a transparent pio drop-in: it reads -e <env>, picks the matching core dir, and execs the real pio with your args unchanged. Notes:

  • core-2 stays in the default ~/.platformio, so bare pio, CI, and IDE integrations are unaffected.
  • core-3 goes to ~/.platformio-core3 (override with PIO_CORE3_DIR). The first core-3 build populates it once (downloads its toolchain/framework); after that, switching cores never re-downloads.
  • It refuses to build core-2 and core-3 envs in a single invocation (they can't share a core dir) — split them into two commands.

Trade-off: the separate core-3 dir costs a few GB of disk. That's the price of never re-downloading on a core switch.

Debug builds

The _dev env variants enable serial debug output on the ESP's second serial port (GPIO2 / Serial1):

pio run -e openevse_wifi_v1_dev -t upload

To route debug output to the main serial port instead, change -DDEBUG_PORT=Serial1 to -DDEBUG_PORT=Serial in platformio.ini — note this interferes with RAPI communication to the controller.

Upload troubleshooting

  • Some boards need manual bootloader mode: hold BOOT, press RESET, release RESET, release BOOT.
  • Try a lower upload baud rate if flashing is unreliable.
  • If the ESP reboot-loops after an upgrade, erase the flash and re-flash:
esptool.py erase_flash

Host-side unit tests

Pure, framework-free logic is tested on the build host via the native_test env:

pio test -e native_test

Test suites live under test/. New host-testable logic should land with a doctest suite alongside it.

The full native firmware build is native_openevse. To build the host binary with the LVGL local UI path enabled, use native_openevse_lvgl:

pio run -e native_openevse_lvgl

That env enables the LVGL local UI code on the host build. By default it runs in headless mode so the UI logic is compiled and exercised without requiring ESP32 display hardware.

For interactive local development you can also open the native LVGL output in an SDL window:

.pio/build/native_openevse_lvgl/program --lvgl-display window

That runtime mode loads SDL2 dynamically when it is available on the host (for example via libsdl2-dev on Debian/Ubuntu) and keeps the default headless path unchanged for CI and screenshot export.

To dump the sample LVGL screens from the native binary after that build completes:

mkdir -p /tmp/lvgl-screens
.pio/build/native_openevse_lvgl/program --dump-lvgl-screens /tmp/lvgl-screens

That command writes the boot, setup, and charge-state captures as .ppm images so review comments can attach fresh screenshots of the LVGL local UI.

divert_sim host build

Build the simulator with PlatformIO:

pio run -e native_simulator

This writes the binary to .pio/build/native_simulator/program. The divert_sim pytest suite uses that binary automatically (or ./divert_sim if present):

cd divert_sim
pip install -r requirements.txt
pytest -v