Firmware & Flash
Four tools handle the end of the build pipeline — listing what firmware is available, downloading it, flashing it to a real device, and capturing serial output from the device after flash.
| Tool | What it does |
|---|---|
firmware.list | List completed builds that have firmware ready to download. |
firmware.download | Get download metadata for a specific build’s firmware. |
flash.run | Flash firmware to a locally-connected ESP device over serial. |
monitor.run | Capture serial output from a locally-connected device for a bounded duration. |
firmware.list
Shows which builds have finished successfully and have firmware you can download.
Input:
{}
Optionally filter by a specific build:
{ "job_id": "0abf...e2" }
| Field | Required | Notes |
|---|---|---|
job_id | No | Filter to one build. Without it, all succeeded builds are listed. |
Returns:
{
"builds": [
{
"task_id": "0abf...e2",
"target": "esp32s3",
"status": "succeeded",
"build_type": "release"
}
]
}
No side effects. Safe to call any time.
firmware.download
Gets the metadata you need to download a build’s firmware. The actual binary transfer happens over the firmware WebRTC DataChannel — this tool gives you the artifact information.
Input:
{
"job_id": "0abf...e2"
}
| Field | Required | Notes |
|---|---|---|
job_id | Yes | Task ID of a succeeded build. |
output_dir | No | Where to save the firmware files. |
Returns:
{
"job_id": "0abf...e2",
"status": "succeeded",
"artifact_lines": [
"build/flash_bundle.tar.gz",
"build/bootloader.bin",
"build/partition_table/partition-table.bin",
"build/<project>.bin"
],
"output_dir": "/tmp/firmware"
}
The build must have status succeeded. Calling this on a failed or
running build returns an error.
Primary artifact is
flash_bundle.tar.gz. Remote builds assemble a signed, self-describing flash bundle (manifest.json+files/containing bootloader, partition table, and app segments) and return it to the client in the same session — there is no separate fetch step. This is whatflash.runand the CLIespctl flashboth consume. The individual.binfiles are still in the listing for inspection, but you almost never pass them to the flasher directly.
flash.run
Flashes firmware to an ESP device connected to your computer’s USB
port. Uses the pure-Rust espflash
library directly — no Python esptool.py dependency. You do not
need to pip install esptool, run a venv, or have Python in your PATH.
Input:
{
"firmware_path": "/path/to/build/flash_bundle.tar.gz",
"port": "/dev/ttyUSB0",
"baud": 460800
}
| Field | Required | Notes |
|---|---|---|
firmware_path | Yes | Path to a flash_bundle.tar.gz (produced by build + firmware.download), an extracted bundle directory, or a raw .bin / .elf file. The bundle form is preferred because it carries the bootloader, partition table, app, and signed manifest in one file. |
port | No | Serial port. Auto-detected if you have exactly one ESP device connected. |
baud | No | Flash baud rate. Default 460800. |
Returns: Status of the flash operation (success or error with details).
When given a bundle, flash.run reads manifest.json, verifies every
segment’s sha256, and writes all segments to flash in a single
espflash session (critical — per-segment writes would reboot the chip
between segments and hang). The chip reboots once at the end.
Policy: never install
esptool.py. Ifflash.runor the CLIespctl flashfails, file a bug report atdocs/espctl-flash-bugs-YYYY-MM-DD.mdin the aegis repo following the pattern ofdocs/infra-bugs-2026-04-11.md. Do NOT work around the failure by installing Python esptool — that silently hides real bugs in the build-to-flash pipeline.
Local only. This tool runs on your computer, not on the build server. It only works in local/stdio MCP mode — not in the browser. For browser flashing, use the Flash tab in the MCP Console.
monitor.run
Captures serial output from a connected ESP device for a bounded
duration. Used right after flash.run to verify a board boots and is
running the firmware you just wrote — for example, watching for the
1 Hz heartbeat log line emitted by the wizard’s Phase-0 verification
firmware.
Input:
{
"port": "/dev/cu.usbmodem1101",
"baud": 115200,
"duration_sec": 30,
"filter": "heartbeat",
"reset_on_connect": true
}
| Field | Required | Notes |
|---|---|---|
port | No | Serial port (e.g. /dev/ttyUSB0, /dev/cu.usbmodem14101, COM3). Auto-detected if you have exactly one ESP device connected. |
baud | No | Baud rate. Default 115200 (the ESP-IDF console default — different from flash.run’s 460800). |
duration_sec | No | How long to capture. Default 30, capped at 600. |
filter | No | Substring; only lines containing it appear in output. Useful for "heartbeat" verification. |
reset_on_connect | No | Default true — pulses DTR/RTS once after open so the chip boots into the application under the capture window. Set false on boards without an auto-reset circuit, or when another tool already reset the chip. Narrower than espctl probe — never enters bootloader mode. |
Returns:
{
"success": true,
"port": "/dev/cu.usbmodem1101",
"baud": 115200,
"duration_ms": 30024,
"bytes_read": 18432,
"lines_captured": 32,
"output": "I (123) heartbeat: tick 0\nI (1234) heartbeat: tick 1\n...",
"truncated": false,
"message": "Captured 18432 byte(s) over 30024 ms from /dev/cu.usbmodem1101 at 115200 baud."
}
The capture buffer is bounded at ~512 KB; if the device produces more
than that within the window, truncated is true and the tail is
dropped.
Local only. Like
flash.run, this only works in local/stdio MCP mode — not in the browser. Browser monitoring uses Web Serial via the MCP Console — Monitor tab.
No panic decoder. This is a raw UTF-8-lossy byte dump. It does not have
idf.py monitor’s ELF-aware backtrace decoding. For long interactive monitoring use the CLIespctl monitorinstead.
CLI: espctl ports
Lists every serial port the OS exposes. USB ports include their VID:PID. Run this first to find your board.
espctl ports
No flags. An empty list prints No serial ports found.
Output
Human mode (table):
PORT TYPE USB VID:PID
----------------------------------------------------------------------
/dev/cu.usbmodem1101 USB 303A:1001
/dev/cu.Bluetooth-Incoming-... Bluetooth -
JSON (--json): an array of port objects. USB entries also carry
vid, pid, vid_pid, manufacturer, product, serial_number.
# Filter to USB serial adapters
espctl --json ports | jq '.[] | select(.vid_pid != null)'
CLI: espctl probe
Opens the bootloader handshake against a real device and reports the
chip type (with revision), MAC address, and flash size. Uses the same
pure-Rust espflash connection
as espctl flash — no Python esptool.py.
espctl probe --port <port>
Inputs
| Flag | Notes |
|---|---|
--port | Required. Run espctl ports first if you don’t know it. |
Output
Human mode:
Port: /dev/cu.usbmodem1101
Chip: ESP32-S3 (revision v0.2)
MAC: 7c:df:a1:00:11:22
Flash size: 8MB
JSON (--json):
{
"port": "/dev/cu.usbmodem1101",
"chip_type": "ESP32-S3 (revision v0.2)",
"mac_address": "7c:df:a1:00:11:22",
"flash_size": "8MB"
}
Failure modes
- Port not in the OS port list → exit 1 (with a hint to run
espctl ports). - Bootloader handshake fails → exit 1.
CLI: espctl flash
Flashes a bundle to a connected device. The MCP equivalent is
flash.run — same engine, same single-session writeback.
espctl flash <bundle_path> --port <port> [--baud <rate>]
Flag matrix
| Argument | Default | Notes |
|---|---|---|
bundle_path (positional) | required | An extracted bundle directory or flash_bundle.tar.gz. |
--port | required | Serial port. |
--baud | 460800 | Flash baud rate. |
Examples
# Default baud (460800)
espctl flash ./build/flash_bundle.tar.gz --port /dev/cu.usbmodem1101
# Faster — if your USB↔serial adapter and cable can keep up
espctl flash ./build/flash_bundle.tar.gz --port /dev/ttyUSB0 --baud 921600
The bundle form (produced by espctl build) carries manifest.json
with sha256 checksums plus all segments (bootloader, partition table,
app). The flasher writes them in one espflash session — per-segment
writes would reboot the chip between segments and hang.
CLI: espctl monitor
Opens a serial monitor on a device and streams output to your terminal. Auto-reconnects on disconnect by default.
espctl monitor --port <port> [--baud <rate>] \
[--no-reconnect] [--no-reset-on-connect]
Flag matrix
| Flag | Default | Notes |
|---|---|---|
--port | required | Serial port. |
--baud | 115200 | Monitor baud rate (default IDF console). |
--no-reconnect | false | Exit on disconnect instead of waiting for the device to come back. |
--no-reset-on-connect | false | Skip the DTR/RTS pulse on open. |
About --no-reset-on-connect
By default, monitor pulses RTS once on open so the chip boots into
the application under the monitor. Use --no-reset-on-connect on
boards without an auto-reset circuit, or when another tool has already
reset the chip and you don’t want a second reset to interrupt boot.
Typical flow
Remote builds plus local flash and monitor:
espctl build . --target esp32s3
espctl flash ./build/flash_bundle.tar.gz --port /dev/cu.usbmodem*
espctl monitor --port /dev/cu.usbmodem*
The build step is remote by default — no --remote flag needed. The
server URL comes from espctl login or defaults to
https://esphome.cloud. Use --remote <url> to override, or
--local for plan-only.
See also
- Build Lifecycle — how to start a build that produces firmware.
- Logs & Artifacts — reading build output and manifest files.
- MCP Console — Flash tab — browser-based flashing.