Mock ESP32 ROM bootloader (SLIP) for CI and local upload testing without hardware
Project description
esp32-mock-bootloader
A mock Espressif ROM bootloader for testing firmware uploads without a board.
Run it on your machine or in CI. Point esptool, arduino-cli, or any ROM-compatible flasher at a TCP port or serial path, and flash as if a chip were connected.
Stability: version
0.xis alpha. CLI flags and protocol details may change before1.0.0.
Table of contents
- Overview
- Features
- Requirements
- Installation
- Quick start
- Usage
- Transports
- GitHub Actions
- Client examples
- How it works
- Protocol references
- Limitations
- Development
- Project layout
- AI disclosure
- License
Overview
Real Espressif chips expose a ROM bootloader over serial. Upload tools speak a SLIP-framed binary protocol: sync, detect the SoC, write flash blocks, verify MD5, and so on.
esp32-mock-bootloader implements enough of that protocol for upload clients to complete a full flash cycle. It does not run your firmware or emulate peripherals — it only answers the bootloader conversation.
esptool / arduino-cli / …
│
▼
socket://127.0.0.1:PORT or /dev/tty* / COM*
│
▼
esp32-mock-bootloader (SLIP server)
│
▼
ACK + chip metadata + in-memory flash image
Chip metadata comes from the installed esptool package. When Espressif adds a new SoC to esptool, this mock can support it without a code change here.
For protocol details and authoritative behavior, see Protocol references.
Features
- No hardware — run upload tests locally and in CI.
- All esptool SoCs — profiles are built from
esptool.targets.CHIP_DEFS. - Auto chip detection —
--chip autolearns the SoC from client traffic. - TCP daemon — background
start/stopwith stablesocket://URLs. - PTY and COM paths — Unix PTY, Windows com0com pairs, or socket fallback.
- GitHub Action — one step to start the mock; teardown runs automatically.
- CI-ready — tested on Ubuntu, Windows, and macOS with esptool integration tests.
Requirements
| Component | Version |
|---|---|
| Python | 3.9 or newer |
| esptool | Installed automatically with this package (runtime dependency) |
| Upload client | e.g. pip esptool, or arduino-cli with a client that supports your transport |
Optional:
- com0com on Windows — for real
COMxports in local testing (setup guide).
Installation
From PyPI:
pip install esp32-mock-bootloader
Pin a release:
pip install esp32-mock-bootloader==0.1.0
From source (development):
git clone https://github.com/lucasssvaz/esp32-mock-bootloader.git
cd esp32-mock-bootloader
pip install -e ".[dev]"
Quick start
# 1. Start the mock (background daemon on port 9876)
esp32-mock-bootloader start
# 2. Flash through it
esptool --chip esp32 \
--port "$(esp32-mock-bootloader url)" \
write-flash 0x10000 firmware.bin
# 3. Stop when done
esp32-mock-bootloader stop
The daemon keeps running between steps 1 and 3. Use status to inspect it, or url to print the socket:// address again.
Usage
CLI commands
| Command | Description |
|---|---|
start |
Start the background daemon and exit once the port is ready |
stop |
Stop the daemon (safe to run if already stopped) |
status |
Show pid, chip mode, detected SoC, and URL (exit 1 if stopped) |
url |
Print socket://127.0.0.1:PORT |
chips |
List SoCs supported by the installed esptool |
run |
Run the server in the foreground (used internally; prefer start) |
Common flags for start and run:
| Flag | Default | Description |
|---|---|---|
--chip |
auto |
Chip profile, or auto to detect from client traffic |
--port |
9876 |
TCP port |
--bind |
127.0.0.1 |
Bind address |
--state-dir |
~/.cache/esp32-mock-bootloader |
Daemon state and logs |
--startup-timeout |
30 |
Seconds start waits for the port (start only) |
--force |
off | Stop an existing daemon on the same port first (start only) |
status --json adds machine-readable output. Set ESP32_MOCK_BOOTLOADER_STATE_DIR to override the default state directory.
Daemon lifecycle
start writes state to {state-dir}/port-{port}.json and logs to port-{port}.log:
{
"pid": 12345,
"port": 9876,
"chip": "auto",
"url": "socket://127.0.0.1:9876",
"detected_chip": "esp32"
}
detected_chip is filled after a client identifies the SoC (in auto mode).
Chip selection
| Mode | When to use |
|---|---|
--chip auto |
Client passes its own --chip; mock learns from registers (recommended for CI matrices) |
--chip esp32, esp32c6, … |
Fixed profile for every session |
In auto mode the mock:
- Returns a ROM-style error on the first
GET_SECURITY_INFOso clients do not lock onto ESP32 viachip_id0. - Returns
0for the legacy probe at0x40001000until chip-specific registers identify the SoC. - Sets
detected_chipfrom unique detect registers or efuse windows (addresses from esptool ROM classes).
Supported chips
Every target in the installed esptool CHIP_DEFS is available:
esp32-mock-bootloader chips
esp32-mock-bootloader chips --json # detect registers and chip_id metadata
New esptool releases can add SoCs without updating this package.
Transports
TCP (default)
The daemon listens on 127.0.0.1:PORT. Pip-installed esptool accepts socket:// URLs via pyserial.
esp32-mock-bootloader start --port 9876
esptool --chip esp32c6 --port socket://127.0.0.1:9876 write-flash 0x10000 app.bin
PTY / serial path
Use --pty with run when a tool expects a device path instead of a URL (common with arduino-cli):
esp32-mock-bootloader run --pty --pty-path-file /tmp/mock-pty --chip esp32
esptool --chip esp32 --port "$(cat /tmp/mock-pty)" write-flash 0x10000 firmware.bin
| Platform | --pty provides |
|---|---|
| Linux / macOS | Real PTY device (e.g. /dev/ttys003) |
| Windows (local) | com0com virtual COM pair |
| Windows (CI) | socket://127.0.0.1:PORT fallback when no COM pair is configured |
Windows with com0com
Install com0com. Run the mock on the server port; the path file receives the client port:
esp32-mock-bootloader run --pty --pty-path-file mock.port ^
--com-port COM18 --com-peer COM19 --chip auto
esptool --chip esp32 --port COM19 write-flash 0x10000 firmware.bin
Environment variables ESP32_MOCK_COM_PORT and ESP32_MOCK_COM_PEER work the same as --com-port / --com-peer.
Helper script (elevated prompt; creates a pair, runs esptool, removes the pair):
python scripts\test_windows_com.py
Set ESP32_MOCK_KEEP_COM_PAIR=1 to leave the pair installed after the script exits.
GitHub Actions
The action starts the daemon in the main step and stops it in a post step when the job ends — including on failure. No manual stop required.
jobs:
upload-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Mock bootloader
uses: lucasssvaz/esp32-mock-bootloader@v0.1.0
id: mock
- name: Flash test firmware
run: |
pip install esptool
esptool --chip esp32 \
--port "${{ steps.mock.outputs.url }}" \
--no-stub --before no-reset --after no-reset \
write-flash 0x10000 firmware.bin
Outputs: url (socket://…), port.
Inputs (all optional):
| Input | Default | Description |
|---|---|---|
chip |
auto |
Chip profile |
port |
9876 |
TCP port |
startup-timeout |
30 |
Startup wait in seconds |
python-version |
3.x |
Python for setup-python |
version |
(empty) | PyPI pin (e.g. 0.1.0); omit to install from the action checkout |
Manual CLI in a workflow (without the action):
- run: pip install esp32-mock-bootloader esptool
- run: esp32-mock-bootloader start
- run: |
esptool --chip esp32 --port "$(esp32-mock-bootloader url)" \
write-flash 0x10000 firmware.bin
- run: esp32-mock-bootloader stop
if: always()
Client examples
esptool (TCP, no stub — typical for CI):
esptool --chip esp32c6 \
--port socket://127.0.0.1:9876 \
--no-stub --before no-reset --after no-reset \
write-flash 0x10000 app.bin
arduino-cli:
arduino-cli compile -b esp32:esp32:esp32 \
-p socket://127.0.0.1:9876 --upload sketch.ino
Shell CI script:
esp32-mock-bootloader start
PORT="$(esp32-mock-bootloader url)"
# run your upload tests against "$PORT"
esp32-mock-bootloader stop
How it works
The mock speaks SLIP-framed ROM commands. Implemented handlers include:
SYNC, FLASH_BEGIN / DATA / END, FLASH_DEFL_*, MEM_* (+ OHAI after MEM_END), READ_REG, GET_SECURITY_INFO, WRITE_REG, SPI_SET_PARAMS, SPI_ATTACH, CHANGE_BAUDRATE, SPI_FLASH_MD5.
Flash data is stored in an in-memory image (erased bytes default to 0xFF). After a stub upload (MEM_END with entrypoint + OHAI), SPI_FLASH_MD5 returns a 16-byte binary digest; in ROM mode it returns 32-byte lowercase hex ASCII — matching esptool’s flash_md5sum() expectations. Unknown commands receive a generic ACK.
The mock validates protocol behavior, not silicon accuracy. It does not model Wi-Fi, sleep, brownout, or real flash timing.
Protocol references
Implementation follows Espressif’s published bootloader protocol and the esptool reference client. Primary sources:
| Topic | Reference |
|---|---|
| Serial protocol overview | esptool serial protocol (ESP32) — same command set is documented per chip under esptool/en/latest/<chip>/advanced-topics/serial-protocol.html |
| Flash upload & MD5 verify | Verifying uploaded data — ROM returns 32 hex ASCII bytes; stub returns 16 raw MD5 bytes before the 2 status bytes |
SPI_FLASH_MD5 (0x13) |
Commands table |
Stub upload & OHAI |
Functional description — initialization — MEM_END entrypoint, unsolicited OHAI SLIP packet |
| Client MD5 handling | esptool loader.py — flash_md5sum — RESP_DATA_LEN 32 (ROM) vs RESP_DATA_LEN_STUB 16 (stub); status bytes follow the digest |
| Stub lifecycle | esptool loader.py — run_stub — RAM download via MEM_*, then mem_finish(entry) |
| Chip profiles & detection | esptool targets / CHIP_DEFS — register addresses, magic values, security info |
Integration tests in downstream projects (e.g. arduino-esp32 upload tests) exercise the default stub path (flasher.py / esptool without --no-stub). CI examples in this repo that pass --no-stub target the ROM MD5 format explicitly.
Limitations
- Protocol emulator only — no application code runs on the mock.
- Client packaging matters — some bundled esptool builds lack
socket://; use PTY/COM or pip esptool. - com0com is local — GitHub-hosted Windows runners use the socket fallback automatically.
- Alpha API — expect changes before
1.0.0.
Development
See CONTRIBUTING.md for pull request guidelines and AI disclosure expectations.
git clone https://github.com/lucasssvaz/esp32-mock-bootloader.git
cd esp32-mock-bootloader
pip install -e ".[dev]"
Run tests:
pytest # parallel by default (pytest-xdist)
pytest -n0 # single process (debugging)
pytest -m "not esptool" # protocol unit tests only
pytest -m "not transport" # skip TCP/PTY integration
pytest tests/test_transports.py # transport matrix only
Coverage (see reports/README.md):
pytest --cov=esp32_mock_bootloader \
--cov-report=term-missing \
--cov-report=html:reports/htmlcov \
--cov-report=xml:reports/coverage.xml
python scripts/check_coverage.py
CI runs the full suite on Ubuntu, Windows, and macOS, enforces coverage baselines on Ubuntu, and uploads an HTML report artifact. Windows com0com tests run locally via scripts/test_windows_com.py.
Build a release wheel:
hatch build
Project layout
esp32-mock-bootloader/
├── CONTRIBUTING.md # PR guidelines and AI policy
├── src/esp32_mock_bootloader/ # Python package (CLI, daemon, SLIP server)
├── action/ # Node.js steps for the GitHub Action
├── action.yml # Composite action entry point
├── tests/ # pytest suite (protocol, esptool, transports)
├── scripts/ # Coverage checker, Windows COM helper
├── reports/ # Coverage config and baselines
└── .github/workflows/ # CI and release pipelines
AI disclosure
This repository was developed with help from AI coding assistants. Every change is reviewed and tested by a human maintainer before merge or release.
Contributor expectations (disclosure, review, commit trailers) are in CONTRIBUTING.md.
License
Copyright 2026 Lucas Saavedra Vaz. Released under the Apache-2.0 license.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file esp32_mock_bootloader-0.1.0.tar.gz.
File metadata
- Download URL: esp32_mock_bootloader-0.1.0.tar.gz
- Upload date:
- Size: 22.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1d716c05e969ce5dd1659fa25dc87eca803f17dbacc9d83ba835fba8b5058f71
|
|
| MD5 |
d9705d58e5e190b9897498694a169023
|
|
| BLAKE2b-256 |
a68efb3c71df71fb75ae6b6e408019d353c21a304ad371d1a68eed8c12aead0a
|
Provenance
The following attestation bundles were made for esp32_mock_bootloader-0.1.0.tar.gz:
Publisher:
release.yml on lucasssvaz/esp32-mock-bootloader
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
esp32_mock_bootloader-0.1.0.tar.gz -
Subject digest:
1d716c05e969ce5dd1659fa25dc87eca803f17dbacc9d83ba835fba8b5058f71 - Sigstore transparency entry: 1785546363
- Sigstore integration time:
-
Permalink:
lucasssvaz/esp32-mock-bootloader@07d71ffd2ef753f5e0c20e763f36250c260ae0dd -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/lucasssvaz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@07d71ffd2ef753f5e0c20e763f36250c260ae0dd -
Trigger Event:
push
-
Statement type:
File details
Details for the file esp32_mock_bootloader-0.1.0-py3-none-any.whl.
File metadata
- Download URL: esp32_mock_bootloader-0.1.0-py3-none-any.whl
- Upload date:
- Size: 26.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3844e8f6f90afe246e797e58dcb779f733bdde1bf0f24131246acf4002c01c26
|
|
| MD5 |
30795f0ab3bd372e2e99e21977fc101f
|
|
| BLAKE2b-256 |
9a90fecd8ad4fb004014503b71bb90ba9c6fb45e38ce4593379098869a0af781
|
Provenance
The following attestation bundles were made for esp32_mock_bootloader-0.1.0-py3-none-any.whl:
Publisher:
release.yml on lucasssvaz/esp32-mock-bootloader
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
esp32_mock_bootloader-0.1.0-py3-none-any.whl -
Subject digest:
3844e8f6f90afe246e797e58dcb779f733bdde1bf0f24131246acf4002c01c26 - Sigstore transparency entry: 1785546437
- Sigstore integration time:
-
Permalink:
lucasssvaz/esp32-mock-bootloader@07d71ffd2ef753f5e0c20e763f36250c260ae0dd -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/lucasssvaz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@07d71ffd2ef753f5e0c20e763f36250c260ae0dd -
Trigger Event:
push
-
Statement type: