firmwareloop 0.7.2__tar.gz → 0.7.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/PKG-INFO +11 -6
  2. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/README.md +9 -4
  3. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/firmwareloop.egg-info/PKG-INFO +11 -6
  4. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/firmwareloop.egg-info/requires.txt +1 -1
  5. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/pyproject.toml +2 -2
  6. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/__init__.py +1 -1
  7. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/fw_mcp_server.py +29 -8
  8. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/lib/ndtdbg.py +111 -9
  9. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/lib/serial_assistant.py +73 -0
  10. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/ndtdbg_cli.py +31 -6
  11. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/serial_cli.py +17 -5
  12. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/setup-agent-mcp.ps1 +2 -2
  13. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/LICENSE +0 -0
  14. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/firmwareloop.egg-info/SOURCES.txt +0 -0
  15. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/firmwareloop.egg-info/dependency_links.txt +0 -0
  16. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/firmwareloop.egg-info/entry_points.txt +0 -0
  17. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/firmwareloop.egg-info/top_level.txt +0 -0
  18. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/setup.cfg +0 -0
  19. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/acceptance-scenario.ps1 +0 -0
  20. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/build.ps1 +0 -0
  21. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/can.ps1 +0 -0
  22. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/check-qoder-mcp.ps1 +0 -0
  23. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/common/build-backends.psm1 +0 -0
  24. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/common/fw.psm1 +0 -0
  25. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/common/uart_probe.py +0 -0
  26. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/doctor.ps1 +0 -0
  27. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/flash.ps1 +0 -0
  28. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/instrument_cli.py +0 -0
  29. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/lib/__init__.py +0 -0
  30. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/lib/instruments.py +0 -0
  31. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/logic_capture.ps1 +0 -0
  32. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/logic_decode.ps1 +0 -0
  33. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/ndtdbg_bridge.ps1 +0 -0
  34. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/reset.ps1 +0 -0
  35. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/sigrok_csv_to_fwloop.py +0 -0
  36. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/test-backends.ps1 +0 -0
  37. {firmwareloop-0.7.2 → firmwareloop-0.7.3}/tools/test.ps1 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)
5
5
  Author-email: WeilinZhang <19523330249@163.com>
6
6
  License: MIT
@@ -18,7 +18,7 @@ Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: pyvisa>=1.14.0
21
- Requires-Dist: pyvisa-py>=0.7.2
21
+ Requires-Dist: pyvisa-py>=0.7.3
22
22
  Requires-Dist: pyserial>=3.5
23
23
  Requires-Dist: pytest>=8.0.0
24
24
  Requires-Dist: pyocd>=0.36.0
@@ -383,7 +383,7 @@ claude mcp remove --scope user agentic-hil
383
383
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
384
384
  ├── demo-make/ 示例固件(Make;构建测试载体)
385
385
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
386
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.2)
386
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.3)
387
387
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
388
388
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
389
389
  ├── CLAUDE.md Claude Code CLI 指南
@@ -393,7 +393,7 @@ claude mcp remove --scope user agentic-hil
393
393
 
394
394
  ---
395
395
 
396
- ## 能力验证状态(v0.7.2
396
+ ## 能力验证状态(v0.7.3
397
397
 
398
398
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
399
399
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -416,7 +416,9 @@ claude mcp remove --scope user agentic-hil
416
416
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
417
417
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
418
418
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
419
- | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1 |
419
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
420
+ | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
421
+ | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
420
422
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
421
423
 
422
424
  ---
@@ -435,8 +437,11 @@ claude mcp remove --scope user agentic-hil
435
437
  | 串口 `SERIAL_PORT_ERROR: ... PermissionError(13)` | 端口被 GUI 上位机独占(NDT Debug Tool / STDataCollection.exe、串口助手)→ 关闭占用方后 `fw_serial_open`;残留会话用 `fw_serial_close`(已自动等待/强杀进程,端口释放可验证) |
436
438
  | `fw_serial_status` 的 `error_class` 为 `UART_BUSY` | 会话未打开或已被占用 → 先 `fw_serial_open`;残留会话 `fw_serial_close` 清理 |
437
439
  | `fw_serial_*` 报 `PermissionError(13)` / `rx_bytes: 0` | 串口被独占:**退出 NDT Debug Tool GUI(STDataCollection.exe)**——它打开时会同时占住 Box 串口与 USB-IIC 通道;失败信封/`fw_serial_status` 的 `suspected_holders` 会直接列出嫌疑进程(注意别杀 MCP server 的 python) |
438
- | 烧录"擦除生效(probe 从非 00 变 00)但校验读回全 FF"、`pages_written` 已发出 | v0.7.1 及更早的逐笔 serve 路径问题 → v0.7.2 起默认走 `flash_engine: bridge`(桥内单体,与 GUI 同进程模型);要复现/排查旧行为用 `flash_engine: "serve"` |
440
+ | 烧录"擦除生效(probe 从非 00 变 00)但校验读回全 FF"、`pages_written` 已发出 | v0.7.1 及更早的逐笔 serve 路径问题 → v0.7.3 起默认走 `flash_engine: bridge`(桥内单体,与 GUI 同进程模型);要复现/排查旧行为用 `flash_engine: "serve"` |
439
441
  | 长调用(capture/measure)客户端超时 | MCP stdio 循环是**单线程**的:长调用会让同会话其它调用排队(含 3s 的小调用)→ 缩短窗口,或用 `fw_ndtdbg_stream_start` job 模式 |
442
+ | 烧录报 `INCOMPLETE_FLASH` | 桥内 flash 连续 30s 无进度(疑似卡在写入中途)→ **芯片处于半擦写态,勿直接使用,必须重新完整烧录**;信封带 `chip_state: partially_erased` 与 `post_kill_power_cycle`(已尝试复位) |
443
+ | 烧录报 `COLD_BOOT_VERIFY_FAILED` | **Flash 字节已通过逐窗校验,但断电重上电后目标没有有效应答/与期望不符**(现场 `.18` 类问题:GUI 报成功、当场可读,冷启动 `Fw_ver = -1`)→ 按固件冷启动路径排查(看门狗/时钟/外设初始化);或用 `cold_boot_expect` 指定期望值、`cold_boot_check: false` 关闭 |
444
+ | `fw_serial_status` 的 `port_probe.openable = false` | 端口当前**无法独占打开**(硬结论)→ 看 `holder_candidates`(`source: cmdline` 比 `name` 准,候选≠持有者)确认占用方 |
440
445
 
