Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

ToolWhat it does
firmware.listList completed builds that have firmware ready to download.
firmware.downloadGet download metadata for a specific build’s firmware.
flash.runFlash firmware to a locally-connected ESP device over serial.
monitor.runCapture 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" }
FieldRequiredNotes
job_idNoFilter 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"
}
FieldRequiredNotes
job_idYesTask ID of a succeeded build.
output_dirNoWhere 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 what flash.run and the CLI espctl flash both consume. The individual .bin files 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
}
FieldRequiredNotes
firmware_pathYesPath 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.
portNoSerial port. Auto-detected if you have exactly one ESP device connected.
baudNoFlash 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. If flash.run or the CLI espctl flash fails, file a bug report at docs/espctl-flash-bugs-YYYY-MM-DD.md in the aegis repo following the pattern of docs/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
}
FieldRequiredNotes
portNoSerial port (e.g. /dev/ttyUSB0, /dev/cu.usbmodem14101, COM3). Auto-detected if you have exactly one ESP device connected.
baudNoBaud rate. Default 115200 (the ESP-IDF console default — different from flash.run’s 460800).
duration_secNoHow long to capture. Default 30, capped at 600.
filterNoSubstring; only lines containing it appear in output. Useful for "heartbeat" verification.
reset_on_connectNoDefault 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 CLI espctl monitor instead.


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 flashno Python esptool.py.

espctl probe --port <port>

Inputs

FlagNotes
--portRequired. 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

ArgumentDefaultNotes
bundle_path (positional)requiredAn extracted bundle directory or flash_bundle.tar.gz.
--portrequiredSerial port.
--baud460800Flash 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

FlagDefaultNotes
--portrequiredSerial port.
--baud115200Monitor baud rate (default IDF console).
--no-reconnectfalseExit on disconnect instead of waiting for the device to come back.
--no-reset-on-connectfalseSkip 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