V4 Multi-Boot Selector
April 19, 2026 · View on GitHub
A firmware selector for the Heltec WiFi LoRa 32 V4.2 (ESP32-S3) that lets you switch between multiple firmwares at boot time — Meshtastic, MeshCore, Reticulum/RNode, or anything else.
How It Works
The 16MB flash is divided into a factory partition (boot selector app) and 4 OTA slots that share a 12 MB region. By default each slot is 3 MB, but the installer wizard can resize them in 64 KB steps — e.g. 5 MB / 2 MB / 2 MB / 3 MB — to fit larger firmwares. On power-up, the selector shows a menu on the OLED labelled with each slot's .bin filename. Pick one with the USER button and the device reboots into it; the selection is remembered for auto-boot next time.
$ \text{Flash} \text{Layout} (16\text{MB}): ┌──────────────┬──────────┬────────────────────────────────┐ │ 0\text{x000000} │ 32\text{KB} │ \text{Bootloader} │ │ 0\text{x008000} │ 4\text{KB} │ \text{Partition} \text{Table} │ │ 0\text{x009000} │ 4\text{KB} │ \text{Selector} \text{Config} (\text{sel\_cfg}) │ │ 0\text{x00E000} │ 8\text{KB} │ \text{OTA} \text{Data} (\text{boot} \text{selection}) │ │ 0\text{x010000} │ 1\text{MB} │ \text{Boot} \text{Selector} (\text{factory}) │ │ 0\text{x110000} │ 12\text{MB} │ \text{OTA} \text{Slots} 0-3 (\text{configurable}) │ │ 0\text{xD10000} │ 544\text{KB} │ \text{SPIFFS} (\text{active}) │ │ 0\text{xD98000} │ 4 \times 544\text{KB} │ \text{Per}-\text{slot} \text{FS} \text{Backups} │ │ 0\text{xFB8000} │ 32\text{KB} │ \text{NVS} (\text{active}) │ │ 0\text{xFC0000} │ 4 \times 32\text{KB} │ \text{Per}-\text{slot} \text{NVS} \text{Backups} │ └──────────────┴──────────┴────────────────────────────────┘ $
Requirements
- PlatformIO (CLI or IDE)
- Heltec WiFi LoRa 32 V4.2
- USB-C cable
- Firmware
.binfiles for the firmwares you want to load
Build
pio run
Flash
The script defaults to /dev/ttyACM1 and baud 115200 (required for the ESP32-S3's native USB-Serial-JTAG). Override with -p / -b if your device enumerates elsewhere.
Interactive installer (recommended)
python3 scripts/flash_firmware.py install
Walks you through:
- Picking a
.binfromfirmware/for each of the four slots (or leaving slots empty). - Reading each selected
.bin, computing its app size, and recommending a per-slot size (firmware + ~25% headroom, rounded up to 64 KB). Any leftover 12 MB budget is added to the slot holding the largest firmware — the one most likely to grow on future updates. Empty slots are given the 64 KB minimum. - Sizing each slot — press Enter to accept the recommendation, or type
3,3M,2048K,0x300000, orautoon one slot to absorb the remainder. Sum must be 12 MB and each size a multiple of 64 KB. - A diff vs the current
partitions.csv. - Regenerating
partitions.csv, runningpio runto rebuild the selector, and flashing everything.
If the layout is unchanged, the rebuild step is skipped.
Non-interactive provisioning
python3 scripts/flash_firmware.py flash-all \
--slot0 firmware/meshtastic.bin \
--slot1 firmware/meshcore.bin \
--slot2 firmware/rnode.bin \
--slot3 firmware/reticulum.bin
Flashes the bootloader, partition table, selector app, and all four firmware slots in one go.
Update a single slot
python3 scripts/flash_firmware.py flash-slot 0 firmware/meshtastic-new.bin
Show partition info
python3 scripts/flash_firmware.py info
Usage
- Press RST or power on — the boot selector always appears
- The splash screen appears for 1.5 seconds
- If a firmware was previously selected, it auto-boots after 5 seconds with a countdown bar
- Short press USER to cycle through firmware slots
- Long press USER to boot the highlighted firmware
- Press any button during countdown to cancel auto-boot and browse the menu
Returning to the Boot Menu
Just press RST. The custom bootloader hook detects hardware resets and always boots the selector. This works because:
- RST button / power cycle → bootloader erases otadata → selector runs
- Software restart (from selector to firmware) → otadata preserved → firmware boots
- Firmware crashes / watchdog → selector runs (safe fallback)
To skip auto-boot and force the full menu: hold USER while pressing RST.
Getting Firmware Binaries
| Firmware | Where to get .bin |
|---|---|
| Meshtastic | meshtastic.org/downloads — pick heltec-v3 variant |
| MeshCore | flasher.meshcore.dev — pick Heltec V4 |
| Reticulum/RNode | Install rnodeconf and extract the firmware, or build from source |
Notes
- The four OTA slots share a 12 MB region. Default layout is 4×3 MB, but the installer can reassign within that budget (e.g. one 5 MB slot for Meshtastic + three 2 MB slots). Each size must be a multiple of 64 KB.
- Per-firmware data isolation: Each firmware's NVS (settings, keys, IDs) and filesystem data are saved/restored automatically when switching. Meshtastic, MeshCore, RNode etc. each keep their own independent config.
- Built-in OTA updates within a firmware (e.g. Meshtastic's own OTA) won't work because the partition labels differ. Update via the flash script instead.
- The active SPIFFS partition is 544 KB and each slot has a matching 544 KB filesystem backup area plus a 32 KB NVS backup area — all fixed, regardless of how the 12 MB OTA region is carved up.
- The OLED menu labels each slot with the
.binfilename the installer was given, not hardcoded firmware names.
Project Structure
├── platformio.ini # Build config
├── partitions.csv # Flash partition table
├── boards/ # Custom board definition
├── src/
│ ├── main.cpp # Entry point
│ ├── config.h # Pin defs, constants
│ ├── display.h/cpp # OLED driver & UI rendering
│ ├── button.h/cpp # USER button input
│ ├── firmware_manager.h/cpp # Partition scanning & OTA
│ └── menu.h/cpp # Menu state machine
├── scripts/
│ └── flash_firmware.py # Flash utility
└── firmware/ # Put your .bin files here
💖 Support this project
If you find this project helpful, consider donating:
