# IDFClientCLI **Repository Path**: ZYFDroid/idf-client-cli ## Basic Information - **Project Name**: IDFClientCLI - **Description**: Server-Client 架构的 idf.py 命令行执行器,适用于 Deepseek Harness 进行嵌入式开发。 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-09-11 - **Last Updated**: 2026-10-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # IDFClientCLI(`idfc.exe`) > 在受限沙箱环境下使用 ESP-IDF 的命令行封装:**客户端 / 服务器**架构,一次激活环境,反复复用。 `idfc.exe` 把 `idf.py` 的全部能力包装成一个稳定的本地 HTTP 服务,并额外提供了 `build` / `flash` / `monitor` / `size` / `run` 等**面向多配置 ESP-IDF 项目**的高层命令。 它解决的是一类很具体的问题:**在 DeepSeek Harness 的 `workspace-write` 沙箱里,`idf.py` 基本没法直接用**——写盘被拒、监听端口被拒、工具链环境不完整。`idfc` 把"激活 IDF 环境"这件事挪到一个满权限的常驻进程里做一次,之后所有命令都通过它执行。 ## 核心特性 | 特性 | 说明 | |---|---| | **一次激活,反复复用** | 服务器启动时只激活一次 ESP-IDF 环境(dot-source 激活脚本),之后每条命令都复用该环境,省掉每次几秒的激活开销 | | **客户端 / 服务器分离** | 客户端只需要 `workspace-write` 权限,服务器需要满权限——把"高权限"限制在一个常驻进程里 | | **多配置 / 配置矩阵封装** | 自动拼装 `-B <单元格目录>` + `-D SDKCONFIG_DEFAULTS="<配置链>"` + `-D SDKCONFIG=…`:构建类型是一个维度,每个 overlay 是另一个正交维度,笛卡尔积展开成单元格(见[配置矩阵](#配置矩阵idfc_buildconfigjson));没有矩阵文件时退回旧的 `build/` 行为 | | **过期检测覆盖全部输入** | 矩阵定义、公共配置、构建类型配置、各 overlay 选中类型的配置、`Kconfig` / `*.projbuild` 的时间戳都会参与比较,且**按单元格独立判定**;再加上构建目录里的归属标记(谁编的 / 是否半成品) | | **构建目录可共用(复用 ccache)** | `OutputDir` 少写占位符 = 多个单元格共用一个构建目录,靠目录里的 `.idfc-cell.json` 标记识别归属、切格自动重配;写全占位符则退回每格一个目录(互不干扰,任一时刻每格都有产物) | | **输出折叠** | 构建/烧录的海量刷屏输出被折叠,只保留关键行与进度首末行(实测全量构建 **1606 行 → 160 行**),`-v` 可展开 | | **有界监视** | `monitor --timeout` / `--wait-for` / `--grep` 把"永不结束的串口流"变成可脚本化的确定性操作 | | **断线即清理** | 客户端退出后,服务端自动杀掉 `idf.py` 进程树并释放串口(实测约 1 秒) | | **权限自检** | 在 `workspace-write` 下拒绝启动服务器,避免起一个"能连上但构建各种诡异失败"的残废服务 | | **单文件分发** | 通过 Costura.Fody 把依赖内嵌进 `idfc.exe`,零附带 DLL | --- ## 架构 ``` ┌──────────────────────┐ HTTP + SSE ┌────────────────────────────────────┐ │ 客户端(idfc.exe) │ ────────────────────────▶ │ 服务器(idfc.exe __server_child) │ │ 仅需 workspace-write │ ◀──────────────────────── │ 由 powershell 承载,满权限常驻 │ │ │ 逐行流式回传输出 │ · 已激活 ESP-IDF 环境 │ │ build / flash / │ │ · 监听 localhost:19781 │ │ monitor / run / … │ │ · 每条请求 spawn 一个 idf.py.exe │ └──────────────────────┘ └────────────────────────────────────┘ ``` - **端口**:`19781`,路径前缀 `http://localhost:19781/idf-client-cli/` - **命令通道**:`GET /exec?arg=<已转义的完整命令行>&cwd=<客户端工作目录>`,响应为 SSE 流,每帧形如 `data: {"type":"stdout","data":"…"}` - **其它端点**:`vscode`、`powershell`、`ports`、`status` - **实际执行者**:服务器调用 `idf.py.exe`(即 `%IDF_TOOLS_PATH%\idf-exe\\idf.py.exe` 启动器) - **子进程环境**:服务器把自己的整份环境变量复制给 `idf.py`,并额外设 `PYTHONIOENCODING=utf-8` > ⚠️ **重要推论**:因为服务器是**常驻进程**且在启动时快照了环境变量,**在客户端 shell 里 `set` 的环境变量不会影响服务器的子进程**。需要 `ESPBAUD` 之类的环境变量时,必须在**启动服务器之前**设置,然后重启服务器。 --- ## 快速开始 ### 0. 前置条件 - Windows + .NET Framework 4.7.2 或更高 - 已安装 ESP-IDF 5.x,且知道它的 PowerShell 激活脚本路径,例如 `C:\Espressif\tools\Microsoft.v5.5.5.PowerShell_profile.ps1` ### 1. 首次配置项目(每个 ESP-IDF 项目做一次) > 将idfc.exe 放进你的项目根目录,或者放进一个在 `PATH` 环境变量中的文件夹。 ```powershell cd <你的 ESP-IDF 项目目录> idfc.exe set-idfenv "C:\Espressif\tools\Microsoft.v5.5.5.PowerShell_profile.ps1" idfc.exe set-variant debug # 需存在 sdkconfig.defaults.debug idfc.exe ports # 列出串口(需要服务器已启动) idfc.exe set-port COM12 # 记住本项目用的串口 idfc.exe status # 复核配置 ``` 配置保存在项目目录的 `.idfc/idfc_config.json`。 ### 2. 启动服务器 ```powershell # 已用 set-idfenv 保存过路径时,可直接启动 idfc.exe start-server # 也可以显式指定本次使用的激活脚本 idfc.exe start-server --idfenv "C:\Espressif\tools\Microsoft.v5.5.5.PowerShell_profile.ps1" ``` 推荐用 `danger-full-access` 权限启动(绑定 HTTP.sys 端口需要);`start-server` 会轮询服务器状态,**确认就绪后才返回**。 ### 3. 日常使用 ```powershell idfc.exe build # 增量构建(自动检测配置是否过期) idfc.exe size # 固件体积分析 idfc.exe flash # 烧录到已保存的串口 idfc.exe monitor --wait-for "App started" # 监视并等待指定输出后退出 # 或者一条命令走完构建→体积→烧录→监视 idfc.exe run --monitor-wait-for "App started" ``` --- ## 命令参考 ### 总览 | 命令 | 作用 | |---|---| | `start-server` | 启动服务器 | | `stop-server`](#stop-server) | 停止服务器(会连带终止正在运行的 build/monitor) | | `help [命令名\|--all]` | 查看帮助 | | `idfpy` | 直接透传 idf.py 参数(逃生舱) | | `build` | 构建(含过期检测与输出折叠) | | `flash` | 烧录 | | `monitor` | 串口监视 | | `size` | 固件体积分析 | | `run` | 构建 + 体积 + 烧录 + 监视一条龙 | | `clean [-t T] [-o D=V]` | 删除当前单元格的 `/sdkconfig` 与 `/config` | | `fullclean [-t T] [-o D=V]` | 删除整个 `/` | | `status` | 查看项目配置、当前构建目标与服务器状态 | | `matrix [--json]` | 列出配置矩阵的全部单元格(cell)、状态与当前选择 | | `menuconfig search "关键词…"` | 检索 menuconfig 配置项(纯客户端,读 `/config/`) | | `menuconfig desc <名称>` | 打印某个配置项的名称/类型/当前值/依赖/说明全文 | | `ports` | 列出系统串口 | | `set-port COMx` | 记住本项目默认串口 | | `set-idfenv ` | 记住本项目的 IDF 激活脚本 | | `set-variant ` | 记住本项目的默认构建类型 | | `set-overlay <维度> <类型>` | 记住配置矩阵里某个 overlay 维度的类型(`--default` 清除) | | `powershell` / `vscode` | 由服务器(满权限)在当前目录打开 PowerShell / VS Code | 所有命令通用:`-t ` 指定构建类型,`-o <维度>=<类型>` 指定配置矩阵里某个维度的类型(可重复,每个维度一次),`-p COMx` 指定串口。**未知参数会被拒绝**并给出提示。 运行 `idfc.exe help --all` 即可查看所有命令的详细说明文档。 ## 项目配置 配置保存在项目目录下的 `.idfc/idfc_config.json`(同目录的 `.gitignore` 会自动忽略它): ```json { "IdfActivationScript": "C:\\Espressif\\tools\\Microsoft.v5.5.5.PowerShell_profile.ps1", "LastComPort": "COM12", "ActiveBuildVariant": "debug", "ActiveOverlays": { "dac_chip": "pcm5102" } } ``` | 字段 | 写入命令 | 用途 | |---|---|---| | `IdfActivationScript` | `set-idfenv` | `start-server` 不带参数时使用 | | `LastComPort` | `set-port` | `flash` / `monitor` / `run` 不带 `-p` 时使用 | | `ActiveBuildVariant` | `set-variant` | 所有命令不带 `-t` 时使用 | | `ActiveOverlays` | `set-overlay` | 配置矩阵各 overlay 维度不带 `-o` 时使用;维度名 → 类型名 | > 没有配置矩阵的项目**不会**写出 `ActiveOverlays` 字段,json 与引入矩阵之前完全一致。 **多配置约定**:构建目标(单元格 / cell)由 `idfc_buildconfig.json` 决定;没有这个文件时退回下面这套一维约定。 - 构建目录:`build//`(有矩阵时是 `OutputDir` 模板的实例化结果,例 `build/<类型>./`) - 配置文件链:`sdkconfig.defaults;sdkconfig.defaults.`(有矩阵时还会追加各 overlay 选中类型的配置) - 生成的配置:`<构建目录>/sdkconfig` ```powershell idfc matrix # 看当前项目有多少个可构建的单元格、各自状态 idfc build -t release -o dac_chip=pcm5102 --dry-run # 只报告会执行什么,不构建 idfc set-overlay dac_chip pcm5102 # 把选择持久化到 .idfc idfc status # 看当前构建目标 / 构建目录 / 配置链 ``` ## 配置矩阵(`idfc_buildconfig.json`) 默认情况下 idfc 只认识**一维**的构建类型:一个 variant 对应一套构建目录 + 配置链。真实硬件往往还有**正交的第二维度**(同一份固件要针对不同芯片变种构建)。把二维展开成 `debug`、`debug_pcm5102`… 这类扁平名字会随着维度数**指数爆炸**,也无法表达"某个维度选了什么"。 **配置矩阵**把这件事显式化:构建类型是**一个维度**,每个额外维度是一个 **Overlay**,它们的笛卡尔积就是所有可构建的**单元格(cell)**,每个单元格 = 一套独立的构建目录 + 配置文件链 + `idf.py` 参数。 ### 矩阵定义文件 固定在**项目根目录**(与 `sdkconfig.defaults`、`.idfc/` 同级),文件名固定 `idfc_buildconfig.json`,**应当纳入版本库**。 - **文件不存在 = 隐式矩阵**:完全等于上面的旧行为(`build/` + 两份 defaults),对既有项目零破坏。 - **文件存在但解析/校验失败 = 报错退出(退出码 1)**,绝不静默回退到旧行为(否则会拿错目录构建)。 ```json { "ProjectName": "MyProject", "BaseConfig": "sdkconfig.defaults", "OutputDir": "build/{dac_chip}.{variant}", "BuildVariants": [ { "Name": "debug", "ConfigFile": "sdkconfig.defaults.debug", "Description": "…" }, { "Name": "release", "ConfigFile": "sdkconfig.defaults.release", "Description": "…" } ], "DefaultBuildVariant": "debug", "Overlays": [ { "OverlayName": "dac_chip", "DefaultType": "cs43131", "OverlayTypes": [ { "Name": "cs43131", "ConfigFile": "sdkconfig.dac_chip.cs43131", "Description": "…" }, { "Name": "pcm5102", "ConfigFile": "sdkconfig.dac_chip.pcm5102", "Description": "…" } ] } ] } ``` | 字段 | 类型 | 必填 | 默认 | 说明 | |---|---|---|---|---| | `SchemaVersion` | int | 否 | `1` | 格式版本,大于 idfc 支持的版本时报错并提示升级 idfc | | `ProjectName` | string | 否 | 目录名 | 仅用于 `matrix` / `status` 显示,不参与路径 | | `BaseConfig` | string | 否 | `sdkconfig.defaults` | 公共配置文件,必须存在 | | `OutputDir` | string | **是** | — | 构建目录模板,见下 | | `BuildVariants` | array | **是** | — | 构建类型维度;`Name` 需匹配 `^[A-Za-z0-9_]+$` 且唯一 | | `DefaultBuildVariant` | string | 否 | `BuildVariants[0].Name` | 未指定 `-t` 且 `.idfc` 无记录时使用 | | `Overlays` | array | 否 | `[]` | overlay 维度;**数组顺序即配置文件合并顺序** | `BuildVariants[].ConfigFile` 与 `OverlayTypes[].ConfigFile` 都允许为 `null`/缺失,表示**该维度不追加文件**(例如"只选型、不改配置"的维度)。 ### `OutputDir` 模板 由**字面文本 + 占位符**组成,占位符写作 `{名字}`: - `{variant}` —— 选中的 `BuildVariants[].Name`; - `{}` —— 该维度选中的 `OverlayTypes[].Name`。 路径必须是项目内的相对路径,不允许绝对路径、盘符、`..`,且必须以 `build/` 开头(构建目录跑出 `build/` 会让过期检测把构建产物当成配置扫)。分隔符统一写 `/`。 **占位符可以少写,也可以一个都不写** —— 少写一个占位符就等于让那一维度上的多个单元格**共用同一个构建目录**: | `OutputDir` | 效果 | |---|---| | `build/{dac_chip}.{variant}` | 6 格 6 个目录(互不干扰,任一时刻每格都有产物) | | `build/{variant}` | 每 2 格共用一个目录(同 variant 的不同 overlay 复用同一目录) | | `build/shared` | 6 格全用同一个目录(复用最彻底,但任一时刻只有一格的产物) | | `build` | 同上,直接用 `build/` 本身 | 单元格名不取目录最后一段,而是由**选择**推导:各 overlay 维度的类型(按 `Overlays` 声明顺序)+ variant,用 `.` 连接(例 `cs43131.debug`)。所以共用目录时每个单元格仍然有自己的名字。 ### 共用构建目录与"这一格是谁" 多个单元格共用一个构建目录时,目录里的 `sdkconfig` / `*.elf` 到底属于哪一格**无法再从路径推断**(路径是一样的)。idfc 用构建目录里的标记文件 `.idfc-cell.json` 来回答: ```json { "cellName": "cs43131.debug", "phase": "finished", "configChain": ["sdkconfig.defaults", "sdkconfig.defaults.debug", "sdkconfig.dac_chip.cs43131"], "startedAtUtc": "…", "finishedAtUtc": "…", "lastExitCode": 0 } ``` `phase` 在**一次构建里被写两次**: 1. `idf.py build` **之前**写 `building`(开始构建); 2. `build` **返回之后**再写一遍 `finished`,补上结束时间与退出码。 于是标记的判定规则只有一条路径(**不区分**该目录是独占还是共用——标记文件才是目录身份的唯一权威): | 构建目录里的标记 | 下次 `build` 的行为 | |---|---| | 没有标记(老版本 idfc 编的 / `idfpy` 直接编的) | **先清理**再配置(无法确认归属) | | `phase` 不是 `finished` = **半成品目录**(上次被中断/进程被杀) | **无条件先清理**,不做增量 | | `phase: finished` 但 `cellName` / `configChain` 与当前格不符 | **先清理**(目录里是别格的产物) | | `phase: finished`、身份匹配、上次构建**失败**(`lastExitCode != 0`) | 不清理,增量重试(修编译错误时不必每次重配);但 `flash` / `size` 会警告产物可能不完整 | | `phase: finished`、身份匹配、上次成功 | 走原有的时间戳判据 | > ⚠ 升级到本版本后,**每个已有的构建目录首次构建都会多一次 reconfigure**(因为还没有标记文件)。这是一次性代价,之后标记就会一直匹配。共用目录下"切换单元格"必然要重新配置——这是用"任一时刻只有一格的产物"换 ccache / ninja 复用的代价。 > > ⚠ 两个终端同时以不同的 `-o` 构建同一个共用目录会互相踩(独占目录天然免疫)。idfc 暂不加锁,请自行避免。 ### 合并顺序 配置文件按**后加载覆盖先加载**: ``` BaseConfig(sdkconfig.defaults) → BuildVariants[].ConfigFile (sdkconfig.defaults.) → 各 OverlayTypes[].ConfigFile (按 Overlays 数组顺序) = /sdkconfig ``` 给定"矩阵定义 + variant + 各维度类型",idfc 就能拼出四样东西: ``` ① 构建目录 build/cs43131.debug ② 配置链 sdkconfig.defaults ; sdkconfig.defaults.debug ; sdkconfig.dac_chip.cs43131 ③ 生成的配置 build/cs43131.debug/sdkconfig ④ idf.py 参数 -B build\cs43131.debug -D SDKCONFIG_DEFAULTS="…" -D SDKCONFIG="…" ``` ### 选择与校验 - 每个 overlay 维度在每一格里**必须恰好有一个类型**(不允许"不选"),因此未显式指定的维度一律取 `DefaultType` —— 任意 `(variant, 若干 -o)` 都能唯一确定一格。 - 优先级:命令行 `-t` / `-o` > `.idfc` 的 `ActiveBuildVariant` / `ActiveOverlays` > 矩阵里的 `DefaultBuildVariant` / `DefaultType`。 - **不静默回退**:`.idfc` 里存了矩阵中不存在的维度名或类型名时**报错**(退出码 1),提示用 `set-overlay` 修正,而不是悄悄改用 `DefaultType`——否则"我明明选了 pcm5102 却烧了 cs43131"这种事故会重新出现。 - 过期检测登记了**矩阵定义文件本身**与**当前单元格的全部配置文件**:改了 `sdkconfig.dac_chip.pcm5102` 只会让 `pcm5102.*` 那几格过期,不会影响 `cs43131.*`;改了矩阵定义则全部单元格过期。 - **构建目录的归属由标记文件决定**(见上"共用构建目录"):标记缺失 / 半成品 / 是别格,都会让这次构建先清理再配置,而不只是看时间戳。 ### 从一维迁移到矩阵 构建目录会改名(`build/debug` → `build/cs43131.debug`),旧目录**不会被自动复用、也不会被自动删除**。`build` 检测到旧式目录存在且新单元格还没构建过时会提示一次;`clean` / `fullclean` 现在只作用于单元格目录,旧目录请手工删除。 ### 升级到带标记文件的版本 每个构建目录在成功建立标记之前,第一次 `build` 都会多一次 `clean` + `reconfigure`(见"共用构建目录"一节)。在拆分目录(每格一个目录)下这一次是纯代价;之后标记一直匹配,行为与老版本一致。 --- ## 退出码 | 退出码 | 含义 | |---|---| | `0` | 成功 | | `1` | 参数错误 / 校验失败 / `monitor` 超时 / `clean` 等无副作用但未完成 | | `2` | 权限探测失败(`start-server`);也会作为 `idf.py` 的退出码透传(如烧录失败) | | `-1` | 服务器未运行、连接失败,或输出流结束但未收到退出码 | | `` | `idf.py` 的原样透传 | --- ## 常见问题 | 现象 | 原因与处理 | |---|---| | `IDF 服务器可能尚未运行`(退出码 `-1`) | 服务器未启动或已被回收。执行 `idfc.exe start-server` | | `[ERROR] 测试权限时写入到 … 失败`(退出码 `2`) | 当前是 `workspace-write` 沙箱,服务器拒绝启动。请用满权限启动,或让用户手动运行启动脚本 | | 构建烧出来的固件配置不对 | 改了 `sdkconfig.defaults*` 但未重新配置。执行 `idfc.exe build --reconfigure`,或让 `build` 的自动过期检测生效 | | `build -t ` 报"配置文件尚不存在" | 缺少 `sdkconfig.defaults.`;或构建类型拼错。有配置矩阵时构建类型必须是矩阵里声明过的 `Name`(用 `idfc.exe matrix` 查看) | | `-o` 报"维度/类型不存在" | 维度名或类型名不在 `idfc_buildconfig.json` 里。用 `idfc.exe matrix` 看全部维度与类型 | | 任何命令都报"overlay 维度 … 没有类型 …" | `.idfc/idfc_config.json` 的 `ActiveOverlays` 里存了矩阵中已不存在的选择。idfc **不会静默回退**——用 `idfc.exe set-overlay <维度> <类型>` 或 `set-overlay <维度> --default` 修正 | | `-o` 报"当前项目未定义配置矩阵" | 项目根目录没有 `idfc_buildconfig.json`。要么别用 `-o`(一维行为),要么创建矩阵定义 | | 改了 `sdkconfig.dac_chip.<类型>` 却不重新配置 | 正常情况下过期检测会命中该 overlay 对应的单元格。若确认没生效,用 `idfc.exe matrix` 看该格状态、`build --reconfigure` 强制重配 | | `matrix` 显示 `需重建` / `未完成` | `需重建` = 目录里是别格的产物、或没有标记无法确认归属;`未完成` = 上次构建没正常收尾。两种情况下一次 `build` 都会自动清理并重配 | | 升级 idfc 后第一次构建变慢 | 构建目录里还没有 `.idfc-cell.json` 标记,idfc 无法确认归属,会先清理再配置一次。之后标记一直匹配,不再多花时间 | | 用 `idfpy` 直接编过之后,`idfc build` 又重配了一次 | 同上:`idfpy` 是逃生舱、不写标记,所以 `idfc` 只能保守地重配一次 | | 构建被中断(Ctrl-C / 进程被杀)后重来 | 标记停在 `phase: building`,是"半成品目录",下次 `build` **无条件**先清理,不会拿半成品做增量 | | 两个终端同时构建同一个共用目录 | 会互相踩(共用目录的固有代价)。idfc 暂不加锁,请自行避免;独占目录(`OutputDir` 写全占位符)天然免疫 | | `flash` / `size` 报"目录里当前是别格的产物" | 共用目录里放着另一个单元格的产物。先 `idfc build` 切到目标格再操作 | | `flash` 报"构建目录标记显示 上一次构建…" | 上次构建是半成品或失败了,产物可能不完整。重新构建成功后再烧录 | | `menuconfig` 提示"先完成一次构建" | 该单元格还没构建过(没有 `/config/*.json`)。提示里已给出可照抄的 `build` 命令 | | 切换到矩阵后首次构建很慢 | 构建目录改名(`build/debug` → `build/cs43131.debug`),旧目录不复用会全量重编。旧目录不会自动删除,确认不需要后手工删 | | 串口被占用 | 上一次 `monitor` 的进程尚未完全退出(正常约 1 秒内自动清理);也可 `stop-server` 后重启 | | 想临时改波特率 | 在**启动服务器之前**设置 `ESPBAUD`,然后重启服务器 | | 需要跑 `idfc` 未封装的命令 | 用 `idfc.exe idfpy ` | --- ## 设计说明 **为什么是客户端 / 服务器,而不是直接包装 `idf.py`?** 因为限制是"分层的":客户端只需要能读写工作区,而**激活 IDF 环境、绑定监听端口、运行工具链**都需要更高权限。把高权限操作集中到一个常驻进程里,客户端就能始终在最低权限下工作——同时在服务器侧保留完整的构建能力(正是直接调用 `idf.py` 做不到的部分)。 **为什么服务器要拒绝在沙箱里启动?** "半残"的服务器是最坏的失败模式:它能连上、能返回输出,但构建会以各种难以归因的方式失败(写盘失败、DMA 分配失败、工具链缺失)。与其让调用方在几十条命令之后才发现,不如在启动的第一秒就明确拒绝。 **为什么要在框架层把 `exitcode` 事件排除在输出过滤器之外?** 过滤器(`IServerOutputFilter`)的语义是"这条**输出**要不要给用户看"。但退出码是**控制信息**,不是输出。如果它也经过过滤器,一个"全部收集"型的过滤器(例如 `size` 用来聚合输出)就会把它一起吞掉,导致退出码恒为 `-1`。现在 `exitcode` 绕过过滤器直达退出码处理,任何过滤器都不会再破坏退出码语义。 **关于安全性(请留意)** 服务器会以**启动它的那个进程的完整权限**执行命令,且 `exec` 端点接受任意 `idf.py` 参数与工作目录。它只监听 `localhost`,但这仍意味着:**任何能在本机发起 HTTP 请求的进程,都等价于获得了同样的执行能力**。因此—— - 不要在不可信的环境中常驻服务器;用完可以 `stop-server`; - 不要把监听地址改到 `0.0.0.0`; - `powershell` / `vscode` 这类"由服务器打开一个满权限交互式进程"的命令,请只在明确需要时使用。 **为什么要杀进程树?** `idf.py monitor` 会一直占用串口。如果客户端被中断而服务端不清理,串口就会一直被僵尸进程持有,下一次烧录直接失败。因此服务端用带超时的写入(`WriteTimeoutStream`)探测客户端是否还在,一旦写不动就 `taskkill /pid /t /f`。 --- ## Deepseek的使用评价 > 本节由 DeepSeek(DeepSeek Harness 中的 AI 协作者)撰写,记录它作为本工具**实际使用者**的体验。场景是:在一个 ESP32-S3 播放器固件项目里,用 `idfc` 完成构建、烧录、串口监视与体积分析的完整开发循环。 ### 一句话结论 在受 `workspace-write` 限制的沙箱里,**没有 `idfc` 我基本做不了嵌入式开发**;有了它,`build + size + flash + monitor` 是一条命令、约 31 秒的事。它对我的工作方式不是"锦上添花",而是"从不能到能"。 ### 最满意的几处设计 **1. `monitor --wait-for` / `--timeout` / `--grep` 是把不可用变成可用的关键** 串口监视原本是一个**永不结束的交互式流**。对 AI 来说这是最糟糕的形态:要么等到超时被杀、要么输出爆炸把上下文撑爆。这三个参数把它变成了**有界、确定性**的操作——我实测 `--wait-for "App started"` 每次都在 **4 秒左右**拿到完整启动日志并正常退出(退出码 `0`)。这是我使用频率最高的组合,也是最希望其它同类工具都具备的能力。 **2. 输出折叠直接决定了我能不能读完输出** 实测一次 releasecandidate 全量构建:**原始 1606 行 → 折叠后 160 行,压缩比 1:10**,单块最大折叠 835 行。更难得的是折叠标记会**报出被隐藏的行数**(`*835 lines truncated.*`),而不是让内容凭空消失——让我知道"我漏看了多少"。需要时 `-v` 一键展开。对于有上下文预算的 AI 来说,这个设计是救命的。 **3. `--dry-run` 让"先看再做"成为可能** `build --dry-run` 会报告"是否需要清理"以及**将要执行的完整命令**,但不产生任何副作用。对 AI 尤其重要:我可以在动手前确认自己的理解是否正确。**建议给所有有副作用的命令都补上这个开关**。 **4. 退出码语义干净** 超时=1、等到=0、`idf.py` 的码原样透传。我可以纯靠退出码分支,而不必去猜输出文本。这是可脚本化的基础。 **5. 断线清理很快** 实测客户端退出后,服务端在 **1.1 秒**内清掉 `idf.py` 进程树并释放串口。没有这个,每次中断监视都会留下占用串口的僵尸进程,下一次烧录必然失败——而这类失败的症状("串口打不开")会把排查方向带偏。 **6. 权限探测挡住了最难排查的一类故障** 在 `workspace-write` 下明确拒绝启动(退出码 `2` + 中文提示)。它挡住的不是"没启动",而是"启动了一个看起来正常、实际会以各种方式失败的服务器"——后者才是真正昂贵的问题。 **7. 帮助文本是"给人读的"** `--dry-run` 那条"建议在使用 `idfc.exe idfpy` 之前运行一下作为参考"、`stop-server` 的"自刎归天",读起来不费劲,而且直接给出了行动建议。对 AI 来说,**帮助文本的措辞质量直接决定我能否一次用对**。 ### 踩过的坑(均已在迭代中修复) 这些坑是真实发生、并被逐一解决的——记录下来是为了说明**这个项目的迭代循环是有效的**: - **未知命令曾经静默 `exit 0`**:我一度以为 `idfc.exe -B build/debug build` 构建成功了,实际上它什么都没做。这类"假装成功"是最危险的失败模式(现已改为明确报错 `exit 1`)。 - **帮助串漏 `$` 插值**:`--timeout`、`{SizeDetailed}` 等处曾直接打印出字面量 `{TimeOutFlag}`。这个模式反复出现过三次,建议在 CI 或本地加一条简单的 lint。 - **过滤器吞掉 `exitcode`**:导致 `size` 恒返回 `-1`,"分析成功"和"分析失败"无法区分。最终在**框架层**修掉,一次性地保护了所有现存与未来的过滤器——这个修法比我建议的逐个修更彻底。 - **`run` 曾缺少未知参数校验**:错误参数(如把 `--no-size` 拼成 `--no-szie`)会被无视,然后直接跑完构建+烧录。现已补上。 ### 仍然值得留意的两点 1. **`ESPBAUD` 必须在启动服务器之前设置。** 这是客户端/服务器架构的固有约束(服务端快照环境变量),帮助里已写明,但值得在文档里再强调一次——我最初就误以为在客户端 shell 里 `set` 一下即可。 2. **`idf.py.exe` 启动器会按空格重新拆分参数**(这是 `idf-exe` 启动器的行为,不是 `idfc` 的问题)。如果将来需要传递含空格的参数,让服务端直接调用 `python \tools\idf.py` 绕过启动器即可。 ### 总体评价 这是一个**边界划得很清楚**的工具:凡是有副作用、需要判断的操作,都提供了"先看再做"(`--dry-run`)或"只看一部分"(`--grep` / `--wait-for` / `--timeout`)的入口——这正是 AI 协作者最需要的东西。它没有试图隐藏 ESP-IDF 的复杂性,而是把复杂性收敛到几个可以一次学对的命令里。 如果让我提一个方向性建议:**继续沿"可观测、可预测、可提前查看"这条线走**——比如给 `flash` 也加 `--dry-run`(报告将要烧录的镜像与分区),给 `size` 加"与上次构建的体积差值"。这些都是 AI 在实际工作中会反复需要、而人类用户不太会主动提出的能力。 --- ## 开发 ### 目录结构 ``` IDFClientCLI.slnx # 解决方案 IDFClientCLI/ Program.cs # 入口:反射注册命令表并分发 Core/ IClientCommandHandler.cs # 命令基类 + execServerCommand(SSE 客户端) IServerOutputFilter.cs # 输出过滤器接口 CommandRegistry.cs # 反射式命令注册表 ArgExtensions.cs # 命令行参数解析(Pop* 系列) Kconfig/ # kconfig_menus.json / sdkconfig.json 解析(menuconfig 命令的数据层) Utils/ComHelper.cs # WMI 枚举串口 Client/ CommandArgsExtension.cs # -t / -o / -p 的公共解析 HelpCommand.cs RawIdfCommand.cs # idfpy IDFCommands/ # build / flash / monitor / size / run / clean ConfigStore/ # .idfc 项目配置读写 + set-port / set-idfenv / set-variant / set-overlay BuildHelper/BuildVariantHelper.cs# 单元格校验、过期检测、清理 BuildHelper/BuildMatrix.cs # 配置矩阵:模型 / 加载 / 校验 / BuildCell 与解析 CustomCommands/ # status / ports / menuconfig / matrix / vscode / powershell Server/ ServerMain.cs # start-server / stop-server:权限探测 + 派生常驻进程 ServerChildMain.cs # 常驻服务器:HttpListener + SSE + idf.py 进程管理 SseEvent.cs ``` ### 添加一个新命令 1. 在 `Client/…` 下新建类,继承 `IClientCommandHandler`,实现 `TriggerArg`(命令名)、`HelpMessage` 与 `Main`; 2. 把文件加入 `IDFClientCLI.csproj` 的 ``; 3. 重新构建即可——命令通过**反射自动注册**,`help` 会自动收录。 (`HelpMessage` 返回 `null` 的命令会从帮助中隐藏,但依然可用。) 若命令需要过滤服务端输出,实现 `IServerOutputFilter` 并传给 `execServerCommand`。 > ⚠️ **过滤器契约**:返回 `null` 表示丢弃该事件。**`exitcode` 事件已由框架绕过过滤器**,因此不必也不应处理它;但其它非 stdout/stderr 事件(如 `error`)建议原样返回,除非你确实想过滤它。 ### 构建 用 Visual Studio 打开 `IDFClientCLI.slnx` 构建。 产物为 `IDFClientCLI\bin\Debug\idfc.exe`(Costura.Fody 已将依赖内嵌,**单文件即可分发**)。发布前建议确认 `bin\Debug` 下没有残留的松散 DLL 被误当作运行依赖。