固件与烧录
五个工具处理构建流水线的末端——列出可用固件、下载固件、烧录到 真实设备、烧录后从设备捕获串口输出,以及(JTAG 调试用)拉取 不剥离符号的应用 ELF。
| 工具 | 做什么 |
|---|---|
firmware.list | 列出已完成的、有固件可下载的构建。 |
firmware.download | 获取某次构建固件的下载元数据。 |
elf.download | 下载某次构建的不剥离符号 ELF(给 openocd-esp32 / GDB 用)。 |
flash.run | 通过串口把固件烧录到本地连接的 ESP 设备。 |
monitor.run | 在限定时间内,从本地设备的串口捕获输出。 |
firmware.list
显示哪些构建已成功完成、有固件可以下载。
输入:
{}
可选:按特定构建过滤:
{ "job_id": "0abf...e2" }
| 字段 | 必需 | 说明 |
|---|---|---|
job_id | 否 | 只看一次构建。不填则列出所有成功的构建。 |
返回:
{
"builds": [
{
"task_id": "0abf...e2",
"target": "esp32s3",
"status": "succeeded",
"build_type": "release"
}
]
}
无副作用。 随时可以调用。
firmware.download
获取下载构建固件所需的元数据。实际的二进制传输走 firmware WebRTC DataChannel——这个工具给你制品信息。
输入:
{
"job_id": "0abf...e2"
}
| 字段 | 必需 | 说明 |
|---|---|---|
job_id | 是 | 一次成功构建的 task ID。 |
output_dir | 否 | 固件文件保存位置。 |
返回:
{
"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"
}
构建必须是 succeeded 状态。对失败或运行中的构建调用会报错。
主制品是
flash_bundle.tar.gz。 远程构建会打包出一个带签名的 自描述烧录包(manifest.json+files/,内含 bootloader、分区表、 应用三段),在同一会话里原子返回给客户端——没有单独的 fetch 步骤。flash.run和命令行espctl flash都消费这个文件。单独的.bin文件还列在artifact_lines里方便检查,但你几乎不会再直接 把它们传给烧录器。
elf.download
下载某次已完成的远程 C ESP-IDF 构建的不剥离符号应用 ELF。
用来驱动 openocd-esp32 +
xtensa-esp32sX-elf-gdb 做 JTAG 级调试 —— 断点、寄存器查看、
RTOS 线程视图。烧录包(flash_bundle.tar.gz)只带可烧录段;调试
符号在 ELF 里,由 agent 自动持久化在烧录包旁边,按需拉取。
输入:
{
"build_id": "0abf...e2",
"output_path": "/tmp/my_proj.elf",
"control_url": "https://esphome.cloud"
}
| 字段 | 必需 | 说明 |
|---|---|---|
build_id | 是 | 通过 build / build.start 提交构建时使用的 job_id。 |
output_path | 是 | ELF 写入位置。父目录不存在会自动创建。 |
control_url | 否 | 控制面 URL。未填时回落到环境变量 CONTROL_PLANE_URL。 |
返回:
{
"build_id": "0abf...e2",
"path": "/tmp/my_proj.elf",
"size_bytes": 11230544,
"sha256": "f0a63ee2...",
"next_steps": "Use this ELF with openocd-esp32 + xtensa-esp32sX-elf-gdb."
}
需要环境变量 MCP_AUTH_SECRET(作为 /mcp-session 的 Bearer
token)。复用 WebRTC firmware DataChannel 和与 flash_bundle.tar.gz
传输完全一致的分块协议(FirmwareMetadataEnvelope →
N×FirmwareChunkEnvelope → FirmwareCompleteEnvelope)。
Rust no_std 构建在 agent 上没有配套 ELF。 通过
espctl build --rust-elf提交的构建以 ELF 作为输入 —— 你本地 已经有了。对这种build_id,elf.download会直接报错。
持久化按 workspace 划分。 Agent 把 ELF 存在
<ESPCTL_WORKSPACE_ROOT>/<build_id>/<app>.elf(默认<workspace-root>/)。Workspace 会被定期 GC, 太老的build_id可能拉不到了。
flash.run
把固件烧录到连接在电脑 USB 口的 ESP 设备。底层直接用纯 Rust 的
espflash 库——不依赖 Python
esptool.py。你不需要 pip install esptool,不需要 venv,也不
需要 PATH 里有 Python。
输入:
{
"firmware_path": "/path/to/build/flash_bundle.tar.gz",
"port": "/dev/ttyUSB0",
"baud": 460800
}
| 字段 | 必需 | 说明 |
|---|---|---|
firmware_path | 是 | 一个 flash_bundle.tar.gz(由 build + firmware.download 产出)、一个已解压的烧录包目录,或者原始的 .bin / .elf 文件。推荐用烧录包形式,它在一个文件里带齐 bootloader、分区表、应用和签名清单。 |
port | 否 | 串口。只连了一个 ESP 设备时自动检测。 |
baud | 否 | 烧录波特率。默认 460800。 |
返回: 烧录操作的状态(成功或错误详情)。
传入烧录包时,flash.run 会读取 manifest.json,校验每一段的
sha256,然后在一次 espflash 会话里把所有段一次性写进 flash
(关键——如果逐段写,芯片会在第一段后重启、第二段就永远 hang)。
芯片只在最后重启一次。
政策:绝不安装
esptool.py。 如果flash.run或命令行espctl flash失败,按docs/infra-bugs-2026-04-11.md的格式在 aegis 仓库里建一份docs/espctl-flash-bugs-YYYY-MM-DD.md提 bug。 不要通过装 Python esptool 来绕开故障——那只会把真正的 build-to-flash 流水线 bug 悄悄藏起来。
仅限本地。 这个工具跑在你的电脑上,不是构建服务器上。只在 本地/stdio MCP 模式下可用——浏览器里不行。浏览器烧录用 MCP 控制台的 Flash 标签。
monitor.run
在限定时间内从连接好的 ESP 设备捕获串口输出。一般紧跟在
flash.run 之后用,验证板子启动、固件确实在跑——例如观察
向导 Phase-0 验证固件每秒发出的 heartbeat 日志行。
输入:
{
"port": "/dev/cu.usbmodem1101",
"baud": 115200,
"duration_sec": 30,
"filter": "heartbeat",
"reset_on_connect": true
}
| 字段 | 必需 | 说明 |
|---|---|---|
port | 否 | 串口(如 /dev/ttyUSB0、/dev/cu.usbmodem14101、COM3)。只连了一个 ESP 设备时自动检测。 |
baud | 否 | 波特率。默认 115200(ESP-IDF 控制台默认值——和 flash.run 的 460800 不同)。 |
duration_sec | 否 | 捕获时长。默认 30 秒,上限 600 秒。 |
filter | 否 | 子串。只有包含它的行会出现在 output 里。"heartbeat" 验证时很有用。 |
reset_on_connect | 否 | 默认 true——打开串口后拉一次 DTR/RTS 脉冲,让芯片在捕获窗口内重启进入应用。没有自动复位电路的板子,或者已经被别的工具复位过的情况,设为 false。比 espctl probe 更克制——不会进 bootloader。 |
返回:
{
"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."
}
捕获缓冲区上限约 512 KB;如果设备在窗口内输出更多,truncated
会变成 true,末尾会被截断。
仅限本地。 和
flash.run一样,只在本地/stdio MCP 模式下 可用——浏览器里不行。浏览器监视器用 MCP 控制台 — Monitor 标签 的 Web Serial。
没有 panic 解码器。 这是一份 UTF-8 lossy 的原始字节转储。 没有
idf.py monitor那种基于 ELF 的 backtrace 解码。需要长时间 交互式监视的话,用命令行espctl monitor。
CLI: espctl ports
列出操作系统看得到的所有串口。USB 串口还会带上 VID:PID。烧录或 开监视器之前,先用这个命令找到你的板子。
espctl ports
没有任何标志。如果列表为空会打印 No serial ports found.。
输出
Human 模式(表格):
PORT TYPE USB VID:PID
----------------------------------------------------------------------
/dev/cu.usbmodem1101 USB 303A:1001
/dev/cu.Bluetooth-Incoming-... Bluetooth -
JSON(--json):一个端口对象数组。USB 项还带 vid、pid、
vid_pid、manufacturer、product、serial_number。
# 过滤出 USB 串口适配器
espctl --json ports | jq '.[] | select(.vid_pid != null)'
CLI: espctl probe
对真实设备打开 bootloader 握手,报告芯片型号(含 revision)、MAC
地址和 flash 大小。底层用和 espctl flash 一样的纯 Rust
espflash 连接 ——
不依赖 Python esptool.py。
espctl probe --port <port>
输入
| 标志 | 说明 |
|---|---|
--port | 必填。不知道端口的话先跑 espctl ports。 |
输出
Human 模式:
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"
}
失败模式
- 端口不在操作系统端口列表里 → 退出 1(消息会提示先跑
espctl ports)。 - bootloader 握手失败 → 退出 1。
CLI: espctl flash
把烧录包烧到连着的设备。MCP 等价工具是 flash.run ——
同一个引擎,同一次会话内写完。
espctl flash <bundle_path> --port <port> [--baud <rate>]
标志矩阵
| 参数 | 默认 | 说明 |
|---|---|---|
bundle_path(位置参数) | 必填 | 一个已解压的烧录包目录,或 flash_bundle.tar.gz。 |
--port | 必填 | 串口。 |
--baud | 460800 | 烧录波特率。 |
示例
# 默认波特率(460800)
espctl flash ./build/flash_bundle.tar.gz --port /dev/cu.usbmodem1101
# 更快 —— 前提是 USB↔串口 转换板和数据线撑得住
espctl flash ./build/flash_bundle.tar.gz --port /dev/ttyUSB0 --baud 921600
烧录包形式(由 espctl build 产出)带一个 manifest.json(含
sha256 校验)和所有段(bootloader、分区表、应用)。烧录器在
一次 espflash 会话里把所有段写完 —— 逐段写会让芯片在第一段后
重启,第二段就 hang 死。
CLI: espctl monitor
打开串口监视器,把输出实时打到你的终端。默认情况下断开会自动 重连。
espctl monitor --port <port> [--baud <rate>] \
[--no-reconnect] [--no-reset-on-connect]
标志矩阵
| 标志 | 默认 | 说明 |
|---|---|---|
--port | 必填 | 串口。 |
--baud | 115200 | 监视器波特率(IDF 控制台默认值)。 |
--no-reconnect | false | 断开时直接退出,而不是等设备回来。 |
--no-reset-on-connect | false | 打开端口时跳过 DTR/RTS 复位脉冲。 |
关于 --no-reset-on-connect
默认行为下,monitor 会在打开端口时拉一次 RTS 复位脉冲,让芯片
在监视器下重新启动到应用。在没有自动复位电路的板子上,或者已经
有别的工具复位过、你不想再被打断的情况下,加 --no-reset-on-connect。
典型流程
远程构建 + 本地烧录 + 本地监视:
espctl build . --target esp32s3
espctl flash ./build/flash_bundle.tar.gz --port /dev/cu.usbmodem*
espctl monitor --port /dev/cu.usbmodem*
构建一步是默认远程的 —— 不需要 --remote 参数。服务器地址来自
espctl login 或默认 https://esphome.cloud。用 --remote <url>
可覆盖,用 --local 切换到仅计划模式。
CLI: espctl elf
下载某次远程构建的不剥离符号应用 ELF。MCP 等价物是
elf.download —— 同一传输,同一鉴权流程。
必须先在构建时显式 opt-in。 Agent 默认不保留 ELF —— 多 MB 的 拷贝放在每次构建之后会浪费磁盘。要让 ELF 可以后取,必须在
espctl build调用时加--elf标志。没加--elf的构建,espctl elf会以 “no ELF retained” 报错。
espctl elf --build-id <ID> [--remote <URL>] [--out <PATH>]
标志矩阵
| 标志 | 默认 | 说明 |
|---|---|---|
--build-id | 必填 | 上一次 espctl build --remote --elf 输出里的 job_id。 |
--remote | 来自 credentials,或 https://esphome.cloud | 控制面 URL 覆盖。 |
--out | 当前目录下 <build_id>.elf | ELF 输出路径。父目录不存在会自动创建。 |
典型流程 —— 通过 openocd-esp32 做 JTAG 调试
# 1. 远程构建,**加 --elf** 让 agent 保留 ELF;记下输出里的 build_id。
espctl build composite-device/firmware/my_proj --target esp32s3 \
--remote https://esphome.cloud --elf
# build_id=4f3a2c...
# 2. 拉 ELF(鉴权优先级和 `espctl build --remote` 一致:
# MCP_AUTH_SECRET 环境变量 → ~/.config/espctl/credentials.json)。
espctl elf --build-id 4f3a2c --remote https://esphome.cloud --out my_proj.elf
# 3. 自己拉起 openocd-esp32 + GDB —— espctl 不挡你的路。
openocd -f board/esp32s3-builtin.cfg -c 'gdb_port 3333'
xtensa-esp32s3-elf-gdb -ex 'target remote :3333' my_proj.elf
openocd-esp32 的板级配置(board/esp32s3-builtin.cfg、
board/esp32-wrover-kit-3.3v.cfg 等)适用于任意标准 JTAG 适配器。
常见组合:芯片自带的 USB-Serial/JTAG(S3/C3 及更新)、或者基于
FTDI 的外部调试器,例如
ESPLink V1.2 ——
它把 UART 和 JTAG 拆到独立的 USB 端点,所以即便烧了
DIS_USB_JTAG,JTAG 仍然能用。
失败模式。 在以下情况
espctl elf会报错:构建时没有传--elf(agent 主动跳过了 ELF 落地)、agent 不认识这个build_id、构建已经被 agent workspace GC、或者构建是 Rust no_std 构建(没有配套 ELF —— 用你本地已有的 ELF)。
另见
- 构建生命周期——怎么启动产生固件的构建。
- 日志与构建产物——读构建输出和 manifest 文件。
- MCP 控制台 — Flash 标签—— 浏览器烧录。