441
446
  完整速查表与手工部署细节见 [`Fwloop_setup_pack/README.md`](Fwloop_setup_pack/README.md)。
442
447
 
@@ -355,7 +355,7 @@ claude mcp remove --scope user agentic-hil
355
355
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
356
356
  ├── demo-make/ 示例固件(Make;构建测试载体)
357
357
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
358
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.2)
358
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.3)
359
359
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
360
360
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
361
361
  ├── CLAUDE.md Claude Code CLI 指南
@@ -365,7 +365,7 @@ claude mcp remove --scope user agentic-hil
365
365
 
366
366
  ---
367
367
 
368
- ## 能力验证状态(v0.7.2
368
+ ## 能力验证状态(v0.7.3
369
369
 
370
370
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
371
371
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -388,7 +388,9 @@ claude mcp remove --scope user agentic-hil
388
388
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
389
389
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
390
390
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
391
- | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1 |
391
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
392
+ | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
393
+ | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
392
394
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
393
395
 
394
396
  ---
@@ -407,8 +409,11 @@ claude mcp remove --scope user agentic-hil
407
409
  | 串口 `SERIAL_PORT_ERROR: ... PermissionError(13)` | 端口被 GUI 上位机独占(NDT Debug Tool / STDataCollection.exe、串口助手)→ 关闭占用方后 `fw_serial_open`;残留会话用 `fw_serial_close`(已自动等待/强杀进程,端口释放可验证) |
408
410
  | `fw_serial_status` 的 `error_class` 为 `UART_BUSY` | 会话未打开或已被占用 → 先 `fw_serial_open`;残留会话 `fw_serial_close` 清理 |
409
411
  | `fw_serial_*` 报 `PermissionError(13)` / `rx_bytes: 0` | 串口被独占:**退出 NDT Debug Tool GUI(STDataCollection.exe)**——它打开时会同时占住 Box 串口与 USB-IIC 通道;失败信封/`fw_serial_status` 的 `suspected_holders` 会直接列出嫌疑进程(注意别杀 MCP server 的 python) |
410
- | 烧录"擦除生效(probe 从非 00 变 00)但校验读回全 FF"、`pages_written` 已发出 | v0.7.1 及更早的逐笔 serve 路径问题 → v0.7.2 起默认走 `flash_engine: bridge`(桥内单体,与 GUI 同进程模型);要复现/排查旧行为用 `flash_engine: "serve"` |
412
+ | 烧录"擦除生效(probe 从非 00 变 00)但校验读回全 FF"、`pages_written` 已发出 | v0.7.1 及更早的逐笔 serve 路径问题 → v0.7.3 起默认走 `flash_engine: bridge`(桥内单体,与 GUI 同进程模型);要复现/排查旧行为用 `flash_engine: "serve"` |
411
413
  | 长调用(capture/measure)客户端超时 | MCP stdio 循环是**单线程**的:长调用会让同会话其它调用排队(含 3s 的小调用)→ 缩短窗口,或用 `fw_ndtdbg_stream_start` job 模式 |
414
+ | 烧录报 `INCOMPLETE_FLASH` | 桥内 flash 连续 30s 无进度(疑似卡在写入中途)→ **芯片处于半擦写态,勿直接使用,必须重新完整烧录**;信封带 `chip_state: partially_erased` 与 `post_kill_power_cycle`(已尝试复位) |
415
+ | 烧录报 `COLD_BOOT_VERIFY_FAILED` | **Flash 字节已通过逐窗校验,但断电重上电后目标没有有效应答/与期望不符**(现场 `.18` 类问题:GUI 报成功、当场可读,冷启动 `Fw_ver = -1`)→ 按固件冷启动路径排查(看门狗/时钟/外设初始化);或用 `cold_boot_expect` 指定期望值、`cold_boot_check: false` 关闭 |
416
+ | `fw_serial_status` 的 `port_probe.openable = false` | 端口当前**无法独占打开**(硬结论)→ 看 `holder_candidates`(`source: cmdline` 比 `name` 准,候选≠持有者)确认占用方 |
412
417
 
413
418
  完整速查表与手工部署细节见 [`Fwloop_setup_pack/README.md`](Fwloop_setup_pack/README.md)。
414
419
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.7.2
3
+ Version: 0.7.3
4
4
  Summary: AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)
5
5
  Author-email: WeilinZhang <19523330249@163.com>
6
6
  License: MIT
@@ -18,7 +18,7 @@ Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
20
  Requires-Dist: pyvisa>=1.14.0
21
- Requires-Dist: pyvisa-py>=0.7.2
21
+ Requires-Dist: pyvisa-py>=0.7.3
22
22
  Requires-Dist: pyserial>=3.5
23
23
  Requires-Dist: pytest>=8.0.0
24
24
  Requires-Dist: pyocd>=0.36.0
@@ -383,7 +383,7 @@ claude mcp remove --scope user agentic-hil
383
383
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
384
384
  ├── demo-make/ 示例固件(Make;构建测试载体)
385
385
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
386
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.2)
386
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.3)
387
387
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
388
388
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
389
389
  ├── CLAUDE.md Claude Code CLI 指南
@@ -393,7 +393,7 @@ claude mcp remove --scope user agentic-hil
393
393
 
394
394
  ---
395
395
 
396
- ## 能力验证状态(v0.7.2
396
+ ## 能力验证状态(v0.7.3
397
397
 
398
398
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
399
399
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -416,7 +416,9 @@ claude mcp remove --scope user agentic-hil
416
416
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
417
417
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
418
418
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
419
- | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1 |
419
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
420
+ | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
421
+ | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
420
422
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
421
423
 
422
424
  ---
@@ -435,8 +437,11 @@ claude mcp remove --scope user agentic-hil
435
437
  | 串口 `SERIAL_PORT_ERROR: ... PermissionError(13)` | 端口被 GUI 上位机独占(NDT Debug Tool / STDataCollection.exe、串口助手)→ 关闭占用方后 `fw_serial_open`;残留会话用 `fw_serial_close`(已自动等待/强杀进程,端口释放可验证) |
436
438
  | `fw_serial_status` 的 `error_class` 为 `UART_BUSY` | 会话未打开或已被占用 → 先 `fw_serial_open`;残留会话 `fw_serial_close` 清理 |
437
439
  | `fw_serial_*` 报 `PermissionError(13)` / `rx_bytes: 0` | 串口被独占:**退出 NDT Debug Tool GUI(STDataCollection.exe)**——它打开时会同时占住 Box 串口与 USB-IIC 通道;失败信封/`fw_serial_status` 的 `suspected_holders` 会直接列出嫌疑进程(注意别杀 MCP server 的 python) |
438
- | 烧录"擦除生效(probe 从非 00 变 00)但校验读回全 FF"、`pages_written` 已发出 | v0.7.1 及更早的逐笔 serve 路径问题 → v0.7.2 起默认走 `flash_engine: bridge`(桥内单体,与 GUI 同进程模型);要复现/排查旧行为用 `flash_engine: "serve"` |
440
+ | 烧录"擦除生效(probe 从非 00 变 00)但校验读回全 FF"、`pages_written` 已发出 | v0.7.1 及更早的逐笔 serve 路径问题 → v0.7.3 起默认走 `flash_engine: bridge`(桥内单体,与 GUI 同进程模型);要复现/排查旧行为用 `flash_engine: "serve"` |
439
441
  | 长调用(capture/measure)客户端超时 | MCP stdio 循环是**单线程**的:长调用会让同会话其它调用排队(含 3s 的小调用)→ 缩短窗口,或用 `fw_ndtdbg_stream_start` job 模式 |
442
+ | 烧录报 `INCOMPLETE_FLASH` | 桥内 flash 连续 30s 无进度(疑似卡在写入中途)→ **芯片处于半擦写态,勿直接使用,必须重新完整烧录**;信封带 `chip_state: partially_erased` 与 `post_kill_power_cycle`(已尝试复位) |
443
+ | 烧录报 `COLD_BOOT_VERIFY_FAILED` | **Flash 字节已通过逐窗校验,但断电重上电后目标没有有效应答/与期望不符**(现场 `.18` 类问题:GUI 报成功、当场可读,冷启动 `Fw_ver = -1`)→ 按固件冷启动路径排查(看门狗/时钟/外设初始化);或用 `cold_boot_expect` 指定期望值、`cold_boot_check: false` 关闭 |
444
+ | `fw_serial_status` 的 `port_probe.openable = false` | 端口当前**无法独占打开**(硬结论)→ 看 `holder_candidates`(`source: cmdline` 比 `name` 准,候选≠持有者)确认占用方 |
440
445
 
441
446
  完整速查表与手工部署细节见 [`Fwloop_setup_pack/README.md`](Fwloop_setup_pack/README.md)。
442
447
 
@@ -1,5 +1,5 @@
1
1
  pyvisa>=1.14.0
2
- pyvisa-py>=0.7.2
2
+ pyvisa-py>=0.7.3
3
3
  pyserial>=3.5
4
4
  pytest>=8.0.0
5
5
  pyocd>=0.36.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "firmwareloop"
7
- version = "0.7.2"
7
+ version = "0.7.3"
8
8
  description = "AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -14,7 +14,7 @@ license = { text = "MIT" }
14
14
  requires-python = ">=3.10"
15
15
  dependencies = [
16
16
  "pyvisa>=1.14.0",
17
- "pyvisa-py>=0.7.2",
17
+ "pyvisa-py>=0.7.3",
18
18
  "pyserial>=3.5",
19
19
  "pytest>=8.0.0",
20
20
  "pyocd>=0.36.0",
@@ -2,4 +2,4 @@
2
2
  FirmwareLoop - AI Agent Firmware Development and Lab Automation Platform.
3
3
  """
4
4
 
5
- __version__ = "0.7.2"
5
+ __version__ = "0.7.3"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.7.2).
3
+ fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.7.3).
4
4
 
5
5
  Exposes high-level firmware engineering and hardware management tools to AI Coding
6
6
  Agents (Antigravity CLI, Claude Code CLI, Qoder IDE, CodeBuddy IDE, Cursor) via the Model Context
@@ -332,7 +332,7 @@ def handle_fw_ndtdbg_write_reg(args: Dict[str, Any]) -> Dict[str, Any]:
332
332
  def handle_fw_ndtdbg_flash(args: Dict[str, Any]) -> Dict[str, Any]:
333
333
  """Erase + flash a .bin/.nfw image onto the NdtBox target chip (ChipSea Fxx) over IIC.
334
334
 
335
- P1-3 (0.7.2): 去掉按签名重试 —— "TARGET_MISMATCH @0x0000" 正是"根本没进 boot"的
335
+ P1-3 (0.7.3): 去掉按签名重试 —— "TARGET_MISMATCH @0x0000" 正是"根本没进 boot"的
336
336
  特征签名 (旧 boot 判据假阳的同一条错误路径), 盲目重试只会重复同一失败路径。
337
337
  整片重写 (<= rewrite_times) 现由 flash_exec 内部完成, 结果带 rewrite_count;
338
338
  BOOT_NOT_ENTERED 直接返回指引 (需断电重启, 重试无意义)。
@@ -348,6 +348,13 @@ def handle_fw_ndtdbg_flash(args: Dict[str, Any]) -> Dict[str, Any]:
348
348
  cmd.extend(["--boot-judge", str(args["boot_judge"])])
349
349
  if args.get("flash_engine"):
350
350
  cmd.extend(["--flash-engine", str(args["flash_engine"])])
351
+ # 冷启动冒烟 (评审 §4): 默认开 -> CLI 默认即开, 仅显式 false 时加关闭旗标
352
+ if args.get("cold_boot_check") is False:
353
+ cmd.append("--no-cold-boot-check")
354
+ if args.get("cold_boot_reg") is not None:
355
+ cmd.extend(["--cold-boot-reg", str(int(args["cold_boot_reg"]))])
356
+ if args.get("cold_boot_expect"):
357
+ cmd.extend(["--cold-boot-expect", str(args["cold_boot_expect"])])
351
358
  if args.get("confirm"):
352
359
  cmd.append("--confirm")
353
360
  _ndtdbg_reg_args(args, cmd)
@@ -1815,6 +1822,20 @@ TOOLS_REGISTRY = {
1815
1822
  "enum": ["bridge", "serve"],
1816
1823
  "description": "Flash transport. 'bridge' (default): whole sequence inside one 32-bit process, frames passed once via stdin, both device handles held - real-hardware validated (6/6). 'serve': one raw I2C transaction per page driven from Python (kept for A/B; field evidence: same frames reported 'erase effective' yet wrote nothing). Use 'serve' only to reproduce/diagnose that case",
1817
1824
  },
1825
+ "cold_boot_check": {
1826
+ "type": "boolean",
1827
+ "description": "Post-flash cold-boot smoke test (default true, recommended): power-cycle the target, then read cold_boot_reg and decide whether the firmware actually runs. Byte-level read-back verification cannot catch 'image flashed OK but the device is dead after power-up' (field case: GUI said Success and read back fine, but Fw_ver = -1 after a real power cycle). Returns COLD_BOOT_VERIFY_FAILED with the raw bytes read vs expected - the flash itself is already verified, so decide whether the failure is expected (e.g. a bootloader-only image or a register that legitimately has no content). Set false to skip",
1828
+ "default": True,
1829
+ },
1830
+ "cold_boot_reg": {
1831
+ "type": "integer",
1832
+ "description": "Register read for the cold-boot check (default 0x10 = firmware version)",
1833
+ "default": 16,
1834
+ },
1835
+ "cold_boot_expect": {
1836
+ "type": "string",
1837
+ "description": "Optional expected hex for cold-boot verification (e.g. '51 17 64 11'); when omitted the check only requires a meaningful (non-all-00/FF) answer",
1838
+ },
1818
1839
  "dll_dir": {
1819
1840
  "type": "string",
1820
1841
  "description": "NdtDebugTool lib directory containing USBIOX.DLL and usb2io.dll; default from lab.yaml ndtdbg.dll_dir",
@@ -2314,7 +2335,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
2314
2335
  },
2315
2336
  "serverInfo": {
2316
2337
  "name": "firmwareloop",
2317
- "version": "0.7.2"
2338
+ "version": "0.7.3"
2318
2339
  }
2319
2340
  }
2320
2341
  }
@@ -2449,7 +2470,7 @@ def _settle_stashed_entrypoints(stashed) -> None:
2449
2470
  def handle_cli_update() -> int:
2450
2471
  """Handle `firmwareloop update` command."""
2451
2472
  print("============================================================")
2452
- print(" FirmwareLoop Auto-Updater (v0.7.2)")
2473
+ print(" FirmwareLoop Auto-Updater (v0.7.3)")
2453
2474
  print("============================================================")
2454
2475
 
2455
2476
  is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
@@ -2880,7 +2901,7 @@ def set_ndtdbg_dlldir(dll_path: str) -> str:
2880
2901
  def handle_cli_setup() -> int:
2881
2902
  """Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, and CodeBuddy IDE."""
2882
2903
  print("============================================================")
2883
- print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.7.2)")
2904
+ print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.7.3)")
2884
2905
  print("============================================================")
2885
2906
  print("")
2886
2907
 
@@ -3005,7 +3026,7 @@ def handle_cli_setup() -> int:
3005
3026
  def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
3006
3027
  """Handle `firmwareloop init [target_chip]` command."""
3007
3028
  print("============================================================")
3008
- print(" FirmwareLoop Multi-Agent Project Initializer (v0.7.2)")
3029
+ print(" FirmwareLoop Multi-Agent Project Initializer (v0.7.3)")
3009
3030
  print("============================================================")
3010
3031
  cwd = os.getcwd()
3011
3032
  print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
@@ -3038,7 +3059,7 @@ def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
3038
3059
 
3039
3060
 
3040
3061
  def print_cli_help() -> None:
3041
- print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.7.2)
3062
+ print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.7.3)
3042
3063
 
3043
3064
  Usage:
3044
3065
  fwloop [command] (or: firmwareloop [command])
@@ -3084,7 +3105,7 @@ def main() -> None:
3084
3105
  sys.exit(2)
3085
3106
  sys.exit(handle_cli_ndtdebugtool(args[1]))
3086
3107
  elif cmd in ["version", "-v", "--version"]:
3087
- print("FirmwareLoop v0.7.2")
3108
+ print("FirmwareLoop v0.7.3")
3088
3109
  sys.exit(0)
3089
3110
  elif cmd in ["help", "-h", "--help"]:
3090
3111
  print_cli_help()
@@ -678,6 +678,17 @@ class NdtBoxIo:
678
678
  return None
679
679
  raise
680
680
 
681
+ def _post_kill_recover(self) -> dict:
682
+ """kill 之后的兜底恢复 (best-effort): 断电复位一次, 让目标离开半擦写/写中途态。
683
+
684
+ 失败不上抛 (本身已在错误路径上), 结果并入错误信封的 post_kill_power_cycle 字段。
685
+ """
686
+ try:
687
+ self.vbrt_reset(DEFAULT_OFF_MS)
688
+ return {"post_kill_power_cycle": "ok"}
689
+ except Exception as exc: # noqa: BLE001
690
+ return {"post_kill_power_cycle": f"failed: {type(exc).__name__}: {exc}"}
691
+
681
692
  def _bridge_plan_json(self, plan: dict) -> str:
682
693
  """桥 flash action 的 stdin plan (只含桥认识的键)。"""
683
694
  return json.dumps({
@@ -718,6 +729,7 @@ class NdtBoxIo:
718
729
  stderr=subprocess.PIPE, text=True, encoding="utf-8",
719
730
  errors="replace", bufsize=1)
720
731
  lines: list = []
732
+ state = {"last_progress": time.monotonic()}
721
733
 
722
734
  def _reader() -> None:
723
735
  try:
@@ -726,6 +738,7 @@ class NdtBoxIo:
726
738
  if not line:
727
739
  continue
728
740
  if line.startswith("#progress"):
741
+ state["last_progress"] = time.monotonic() # 进度续期 (stall 守护)
729
742
  print(f"[flash] {line[len('#progress'):].strip()}",
730
743
  file=sys.stderr, flush=True)
731
744
  continue
@@ -743,18 +756,37 @@ class NdtBoxIo:
743
756
  except Exception as exc:
744
757
  proc.kill()
745
758
  raise NdtBoxError("UNKNOWN_ERROR", f"向桥 flash 写入计划失败: {exc}") from exc
746
- deadline = time.monotonic() + max(5.0, budget_ms / 1000.0)
747
- while time.monotonic() < deadline:
759
+ # 守护 = "无进度"判定, 而非固定总时长: 桥逐页/逐窗吐 #progress, 每次续期;
760
+ # 健康但慢的全片擦除 (长 erase_ms / 大镜像) 不会被误杀 (评审 3.2-①)。
761
+ # stall_s / hard_timeout_s 可按 plan 覆盖 (默认 30s 无进度判卡; 硬上限 3x 预算且 >=180s)
762
+ stall_s = max(1.0, float(plan.get("stall_s") or 30.0))
763
+ hard_s = float(plan.get("hard_timeout_s") or max(180.0, 3.0 * budget_ms / 1000.0))
764
+ hard_deadline = t0 + hard_s
765
+ while True:
748
766
  if proc.poll() is not None and not reader.is_alive():
749
767
  break
768
+ now = time.monotonic()
769
+ if now > hard_deadline:
770
+ proc.kill()
771
+ raise NdtBoxError(
772
+ "BUS_TIMEOUT",
773
+ f"桥内 flash 超过硬上限 (预算 {budget_ms}ms, {pages} 页/{windows} 窗): "
774
+ "已终止桥进程。检查目标是否卡在擦除/页写; 可先 fw_ndtdbg_power_cycle 复位",
775
+ None)
776
+ if now - state["last_progress"] > stall_s:
777
+ proc.kill()
778
+ recover = self._post_kill_recover()
779
+ raise NdtBoxError(
780
+ "INCOMPLETE_FLASH",
781
+ f"桥内 flash 连续 {int(stall_s)}s 无任何进度 ⇒ 目标疑似卡在写入中途。"
782
+ "已终止桥进程并尝试复位, 但**芯片可能处于半擦写状态, 请勿直接使用, "
783
+ "必须重新完整烧录**",
784
+ f"最后一条桥输出: {(lines[-1] if lines else '无')[:120]}",
785
+ {"engine": "bridge", "chip_state": "partially_erased",
786
+ "pages_written": pages, "read_windows": windows, **recover,
787
+ "hint": "重新调用 fw_ndtdbg_flash 完整烧录; 若反复卡住, 先 "
788
+ "fw_ndtdbg_power_cycle 复位再试"})
750
789
  time.sleep(0.05)
751
- else:
752
- proc.kill()
753
- raise NdtBoxError(
754
- "BUS_TIMEOUT",
755
- f"桥内 flash 超时 (预算 {budget_ms}ms, {pages} 页/{windows} 窗): 已终止桥进程。"
756
- "检查目标是否卡在擦除/页写 (可先 fw_ndtdbg_power_cycle 复位再试)",
757
- None);
758
790
  elapsed_ms = round((time.monotonic() - t0) * 1000)
759
791
  if not lines:
760
792
  err = ""
@@ -827,6 +859,7 @@ class NdtBoxIo:
827
859
  total_ms = round((time.monotonic() - t_all) * 1000)
828
860
  return {
829
861
  "verify": data.get("verify", "pass"),
862
+ "chip_state": "verified" if data.get("verify", "pass") == "pass" else "unknown",
830
863
  "pages_written": data.get("pages_written"),
831
864
  "read_windows": data.get("read_windows"),
832
865
  "rewrite_count": attempt,
@@ -1391,9 +1424,60 @@ class ChipSeaFxx:
1391
1424
 
1392
1425
  # -- 固件烧录 (对齐 ChipSea.cs UpdateFW: 仅芯海 Fxx, 整片擦写) --
1393
1426
 
1427
+ def cold_boot_check(self, off_ms: int = DEFAULT_OFF_MS, reg: int = 0x10,
1428
+ expect: str | None = None) -> dict:
1429
+ """烧后冷启动冒烟 (评审 §4): 断电重上电 -> 读已知寄存器 -> 判定固件是否真的跑起来。
1430
+
1431
+ 只校验 Flash 字节的流程抓不到"镜像烧对了但跑不起来"——现场 `.18` 正是: GUI 报
1432
+ `FW Upgrade Success` 且当场可读, 断电重上电后 `Fw_ver = -1`。这里 fail-closed:
1433
+ expect 给定时逐字节比对; 未给定时判"有无效应答"(读不到或全 00/FF 都算失败)。
1434
+ """
1435
+ import time
1436
+ self._io.vbrt_reset(off_ms) # 冷启动 (真 POR 上电)
1437
+ time.sleep(0.3) # 让固件跑到能响应从机的程度
1438
+ want = bytes.fromhex(str(expect).replace(" ", "")) if expect else None
1439
+ raw: bytes | None = None
1440
+ err: str | None = None
1441
+ for _ in range(3):
1442
+ try:
1443
+ raw = self._io.read_reg(reg, len(want) if want else 4)
1444
+ break
1445
+ except NdtBoxError as exc:
1446
+ err = f"{exc.error_class}: {exc.message}"
1447
+ time.sleep(0.2)
1448
+ out: dict = {"checked": True, "reg": f"0x{reg:02X}",
1449
+ "raw_hex": raw.hex().upper() if raw else None,
1450
+ "expect_hex": want.hex().upper() if want else None,
1451
+ "read_error": err}
1452
+ if raw is None:
1453
+ out["ok"] = False
1454
+ out["conclusion"] = ("冷启动后该寄存器无应答: 固件可能未起振/未使能 I2C 从机 "
1455
+ "—— Flash 字节已验证, 但目标跑不起来 (现场 .18 类问题的特征)")
1456
+ return out
1457
+ if want is not None:
1458
+ if raw[:len(want)] != want:
1459
+ out["ok"] = False
1460
+ out["conclusion"] = (f"冷启动后 {out['reg']} 读回 {out['raw_hex']} "
1461
+ f"!= 期望 {out['expect_hex']}")
1462
+ return out
1463
+ out["ok"] = True
1464
+ out["conclusion"] = f"冷启动读回与期望一致 ({out['raw_hex']})"
1465
+ return out
1466
+ if all(b in (0x00, 0xFF) for b in raw):
1467
+ out["ok"] = False
1468
+ out["conclusion"] = (f"冷启动后 {out['reg']} 读回全 00/FF ({out['raw_hex']}): 无有效应答 "
1469
+ "—— 疑似固件未起振; 若该寄存器本就不该有内容, 请用 cold_boot_expect "
1470
+ "指定期望值或关闭 cold_boot_check")
1471
+ return out
1472
+ out["ok"] = True
1473
+ out["conclusion"] = f"冷启动读回 {out['raw_hex']} (有有效应答)"
1474
+ return out
1475
+
1394
1476
  def flash_bin(self, file_path: str, erase_ms: int = 2000,
1395
1477
  off_ms: int | None = None, rewrite_times: int | None = None,
1396
1478
  boot_judge: str | None = None, engine: str | None = None,
1479
+ cold_boot_check: bool = True, cold_boot_reg: int = 0x10,
1480
+ cold_boot_expect: str | None = None,
1397
1481
  progress=None) -> dict:
1398
1482
  """经 NdtBox IIC 烧录 .bin/.nfw 固件到芯海 Fxx (UpdateFW 语义)。
1399
1483
 
@@ -1432,6 +1516,22 @@ class ChipSeaFxx:
1432
1516
  res = self._io.flash_exec(plan)
1433
1517
  if progress:
1434
1518
  progress("verify", len(windows), len(windows))
1519
+ # 冷启动冒烟 (评审 §4, 默认开): Flash 字节校验通过 ≠ 固件跑得起来
1520
+ cold: dict | None = None
1521
+ if cold_boot_check and res.get("verify", "pass") == "pass":
1522
+ cold = self.cold_boot_check(off_ms=plan["off_ms"], reg=int(cold_boot_reg),
1523
+ expect=cold_boot_expect)
1524
+ if not cold.get("ok"):
1525
+ raise NdtBoxError(
1526
+ "COLD_BOOT_VERIFY_FAILED",
1527
+ f"烧录逐窗校验通过, 但冷启动冒烟失败: {cold.get('conclusion')}",
1528
+ "Flash 字节已写入并通过回读校验; 失败发生在'断电重上电后能否工作'这一步",
1529
+ {"verify": "pass", "engine": res.get("engine", plan["engine"]),
1530
+ "pages_written": len(pages), "window_verified": True,
1531
+ "cold_boot": cold,
1532
+ "hint": "①确认目标固件在本寄存器上确有可读内容 (否则用 cold_boot_expect 指定)"
1533
+ "; ②按现场 .18 类问题排查冷启动路径 (看门狗/时钟/外设初始化); "
1534
+ "③如确不需要该校验, 用 cold_boot_check=false / --no-cold-boot-check 关闭"})
1435
1535
  return {
1436
1536
  "file": file_path,
1437
1537
  "file_type": fw_type,
@@ -1440,6 +1540,8 @@ class ChipSeaFxx:
1440
1540
  "read_windows": len(windows),
1441
1541
  "erase_ms": int(erase_ms),
1442
1542
  "off_ms": plan["off_ms"],
1543
+ "chip_state": res.get("chip_state"),
1544
+ "cold_boot": cold,
1443
1545
  "rewrite_times": plan["rewrite_times"],
1444
1546
  "engine": res.get("engine", plan["engine"]),
1445
1547
  "boot_judge": res.get("boot_judge", plan["boot_judge"]),
@@ -1037,6 +1037,79 @@ def suspected_port_holders(session_pid: int | None = None) -> list[dict]:
1037
1037
  return out
1038
1038
 
1039
1039
 
1040
+ def port_exclusive_probe(port: str | None, baud: int = 115200) -> dict:
1041
+ """直接试开端口 ⇒ 给出"当前能否独占打开"的**硬结论**(评审 3.2-②)。
1042
+
1043
+ `tasklist` 看不到 COM 端口持有者(进程加载了模块 ≠ 打开了端口),任何进程名单都只是
1044
+ 推断;试开才是判据。返回 {port, openable, error, conclusion}。
1045
+ """
1046
+ if not port:
1047
+ return {"port": None, "openable": None, "error": "未指定端口",
1048
+ "conclusion": "无端口可探测 (未配置 serial.port)"}
1049
+ try:
1050
+ import serial as pyserial
1051
+ except ImportError as exc: # noqa: BLE001
1052
+ return {"port": port, "openable": None, "error": f"pyserial 未安装: {exc}",
1053
+ "conclusion": "无法探测 (缺 pyserial)"}
1054
+ ser = None
1055
+ try:
1056
+ ser = pyserial.Serial(port=port, baudrate=int(baud), timeout=0.2, write_timeout=0.5)
1057
+ return {"port": port, "openable": True, "error": None,
1058
+ "conclusion": f"{port} 当前可被本进程独占打开 ⇒ 若会话仍读不到数据, "
1059
+ "原因不在端口占用 (检查波特率/接线/目标是否在输出)"}
1060
+ except Exception as exc: # noqa: BLE001
1061
+ return {"port": port, "openable": False,
1062
+ "error": f"{type(exc).__name__}: {exc}",
1063
+ "conclusion": f"{port} 当前无法独占打开 ⇒ 被其它程序占用或设备不在位"}
1064
+ finally:
1065
+ try:
1066
+ if ser is not None:
1067
+ ser.close()
1068
+ except Exception: # noqa: BLE001
1069
+ pass
1070
+
1071
+
1072
+ def port_holder_candidates(port: str | None, session_pid: int | None = None) -> list[dict]:
1073
+ """端口占用**候选名单**(需人工确认;候选 ≠ 持有者)。
1074
+
1075
+ 两个来源, 按置信度标注:
1076
+ - `source="cmdline"`(较准): Win32_Process 命令行含端口名 ⇒ 该进程可能在用这个口;
1077
+ - `source="name"`(较低): 进程名像 GUI/上位机/残留 python serve。
1078
+ **硬结论请看 port_exclusive_probe()。**
1079
+ """
1080
+ out: list[dict] = []
1081
+ if os.name != "nt":
1082
+ return out
1083
+ self_pid = os.getpid()
1084
+ if port:
1085
+ try:
1086
+ ps = ("Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "
1087
+ f"'*{port}*' }} | Select-Object -First 12 ProcessId,Name,CommandLine | "
1088
+ "ConvertTo-Json -Compress")
1089
+ res = subprocess.run(["powershell", "-NoProfile", "-NonInteractive", "-Command", ps],
1090
+ capture_output=True, text=True, timeout=30, errors="replace")
1091
+ raw = (res.stdout or "").strip()
1092
+ if raw:
1093
+ data = json.loads(raw)
1094
+ if isinstance(data, dict):
1095
+ data = [data]
1096
+ for item in data:
1097
+ pid = int(item.get("ProcessId") or 0)
1098
+ if pid in (self_pid, session_pid):
1099
+ continue
1100
+ out.append({"name": item.get("Name") or "?", "pid": pid,
1101
+ "why": f"命令行含 {port}", "source": "cmdline",
1102
+ "confidence": "medium", "cmdline": (item.get("CommandLine") or "")[:200]})
1103
+ except Exception: # noqa: BLE001
1104
+ pass
1105
+ known = {h["pid"] for h in out}
1106
+ for h in suspected_port_holders(session_pid):
1107
+ if h["pid"] in known or h["pid"] == self_pid:
1108
+ continue
1109
+ out.append({**h, "source": "name", "confidence": "low"})
1110
+ return out
1111
+
1112
+
1040
1113
  def pid_alive(pid: int | None) -> bool:
1041
1114
  """进程存活探测。
1042
1115
 
@@ -256,9 +256,10 @@ def cmd_status(io: NdtBoxIo) -> None:
256
256
  {"symptom": "只有写事务 NACK, 读事务正常",
257
257
  "likely": "不是从机死: 该寄存器不可写/写保护, 或目标固件未实现该写寄存器 "
258
258
  "(此时应回读邻近寄存器确认链路仍活)"},
259
- {"symptom": "原本可读, 新增静态寄存器地址后整机读不到",
260
- "likely": "新增地址破坏了从机寄存器表约定 (必须按 addr 升序且不与擦除魔数 0x0000 "
261
- "冲突) ⇒ 建议优先复用已有寄存器的空闲字, 避免新增地址"},
259
+ {"symptom": "目标固件刚被改动过",
260
+ "likely": "按序排除: ①从机是否真的使能 (初始化是否提前 return); ②冷启动 vs 热复位"
261
+ "行为差异 (热复位可读、断电重上电读不到属另一类问题); ③改动静态寄存器表/"
262
+ "RAM 布局后整套读取是否仍正常 (表必须按 addr 升序, 且不与 0x0000 冲突)"},
262
263
  {"symptom": "BUS_TIMEOUT", "likely": "时钟拉伸、关中断死循环 (fault handler 自旋饿死 I2C 中断)"},
263
264
  ]
264
265
  ok("status", data, log="Open + ReadFwVersion(0x10) + ReadModuleConnect(0x29)")
@@ -518,7 +519,9 @@ def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s:
518
519
 
519
520
  def cmd_flash_bin(io: NdtBoxIo, config: dict, file_path: str, erase_ms: int, confirm: bool,
520
521
  off_ms: int | None = None, rewrite_times: int | None = None,
521
- boot_judge: str | None = None, engine: str | None = None) -> None:
522
+ boot_judge: str | None = None, engine: str | None = None,
523
+ cold_boot_check: bool | None = None, cold_boot_reg: int | None = None,
524
+ cold_boot_expect: str | None = None) -> None:
522
525
  # 烧录 = 整片擦除 + 页写, 破坏性高于 write-reg, 同款双闸门
523
526
  ndt_cfg = config.get("ndtdbg") or {}
524
527
  allowed = ndt_cfg.get("write_allowed", False)
@@ -540,13 +543,23 @@ def cmd_flash_bin(io: NdtBoxIo, config: dict, file_path: str, erase_ms: int, con
540
543
  # 0.7.2: 烧录引擎 (bridge 默认 = 桥内单体 flash; serve = 逐笔 Python 驱动, 供 A/B 复现)
541
544
  if engine is None:
542
545
  engine = flash_cfg.get("engine")
546
+ # 冷启动冒烟 (评审 §4): 默认开; lab.yaml ndtdbg.flash.cold_boot_check / reg / expect 可覆盖
547
+ if cold_boot_check is None:
548
+ cold_boot_check = flash_cfg.get("cold_boot_check", True)
549
+ if cold_boot_reg is None:
550
+ cold_boot_reg = flash_cfg.get("cold_boot_reg", 0x10)
551
+ if cold_boot_expect is None:
552
+ cold_boot_expect = flash_cfg.get("cold_boot_expect")
543
553
 
544
554
  def progress(phase: str, done: int, total: int) -> None:
545
555
  print(f"[flash-bin] {phase} {done}/{total}", file=sys.stderr, flush=True)
546
556
 
547
557
  result = ChipSeaFxx(io).flash_bin(file_path, erase_ms=erase_ms, off_ms=off_ms,
548
558
  rewrite_times=rewrite_times, boot_judge=boot_judge,
549
- engine=engine, progress=progress)
559
+ engine=engine, cold_boot_check=bool(cold_boot_check),
560
+ cold_boot_reg=int(cold_boot_reg),
561
+ cold_boot_expect=cold_boot_expect,
562
+ progress=progress)
550
563
  result["authorization"] = {"write_allowed": True,
551
564
  "granted_by": write_allowed_source() or "(default false)",
552
565
  "chain": list(_CONFIG_FILES)}
@@ -611,6 +624,15 @@ def build_parser() -> argparse.ArgumentParser:
611
624
  f.add_argument("--flash-engine", choices=["bridge", "serve"], default=None,
612
625
  help="烧录引擎 (默认 bridge = 桥内单体 flash: 单进程、帧经 stdin 一次传入、"
613
626
  "现场 6/6 verify pass; serve = 逐笔 raw 事务的 Python 驱动, 供 A/B 复现)")
627
+ f.add_argument("--cold-boot-check", dest="cold_boot_check", action="store_true", default=None,
628
+ help="烧后冷启动冒烟 (默认开): 断电重上电 -> 读 cold-boot-reg -> 判定固件是否"
629
+ "真的跑起来; 失败报 COLD_BOOT_VERIFY_FAILED (Flash 字节已通过校验)")
630
+ f.add_argument("--no-cold-boot-check", dest="cold_boot_check", action="store_false",
631
+ help="关闭冷启动冒烟")
632
+ f.add_argument("--cold-boot-reg", type=lambda x: int(x, 0), default=None,
633
+ help="冷启动冒烟读的寄存器 (默认 0x10 版本寄存器)")
634
+ f.add_argument("--cold-boot-expect", default=None,
635
+ help="冷启动冒烟期望值 (hex, 如 '51 17 64 11'); 不传则只判'有无效应答'")
614
636
 
615
637
  st = sub.add_parser("stream", parents=[common],
616
638
  help="高速连续采集寄存器组到 CSV (单会话长驻, 只读)")
@@ -671,7 +693,10 @@ def main(argv=None) -> None:
671
693
  elif args.cmd == "flash-bin":
672
694
  cmd_flash_bin(io, config, args.file, args.erase_ms, args.confirm,
673
695
  off_ms=args.off_ms, rewrite_times=args.rewrite_times,
674
- boot_judge=args.boot_judge, engine=args.flash_engine)
696
+ boot_judge=args.boot_judge, engine=args.flash_engine,
697
+ cold_boot_check=args.cold_boot_check,
698
+ cold_boot_reg=args.cold_boot_reg,
699
+ cold_boot_expect=args.cold_boot_expect)
675
700
  elif args.cmd == "stream":
676
701
  specs = [parse_reg_spec(s) for s in args.regs]
677
702
  cmd_stream(io, config, specs, args.interval_ms, args.duration_s,
@@ -36,6 +36,8 @@ from lib.serial_assistant import ( # noqa: E402
36
36
  parse_hex,
37
37
  probe_session,
38
38
  to_hex,
39
+ port_exclusive_probe,
40
+ port_holder_candidates,
39
41
  suspected_port_holders,
40
42
  wait_pid_exit,
41
43
  )
@@ -123,8 +125,10 @@ def cmd_status(_args: argparse.Namespace) -> None:
123
125
  "started_at": stats.get("started_at")},
124
126
  "conclusion": f"会话存活 (port={sess.get('port') or stats.get('port')}, "
125
127
  f"opened={alive})" if alive else "serve 进程存活但串口未打开",
126
- # P1-4: 串口被谁占着 —— 直接给嫌疑名单 (GUI/上位机 + 残留 python serve)
127
- "suspected_holders": suspected_port_holders(sess.get("pid")),
128
+ # P1-4 / 评审 3.2-②: 先给硬结论 (端口能否独占打开), 再给需人工确认的候选名单
129
+ "port_probe": port_exclusive_probe(sess.get("port") or stats.get("port")),
130
+ "holder_candidates": port_holder_candidates(sess.get("port") or stats.get("port"),
131
+ sess.get("pid")),
128
132
  })
129
133
 
130
134
 
@@ -152,7 +156,8 @@ def cmd_open(args: argparse.Namespace) -> None:
152
156
  "holder_hint",
153
157
  "端口可能被 GUI 上位机占用: NDT Debug Tool (STDataCollection.exe) 打开时会同时占用 "
154
158
  "Box 串口与 USB-IIC 通道; 请关闭占用方后重试, 残留会话可用 fw_serial_close 清理")
155
- extra.setdefault("suspected_holders", suspected_port_holders())
159
+ extra.setdefault("port_probe", port_exclusive_probe(cfg.get("port")))
160
+ extra.setdefault("holder_candidates", port_holder_candidates(cfg.get("port")))
156
161
  fail(exc.error_class, exc.detail, extra=extra)
157
162
  data = {k: info.get(k) for k in
158
163
  ("port", "baud", "simulated", "log_path", "reused", "pid", "started_at")}
@@ -334,10 +339,17 @@ def cmd_parse(args: argparse.Namespace) -> None:
334
339
 
335
340
 
336
341
  def _port_error_extra(exc: SerialAssistantError) -> dict:
337
- """串口类错误的统一附加信息: 嫌疑占用进程名单 (P1-4)。"""
342
+ """串口类错误的统一附加信息 (P1-4 / 评审 3.2-②)。
343
+
344
+ 顺序说明设计: **硬结论在前** (port_probe = 直接试开端口能否独占), 候选名单在后
345
+ (cmdline 匹配较准 → 进程名匹配较低, 都只是"需人工确认"的候选)。
346
+ """
338
347
  extra = dict(exc.extra)
339
348
  if exc.error_class in ("SERIAL_PORT_ERROR", "UART_BUSY"):
340
- extra.setdefault("suspected_holders", suspected_port_holders())
349
+ port = extra.get("port") or (_load_session_file() or {}).get("port")
350
+ extra.setdefault("port", port)
351
+ extra.setdefault("port_probe", port_exclusive_probe(port))
352
+ extra.setdefault("holder_candidates", port_holder_candidates(port))
341
353
  return extra
342
354
 
343
355
 
@@ -1,6 +1,6 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Dual-Tier MCP setup and registration helper (v0.7.2).
3
+ Dual-Tier MCP setup and registration helper (v0.7.3).
4
4
  Inspects installed AI coding agents (Qoder, Claude Code, Antigravity, Cursor)
5
5
  and configures global MCP / skills or prints exact registration commands.
6
6
 
@@ -242,7 +242,7 @@ if ($Json) {
242
242
  Write-FwJson ([pscustomobject]$report)
243
243
  } else {
244
244
  Write-Host "============================================================" -ForegroundColor Cyan
245
- Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.7.2)" -ForegroundColor Green
245
+ Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.7.3)" -ForegroundColor Green
246
246
  Write-Host "============================================================" -ForegroundColor Cyan
247
247
  Write-Host ""
248
248
  Write-Host "[1] Google Antigravity / Gemini CLI:" -ForegroundColor Yellow
File without changes
File without changes
File without changes