firmwareloop 0.7.0__tar.gz → 0.7.1__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.0 → firmwareloop-0.7.1}/PKG-INFO +7 -7
  2. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/README.md +5 -5
  3. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/firmwareloop.egg-info/PKG-INFO +7 -7
  4. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/firmwareloop.egg-info/requires.txt +1 -1
  5. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/pyproject.toml +2 -2
  6. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/__init__.py +1 -1
  7. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/fw_mcp_server.py +17 -10
  8. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/lib/ndtdbg.py +115 -35
  9. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/ndtdbg_cli.py +15 -5
  10. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/setup-agent-mcp.ps1 +2 -2
  11. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/LICENSE +0 -0
  12. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/firmwareloop.egg-info/SOURCES.txt +0 -0
  13. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/firmwareloop.egg-info/dependency_links.txt +0 -0
  14. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/firmwareloop.egg-info/entry_points.txt +0 -0
  15. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/firmwareloop.egg-info/top_level.txt +0 -0
  16. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/setup.cfg +0 -0
  17. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/acceptance-scenario.ps1 +0 -0
  18. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/build.ps1 +0 -0
  19. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/can.ps1 +0 -0
  20. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/check-qoder-mcp.ps1 +0 -0
  21. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/common/build-backends.psm1 +0 -0
  22. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/common/fw.psm1 +0 -0
  23. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/common/uart_probe.py +0 -0
  24. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/doctor.ps1 +0 -0
  25. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/flash.ps1 +0 -0
  26. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/instrument_cli.py +0 -0
  27. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/lib/__init__.py +0 -0
  28. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/lib/instruments.py +0 -0
  29. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/lib/serial_assistant.py +0 -0
  30. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/logic_capture.ps1 +0 -0
  31. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/logic_decode.ps1 +0 -0
  32. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/ndtdbg_bridge.ps1 +0 -0
  33. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/reset.ps1 +0 -0
  34. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/serial_cli.py +0 -0
  35. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/sigrok_csv_to_fwloop.py +0 -0
  36. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/test-backends.ps1 +0 -0
  37. {firmwareloop-0.7.0 → firmwareloop-0.7.1}/tools/test.ps1 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.7.0
3
+ Version: 0.7.1
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.0
21
+ Requires-Dist: pyvisa-py>=0.7.1
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.0)
386
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.1)
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.0
396
+ ## 能力验证状态(v0.7.1
397
397
 
398
398
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
399
399
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -416,7 +416,7 @@ 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;双闸门 Fail-Closed)+ v0.7.0 boot 判据后移/整片重写(待真机复测首试成功率) |
419
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass;双闸门 Fail-Closed)+ v0.7.1 **差分 boot 判据**(真机实测: 擦除后读 0x0000 = 00 00 00 00, 旧"全 FF"判据在该 ROM 上永久误判 → 已改为"仅同一条非平凡应答才熔断", 并提供 `boot_judge: off` 关闭开关) |
420
420
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
421
421
 
422
422
  ---
@@ -430,8 +430,8 @@ claude mcp remove --scope user agentic-hil
430
430
  | 写操作 `PERMISSION_DENIED` | 全局 `lab.yaml` 的 `ndtdbg.write_allowed` 为 `false` → 临时改 `true` 并重启 Agent(测试完建议改回 `false`) |
431
431
  | sigrok 报 `Failed to receive reply to EP1 command 0x7d` | Saleae 驱动不是 libusb-win32 → 用 Zadig 重装;或设备被 Logic 软件占用(先退出) |
432
432
  | `fw_logic_decode` 空帧 | 抓取窗口内无总线事务 → 改用 `fw_ndtdbg_capture_seq`(抓取与寄存器序列同窗口进行),不要串行先抓后写 |
433
- | 烧录报 `BOOT_NOT_ENTERED`(擦除后 0x0000 仍读回数据) | 目标没进 boot ROM(应答的是运行态 App)→ 仅对目标板断电重启(勿动 BOX)后重试;反复出现时确认 `off_ms`(默认 500ms,独立看门狗芯片需真 POR)与 io10 供电复位接线 |
434
- | `fw_ndtdbg_flash` 报 `TARGET_MISMATCH: 烧录校验失败 @0x....` | 响应已带 `stage_timings`/`probe_hex`/`first_mismatch_*`:先看 `probe_hex`(非 FFFFFFFF = 根本没进 boot,属上一条),再看失配偏移是否恒定(恒定=页写未落地);整片重写次数由 `rewrite_times` 控制(默认 3) |
433
+ | 烧录报 `BOOT_NOT_ENTERED`(擦除魔数前后 0x0000 读回**完全相同**的非平凡数据) | 差分判据认定应答者仍是运行态 App(擦除被忽略)→ 仅对目标板断电重启(勿动 BOX)后重试;反复出现时确认 `off_ms`(默认 500ms,独立看门狗芯片需真 POR)与 io10 供电复位接线。若你确认目标 ROM 的 0x0000 语义特殊,用 `boot_judge: off`(lab.yaml/CLI `--boot-judge off`/MCP `boot_judge`)关闭判据,让逐窗校验做唯一判据 |
434
+ | `fw_ndtdbg_flash` 报 `TARGET_MISMATCH: 烧录校验失败 @0x....` | 校验是**权威判据**(不是探测启发式)。信封带 `stage_timings`/`probe_before_hex`/`probe_hex`/`boot_evidence`/`first_mismatch_*`:`boot_evidence=erase_effective` 说明擦除生效 → 看失配偏移是否恒定(恒定=页写未落地);`inconclusive` 说明探测无区分度(擦除后读回全 00/全 FF)→ 需人工确认是否真进了 boot;整片重写次数由 `rewrite_times` 控制(默认 3) |
435
435
  | 串口 `SERIAL_PORT_ERROR: ... PermissionError(13)` | 端口被 GUI 上位机独占(NDT Debug Tool / STDataCollection.exe、串口助手)→ 关闭占用方后 `fw_serial_open`;残留会话用 `fw_serial_close`(已自动等待/强杀进程,端口释放可验证) |
436
436
  | `fw_serial_status` 的 `error_class` 为 `UART_BUSY` | 会话未打开或已被占用 → 先 `fw_serial_open`;残留会话 `fw_serial_close` 清理 |
437
437
 
@@ -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.0)
358
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.1)
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.0
368
+ ## 能力验证状态(v0.7.1
369
369
 
370
370
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
371
371
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -388,7 +388,7 @@ 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;双闸门 Fail-Closed)+ v0.7.0 boot 判据后移/整片重写(待真机复测首试成功率) |
391
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass;双闸门 Fail-Closed)+ v0.7.1 **差分 boot 判据**(真机实测: 擦除后读 0x0000 = 00 00 00 00, 旧"全 FF"判据在该 ROM 上永久误判 → 已改为"仅同一条非平凡应答才熔断", 并提供 `boot_judge: off` 关闭开关) |
392
392
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
393
393
 
394
394
  ---
@@ -402,8 +402,8 @@ claude mcp remove --scope user agentic-hil
402
402
  | 写操作 `PERMISSION_DENIED` | 全局 `lab.yaml` 的 `ndtdbg.write_allowed` 为 `false` → 临时改 `true` 并重启 Agent(测试完建议改回 `false`) |
403
403
  | sigrok 报 `Failed to receive reply to EP1 command 0x7d` | Saleae 驱动不是 libusb-win32 → 用 Zadig 重装;或设备被 Logic 软件占用(先退出) |
404
404
  | `fw_logic_decode` 空帧 | 抓取窗口内无总线事务 → 改用 `fw_ndtdbg_capture_seq`(抓取与寄存器序列同窗口进行),不要串行先抓后写 |
405
- | 烧录报 `BOOT_NOT_ENTERED`(擦除后 0x0000 仍读回数据) | 目标没进 boot ROM(应答的是运行态 App)→ 仅对目标板断电重启(勿动 BOX)后重试;反复出现时确认 `off_ms`(默认 500ms,独立看门狗芯片需真 POR)与 io10 供电复位接线 |
406
- | `fw_ndtdbg_flash` 报 `TARGET_MISMATCH: 烧录校验失败 @0x....` | 响应已带 `stage_timings`/`probe_hex`/`first_mismatch_*`:先看 `probe_hex`(非 FFFFFFFF = 根本没进 boot,属上一条),再看失配偏移是否恒定(恒定=页写未落地);整片重写次数由 `rewrite_times` 控制(默认 3) |
405
+ | 烧录报 `BOOT_NOT_ENTERED`(擦除魔数前后 0x0000 读回**完全相同**的非平凡数据) | 差分判据认定应答者仍是运行态 App(擦除被忽略)→ 仅对目标板断电重启(勿动 BOX)后重试;反复出现时确认 `off_ms`(默认 500ms,独立看门狗芯片需真 POR)与 io10 供电复位接线。若你确认目标 ROM 的 0x0000 语义特殊,用 `boot_judge: off`(lab.yaml/CLI `--boot-judge off`/MCP `boot_judge`)关闭判据,让逐窗校验做唯一判据 |
406
+ | `fw_ndtdbg_flash` 报 `TARGET_MISMATCH: 烧录校验失败 @0x....` | 校验是**权威判据**(不是探测启发式)。信封带 `stage_timings`/`probe_before_hex`/`probe_hex`/`boot_evidence`/`first_mismatch_*`:`boot_evidence=erase_effective` 说明擦除生效 → 看失配偏移是否恒定(恒定=页写未落地);`inconclusive` 说明探测无区分度(擦除后读回全 00/全 FF)→ 需人工确认是否真进了 boot;整片重写次数由 `rewrite_times` 控制(默认 3) |
407
407
  | 串口 `SERIAL_PORT_ERROR: ... PermissionError(13)` | 端口被 GUI 上位机独占(NDT Debug Tool / STDataCollection.exe、串口助手)→ 关闭占用方后 `fw_serial_open`;残留会话用 `fw_serial_close`(已自动等待/强杀进程,端口释放可验证) |
408
408
  | `fw_serial_status` 的 `error_class` 为 `UART_BUSY` | 会话未打开或已被占用 → 先 `fw_serial_open`;残留会话 `fw_serial_close` 清理 |
409
409
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.7.0
3
+ Version: 0.7.1
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.0
21
+ Requires-Dist: pyvisa-py>=0.7.1
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.0)
386
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.1)
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.0
396
+ ## 能力验证状态(v0.7.1
397
397
 
398
398
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
399
399
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -416,7 +416,7 @@ 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;双闸门 Fail-Closed)+ v0.7.0 boot 判据后移/整片重写(待真机复测首试成功率) |
419
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass;双闸门 Fail-Closed)+ v0.7.1 **差分 boot 判据**(真机实测: 擦除后读 0x0000 = 00 00 00 00, 旧"全 FF"判据在该 ROM 上永久误判 → 已改为"仅同一条非平凡应答才熔断", 并提供 `boot_judge: off` 关闭开关) |
420
420
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
421
421
 
422
422
  ---
@@ -430,8 +430,8 @@ claude mcp remove --scope user agentic-hil
430
430
  | 写操作 `PERMISSION_DENIED` | 全局 `lab.yaml` 的 `ndtdbg.write_allowed` 为 `false` → 临时改 `true` 并重启 Agent(测试完建议改回 `false`) |
431
431
  | sigrok 报 `Failed to receive reply to EP1 command 0x7d` | Saleae 驱动不是 libusb-win32 → 用 Zadig 重装;或设备被 Logic 软件占用(先退出) |
432
432
  | `fw_logic_decode` 空帧 | 抓取窗口内无总线事务 → 改用 `fw_ndtdbg_capture_seq`(抓取与寄存器序列同窗口进行),不要串行先抓后写 |
433
- | 烧录报 `BOOT_NOT_ENTERED`(擦除后 0x0000 仍读回数据) | 目标没进 boot ROM(应答的是运行态 App)→ 仅对目标板断电重启(勿动 BOX)后重试;反复出现时确认 `off_ms`(默认 500ms,独立看门狗芯片需真 POR)与 io10 供电复位接线 |
434
- | `fw_ndtdbg_flash` 报 `TARGET_MISMATCH: 烧录校验失败 @0x....` | 响应已带 `stage_timings`/`probe_hex`/`first_mismatch_*`:先看 `probe_hex`(非 FFFFFFFF = 根本没进 boot,属上一条),再看失配偏移是否恒定(恒定=页写未落地);整片重写次数由 `rewrite_times` 控制(默认 3) |
433
+ | 烧录报 `BOOT_NOT_ENTERED`(擦除魔数前后 0x0000 读回**完全相同**的非平凡数据) | 差分判据认定应答者仍是运行态 App(擦除被忽略)→ 仅对目标板断电重启(勿动 BOX)后重试;反复出现时确认 `off_ms`(默认 500ms,独立看门狗芯片需真 POR)与 io10 供电复位接线。若你确认目标 ROM 的 0x0000 语义特殊,用 `boot_judge: off`(lab.yaml/CLI `--boot-judge off`/MCP `boot_judge`)关闭判据,让逐窗校验做唯一判据 |
434
+ | `fw_ndtdbg_flash` 报 `TARGET_MISMATCH: 烧录校验失败 @0x....` | 校验是**权威判据**(不是探测启发式)。信封带 `stage_timings`/`probe_before_hex`/`probe_hex`/`boot_evidence`/`first_mismatch_*`:`boot_evidence=erase_effective` 说明擦除生效 → 看失配偏移是否恒定(恒定=页写未落地);`inconclusive` 说明探测无区分度(擦除后读回全 00/全 FF)→ 需人工确认是否真进了 boot;整片重写次数由 `rewrite_times` 控制(默认 3) |
435
435
  | 串口 `SERIAL_PORT_ERROR: ... PermissionError(13)` | 端口被 GUI 上位机独占(NDT Debug Tool / STDataCollection.exe、串口助手)→ 关闭占用方后 `fw_serial_open`;残留会话用 `fw_serial_close`(已自动等待/强杀进程,端口释放可验证) |
436
436
  | `fw_serial_status` 的 `error_class` 为 `UART_BUSY` | 会话未打开或已被占用 → 先 `fw_serial_open`;残留会话 `fw_serial_close` 清理 |
437
437
 
@@ -1,5 +1,5 @@
1
1
  pyvisa>=1.14.0
2
- pyvisa-py>=0.7.0
2
+ pyvisa-py>=0.7.1
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.0"
7
+ version = "0.7.1"
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.0",
17
+ "pyvisa-py>=0.7.1",
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.0"
5
+ __version__ = "0.7.1"
@@ -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.0).
3
+ fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.7.1).
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.0): 去掉按签名重试 —— "TARGET_MISMATCH @0x0000" 正是"根本没进 boot"的
335
+ P1-3 (0.7.1): 去掉按签名重试 —— "TARGET_MISMATCH @0x0000" 正是"根本没进 boot"的
336
336
  特征签名 (旧 boot 判据假阳的同一条错误路径), 盲目重试只会重复同一失败路径。
337
337
  整片重写 (<= rewrite_times) 现由 flash_exec 内部完成, 结果带 rewrite_count;
338
338
  BOOT_NOT_ENTERED 直接返回指引 (需断电重启, 重试无意义)。
@@ -344,6 +344,8 @@ def handle_fw_ndtdbg_flash(args: Dict[str, Any]) -> Dict[str, Any]:
344
344
  cmd.extend(["--off-ms", str(int(args["off_ms"]))])
345
345
  if args.get("rewrite_times") is not None:
346
346
  cmd.extend(["--rewrite-times", str(max(0, int(args["rewrite_times"])))])
347
+ if args.get("boot_judge"):
348
+ cmd.extend(["--boot-judge", str(args["boot_judge"])])
347
349
  if args.get("confirm"):
348
350
  cmd.append("--confirm")
349
351
  _ndtdbg_reg_args(args, cmd)
@@ -1770,7 +1772,7 @@ TOOLS_REGISTRY = {
1770
1772
  "handler": handle_fw_ndtdbg_write_reg,
1771
1773
  },
1772
1774
  "fw_ndtdbg_flash": {
1773
- "description": "Erase and flash a .bin/.nfw firmware image onto the NdtBox target chip (ChipSea Fxx) over IIC via NdtDebugTool (power-cycle reset into boot -> erase magic AA 55 A5 5A -> BOOT JUDGEMENT: 0x0000 must read all-0xFF, a running App answers its own static registers and is rejected with BOOT_NOT_ENTERED -> 128B page writes -> 256B read-back verify, whole-chip rewrite on mismatch -> finish 'PASs'). DESTRUCTIVE full-chip erase: requires lab.yaml ndtdbg.write_allowed: true AND confirm: true, otherwise PERMISSION_DENIED.",
1775
+ "description": "Erase and flash a .bin/.nfw firmware image onto the NdtBox target chip (ChipSea Fxx) over IIC via NdtDebugTool (power-cycle reset into boot -> erase magic AA 55 A5 5A -> differential BOOT JUDGEMENT: 0x0000 is sampled once BEFORE the power cycle and once AFTER the erase; only an identical NON-TRIVIAL reading (the same App answering before and after a command only the boot ROM executes) raises BOOT_NOT_ENTERED - all-00/all-FF/no-ACK are inconclusive and proceed, because a real ROM may answer 0x0000 with 00 00 00 00 after erase -> 128B page writes -> 256B read-back verify = the authoritative criterion, whole-chip rewrite on mismatch -> finish 'PASs'). DESTRUCTIVE full-chip erase: requires lab.yaml ndtdbg.write_allowed: true AND confirm: true, otherwise PERMISSION_DENIED.",
1774
1776
  "inputSchema": {
1775
1777
  "type": "object",
1776
1778
  "properties": {
@@ -1799,7 +1801,12 @@ TOOLS_REGISTRY = {
1799
1801
  },
1800
1802
  "rewrite_times": {
1801
1803
  "type": "integer",
1802
- "description": "Whole-chip rewrite budget when read-back verify mismatches (aligned with ChipSea.cs REWRITE_TIMES). Default 3; 0 disables rewriting. The result carries rewrite_count/attempts, and a failed flash reports stage_timings/probe_hex/first_mismatch_* for post-mortem",
1804
+ "description": "Whole-chip rewrite budget when read-back verify mismatches (aligned with ChipSea.cs REWRITE_TIMES). Default 3; 0 disables rewriting. The result carries rewrite_count/attempts, and a failed flash reports stage_timings/probe_before_hex/probe_hex/boot_evidence/first_mismatch_* for post-mortem",
1805
+ },
1806
+ "boot_judge": {
1807
+ "type": "string",
1808
+ "enum": ["diff", "off"],
1809
+ "description": "Boot-probe criterion. 'diff' (default): compare 0x0000 before the power cycle vs after the erase, and raise BOOT_NOT_ENTERED only when both reads are identical NON-TRIVIAL data (proof the same running App answered the erase magic). 'off': only record probe evidence (probe_before_hex/probe_hex/boot_evidence) and never block - the 256B read-back verify stays the sole criterion, i.e. exactly the bridge/GUI behaviour. Use 'off' if a target ROM reports an unexpected 0x0000 signature",
1803
1810
  },
1804
1811
  "dll_dir": {
1805
1812
  "type": "string",
@@ -2300,7 +2307,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
2300
2307
  },
2301
2308
  "serverInfo": {
2302
2309
  "name": "firmwareloop",
2303
- "version": "0.7.0"
2310
+ "version": "0.7.1"
2304
2311
  }
2305
2312
  }
2306
2313
  }
@@ -2435,7 +2442,7 @@ def _settle_stashed_entrypoints(stashed) -> None:
2435
2442
  def handle_cli_update() -> int:
2436
2443
  """Handle `firmwareloop update` command."""
2437
2444
  print("============================================================")
2438
- print(" FirmwareLoop Auto-Updater (v0.7.0)")
2445
+ print(" FirmwareLoop Auto-Updater (v0.7.1)")
2439
2446
  print("============================================================")
2440
2447
 
2441
2448
  is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
@@ -2866,7 +2873,7 @@ def set_ndtdbg_dlldir(dll_path: str) -> str:
2866
2873
  def handle_cli_setup() -> int:
2867
2874
  """Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, and CodeBuddy IDE."""
2868
2875
  print("============================================================")
2869
- print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.7.0)")
2876
+ print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.7.1)")
2870
2877
  print("============================================================")
2871
2878
  print("")
2872
2879
 
@@ -2991,7 +2998,7 @@ def handle_cli_setup() -> int:
2991
2998
  def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
2992
2999
  """Handle `firmwareloop init [target_chip]` command."""
2993
3000
  print("============================================================")
2994
- print(" FirmwareLoop Multi-Agent Project Initializer (v0.7.0)")
3001
+ print(" FirmwareLoop Multi-Agent Project Initializer (v0.7.1)")
2995
3002
  print("============================================================")
2996
3003
  cwd = os.getcwd()
2997
3004
  print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
@@ -3024,7 +3031,7 @@ def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
3024
3031
 
3025
3032
 
3026
3033
  def print_cli_help() -> None:
3027
- print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.7.0)
3034
+ print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.7.1)
3028
3035
 
3029
3036
  Usage:
3030
3037
  fwloop [command] (or: firmwareloop [command])
@@ -3070,7 +3077,7 @@ def main() -> None:
3070
3077
  sys.exit(2)
3071
3078
  sys.exit(handle_cli_ndtdebugtool(args[1]))
3072
3079
  elif cmd in ["version", "-v", "--version"]:
3073
- print("FirmwareLoop v0.7.0")
3080
+ print("FirmwareLoop v0.7.1")
3074
3081
  sys.exit(0)
3075
3082
  elif cmd in ["help", "-h", "--help"]:
3076
3083
  print_cli_help()
@@ -262,9 +262,9 @@ PAGE_READ_GAP_MS = 10 # 回读窗口间隔 (ReadFwAndCheck Thread.Sleep(10)
262
262
  def _flash_envelope(timings: dict | None = None, **extra) -> dict:
263
263
  """P1-2: 烧录成功/失败共用同一组诊断字段。
264
264
 
265
- 分阶段耗时 (stage_timings) + boot 探测原始数据 (probe_hex) + 首个失配点
266
- (first_mismatch_offset/expect/got) + 页数与重写次数——失败不再是"只有一句话",
267
- 能当场区分 "探测假阳 (根本没进 boot)" "进了 boot 但页写没落地"。
265
+ 分阶段耗时 (stage_timings) + boot 探测原始数据 (probe_before_hex/probe_hex) +
266
+ 判据结论 (boot_evidence) + 首个失配点 (first_mismatch_offset/expect/got) +
267
+ 页数与重写次数——失败不再是"只有一句话", 能当场区分"根本没进 boot"与"页写没落地"。
268
268
  """
269
269
  out: dict = {"stage_timings": dict(timings or {})}
270
270
  for key, val in extra.items():
@@ -273,6 +273,38 @@ def _flash_envelope(timings: dict | None = None, **extra) -> dict:
273
273
  return out
274
274
 
275
275
 
276
+ BOOT_JUDGE_MODES = ("diff", "off")
277
+
278
+
279
+ def classify_boot_evidence(before: bytes | None, after: bytes | None) -> str:
280
+ """烧录 boot 判据 (0.7.1 差分判据, 取代被实战证伪的"擦除后必须全 FF")。
281
+
282
+ 返回: 'app_still_answering' (唯一熔断情形) | 'erase_effective' | 'inconclusive' | 'not_evaluated'
283
+
284
+ 实战铁证 (2026-09-11, CSA37F72): 擦除后读 0x0000 = `00 00 00 00` 而非 FF ——
285
+ 桥内单体 flash (无判据、时序同 GUI) 对同一批文件全部 verify pass ⇒ "全 FF" 只是
286
+ 该 ROM 的**未验证假设**, 一旦当硬判据就会永久误判 BOOT_NOT_ENTERED。
287
+
288
+ 现判据只在"有正面证据表明应答者仍是运行态固件"时熔断:
289
+ * 两次读数不同 (含"有应答 -> 无应答") ⇒ 擦除生效 ⇒ 继续;
290
+ * 任一侧无应答 (None) ⇒ 无法比对 ⇒ 不确定, 继续;
291
+ * 两次相同但内容是全 00/全 FF ⇒ 无法区分 (本 ROM 擦除后正是全 00, 而某些固件
292
+ 的静态寄存器也可能全 0) ⇒ 不确定, 继续;
293
+ * 两次相同且是非平凡数据 (如 `25 01 18 01`) ⇒ 擦除魔数 (boot ROM 专属命令)
294
+ 前后是同一条应答 ⇒ 魔数被忽略, 应答者仍是 App ⇒ 熔断。
295
+ 一切不确定情形都放行, 由页写后的逐窗回读校验做权威兜底 (fail-closed)。
296
+ """
297
+ if before is None and after is None:
298
+ return "inconclusive"
299
+ if before is None or after is None:
300
+ return "erase_effective" # 一侧有内容: 相较之下状态变了
301
+ if before != after:
302
+ return "erase_effective"
303
+ if all(b in (0x00, 0xFF) for b in after):
304
+ return "inconclusive" # 全 00/全 FF 无区分度
305
+ return "app_still_answering"
306
+
307
+
276
308
  def load_firmware(file_path: str) -> tuple[str, bytes]:
277
309
  """读取固件: .nfw 去 256 头 / .bin 128 对齐 0xFF 补齐 (ChipSea.cs LoadFile)。返回 (类型, 载荷)。"""
278
310
  if not os.path.isfile(file_path):
@@ -615,25 +647,38 @@ class NdtBoxIo:
615
647
  "duration_actual_ms": int((time.monotonic() - started) * 1000),
616
648
  }
617
649
 
650
+ def _probe_0000(self) -> bytes | None:
651
+ """读 0x0000 4 字节 (boot 判据探测): 有应答返回字节, 无应答 (TARGET_MISMATCH) 返回 None。
652
+
653
+ BUS_TIMEOUT (clock-stretch 挂死) / PROTOCOL_ERROR (会话错位) 上抛——这两种状态下
654
+ 任何继续操作都不可信 (fail-closed)。
655
+ """
656
+ try:
657
+ return self.raw_write_read(b"\x00\x00", 4)
658
+ except NdtBoxError as exc:
659
+ if exc.error_class == "TARGET_MISMATCH":
660
+ return None
661
+ raise
662
+
618
663
  def flash_exec(self, plan: dict, timeout_ms: int | None = None) -> dict:
619
664
  """Python 驱动整条 UpdateFW 序列 (0.4.0 serve 化): usb2io 断电复位进 boot +
620
665
  擦除 -> 页写 -> 回读校验 -> 完成魔数, 全部 I2C 事务经 serve 会话 (同进程同
621
666
  会话连续, 复刻原厂 GUI 进程模型)。校验 (逐窗 vs expect_hex) 在本模块完成,
622
- 失败 Fail-Closed 抛 TARGET_MISMATCH (信封带 stage_timings/probe_hex/first_mismatch_*)。
667
+ 并是 burn 成功与否的**权威判据**; 失败 Fail-Closed 抛 TARGET_MISMATCH
668
+ (信封带 stage_timings/probe_*/boot_evidence/first_mismatch_*)。
623
669
  校验不一致时按 rewrite_times 整片重写 (对齐 ChipSea.cs REWRITE_TIMES)。
624
670
 
625
671
  plan: {"off_ms":500,"erase_ms":2000,"page_gap_ms":30,"read_gap_ms":10,
626
- "rewrite_times":3,
672
+ "rewrite_times":3,"boot_judge":"diff",
627
673
  "writes":["<2B 地址+128B 数据帧 hex>",...],
628
674
  "windows":[{"off":0,"expect_hex":"<256B hex>"},...]}
629
675
 
630
- boot-in 判据 (F1, 2026-09-11 F72 实战修正): 擦除魔数 AA55A55A 是 boot ROM 专属
631
- 命令, App 不实现就只会忽略——而"运行态固件不应答 0x0000"的旧假设不成立
632
- (App 注册的静态寄存器 iic_test @0x00 会正常应答 25 01 18 01 02 05 00 01),
633
- 旧判据因此假阳, 擦除/378 页写全发给运行中的 App, 最后报误导性的 "校验失败
634
- @0x0000"。故判据后移到擦除 + erase_ms 之后读 0x0000: 全 FF (或读无应答) =
635
- ROM 已擦除; 读到任何非 FF 数据 = 仍在跟 App 说话 -> BOOT_NOT_ENTERED
636
- 时序随之与 GUI/桥内 flash 等价: vbrt -> 100ms(boot ready) -> 擦除魔数。
676
+ boot 判据 (0.7.1 差分判据; 详见 classify_boot_evidence):
677
+ 断电复位**前**读一次 0x0000 作为基线 (在关键窗口之外, 不影响"断电->擦除"间隔,
678
+ 时序仍与 GUI/桥内 flash 等价: vbrt -> 100ms(boot ready) -> 擦除魔数), 擦除
679
+ + erase_ms 后再读一次, 只在"两次是同一条非平凡应答"时判 BOOT_NOT_ENTERED;
680
+ 00/全 FF/任一侧无应答一律放行, 由页写后的逐窗校验兜底。boot_judge="off"
681
+ 完全关闭该判据 (仅记录证据, 不熔断)
637
682
  """
638
683
  import time
639
684
  off_ms = int(plan.get("off_ms", DEFAULT_OFF_MS))
@@ -641,6 +686,10 @@ class NdtBoxIo:
641
686
  page_gap = int(plan.get("page_gap_ms", PAGE_WRITE_GAP_MS))
642
687
  read_gap = int(plan.get("read_gap_ms", PAGE_READ_GAP_MS))
643
688
  rewrite_times = max(0, int(plan.get("rewrite_times", _REWRITE_TIMES)))
689
+ boot_judge = str(plan.get("boot_judge", "diff") or "diff").lower()
690
+ if boot_judge not in BOOT_JUDGE_MODES:
691
+ raise NdtBoxError("CONFIG_ERROR",
692
+ f"boot_judge 非法 (可选 {'/'.join(BOOT_JUDGE_MODES)})", boot_judge)
644
693
  pages = plan.get("writes", [])
645
694
  windows = plan.get("windows", [])
646
695
  t_all = time.monotonic()
@@ -648,9 +697,20 @@ class NdtBoxIo:
648
697
  timings = {"power_cycle_ms": 0, "boot_probe_ms": 0, "erase_ms": 0,
649
698
  "write_ms": 0, "verify_ms": 0}
650
699
  probe_hex: str | None = None
700
+ probe_before_hex: str | None = None
701
+ boot_evidence = "not_evaluated" if boot_judge == "off" else "inconclusive"
651
702
  rewrite_count = 0
652
703
  with self.session():
653
704
  for attempt in range(rewrite_times + 1):
705
+ # --- 0) 基線探测 (仅在首试判据开启时): 断电复位前采样 0x0000 ---
706
+ # 放在 vbrt 之前 => "断电 -> 擦除魔数" 窗口内不多任何事务 (与桥内
707
+ # 单体 flash/GUI 时序一致), 同时拿到"运行态固件应答长什么样"的基线。
708
+ if boot_judge == "diff" and attempt == 0:
709
+ t_stage = time.monotonic()
710
+ probe_before = self._probe_0000()
711
+ if probe_before:
712
+ probe_before_hex = probe_before.hex().upper()
713
+ timings["boot_probe_ms"] += round((time.monotonic() - t_stage) * 1000)
654
714
  # --- 1) 断电复位进 boot (IOoperation.VBRTOFF) ---
655
715
  t_stage = time.monotonic()
656
716
  self.vbrt_reset(off_ms)
@@ -668,6 +728,8 @@ class NdtBoxIo:
668
728
  "擦除命令超时无应答 (目标未进 boot 或总线挂死)。请仅对目标板断电重启 "
669
729
  "(勿动 BOX), 然后重新调用 fw_ndtdbg_flash", exc.detail,
670
730
  _flash_envelope(timings, probe_hex=probe_hex,
731
+ probe_before_hex=probe_before_hex,
732
+ boot_evidence=boot_evidence,
671
733
  rewrite_count=rewrite_count)) from exc
672
734
  raise NdtBoxError(
673
735
  "BOOT_NOT_ENTERED",
@@ -677,34 +739,38 @@ class NdtBoxIo:
677
739
  "独立看门狗芯片需要真断电)",
678
740
  exc.detail,
679
741
  _flash_envelope(timings, probe_hex=probe_hex,
742
+ probe_before_hex=probe_before_hex,
743
+ boot_evidence=boot_evidence,
680
744
  rewrite_count=rewrite_count)) from exc
681
745
  time.sleep(erase_ms / 1000.0)
682
746
  timings["erase_ms"] += round((time.monotonic() - t_stage) * 1000)
683
- # --- 3) boot 判据 (擦除后 0x0000 全 FF = ROM 已擦除; 非 FF = 还在跟 App 说话) ---
747
+ # --- 3) boot 判据 (差分: 只有"魔数前后同一条非平凡应答"才熔断) ---
684
748
  t_stage = time.monotonic()
685
- probe: bytes | None = None
686
- try:
687
- probe = self.raw_write_read(b"\x00\x00", 4)
688
- except NdtBoxError as exc:
689
- if exc.error_class == "BUS_TIMEOUT":
690
- raise # clock-stretch 挂死: 上抛 (信封已带断电指引)
691
- probe = None # 无应答: ROM 在 boot 窗口内的读协议不确定, 交给后续校验兜底
749
+ probe = self._probe_0000()
692
750
  timings["boot_probe_ms"] += round((time.monotonic() - t_stage) * 1000)
693
751
  if probe:
694
752
  probe_hex = probe.hex().upper()
695
- if probe and any(b != 0xFF for b in probe):
753
+ if boot_judge == "diff" and attempt == 0:
754
+ boot_evidence = classify_boot_evidence(
755
+ bytes.fromhex(probe_before_hex) if probe_before_hex else None, probe)
756
+ elif boot_judge == "off":
757
+ boot_evidence = "not_evaluated"
758
+ # 重写轮次不再判据 (基线已过期), 证据沿用首试结论
759
+ if boot_evidence == "app_still_answering":
696
760
  raise NdtBoxError(
697
761
  "BOOT_NOT_ENTERED",
698
- f"擦除后 0x0000 仍读回数据 ({probe_hex}): 目标未进入 boot ROM —— "
699
- "擦除魔数被忽略, 说明当前应答的是目标固件本身 (它在该地址注册了会应答本"
700
- "工具读协议的寄存器, 实测 iic_test[8] @0x00 = 25 01 18 01 02 05 00 01)。"
762
+ f"擦除魔数前后 0x0000 读回完全相同 ({probe_hex}): 擦除被忽略 目标未进入 "
763
+ "boot ROM (该命令只有 boot ROM 执行, 同一条应答说明应答者仍是运行态固件)。"
701
764
  "请仅对目标板断电重启 (勿动 BOX) 后重试; 若反复出现: ①确认 off_ms 足以构成"
702
765
  "真 POR (默认 500ms, 独立看门狗芯片只有上电/掉电/复位脚能清零); ②检查 "
703
766
  "usb2io IO10 供电复位接线; ③确认烧录的是本芯片 boot ROM 支持的协议; "
704
767
  "④若目标固件由你 (agent) 改动过, 先确认它确实使能了 I2C 从机 (从机未 init "
705
- "或提前 return 时该地址不应答, 现象会变成读回全 FF, 与掉电不可区分)",
768
+ "或提前 return 时该地址不应答, 现象会变成读回全 FF, 与掉电不可区分)"
769
+ "如仍要强制烧录: lab.yaml ndtdbg.flash.boot_judge: off / CLI --boot-judge off",
706
770
  None,
707
771
  _flash_envelope(timings, probe_hex=probe_hex,
772
+ probe_before_hex=probe_before_hex,
773
+ boot_evidence=boot_evidence,
708
774
  pages_written=0, rewrite_count=rewrite_count))
709
775
  # --- 4) 逐页写 (WriteFw) ---
710
776
  t_stage = time.monotonic()
@@ -739,11 +805,17 @@ class NdtBoxIo:
739
805
  if attempt < rewrite_times:
740
806
  rewrite_count += 1 # 整片重写 (ChipSea.cs REWRITE_TIMES 语义)
741
807
  continue
808
+ hint = ("校验是烧录成功与否的权威判据 (非探测启发式): 若 probe 前后一致且为"
809
+ "全 00/全 FF, 说明本次未进 boot 与'擦除生效'无法区分——请检查目标是否"
810
+ "真的断电复位 (off_ms/IO10 接线)、以及目标固件是否使能了 I2C 从机; "
811
+ "必要时可 --boot-judge off 关闭探测判据 (不改变校验)")
742
812
  raise NdtBoxError(
743
813
  "TARGET_MISMATCH",
744
814
  f"烧录校验失败 @0x{mismatch['offset']:04X} (整片重写 {rewrite_count} 次后仍不一致)",
745
- f"expect={mismatch['expect']} got={mismatch['got']}",
815
+ f"expect={mismatch['expect']} got={mismatch['got']}; {hint}",
746
816
  _flash_envelope(timings, probe_hex=probe_hex,
817
+ probe_before_hex=probe_before_hex,
818
+ boot_evidence=boot_evidence,
747
819
  pages_written=len(pages),
748
820
  pages_written_total=len(pages) * (rewrite_count + 1),
749
821
  rewrite_count=rewrite_count,
@@ -759,7 +831,9 @@ class NdtBoxIo:
759
831
  timings["total_ms"] = round((time.monotonic() - t_all) * 1000)
760
832
  out = {"verify": "pass", "pages_written": len(pages),
761
833
  "read_windows": len(windows), "rewrite_count": rewrite_count,
762
- "attempts": rewrite_count + 1, "probe_hex": probe_hex,
834
+ "attempts": rewrite_count + 1,
835
+ "probe_hex": probe_hex, "probe_before_hex": probe_before_hex,
836
+ "boot_evidence": boot_evidence, "boot_judge": boot_judge,
763
837
  "off_ms": off_ms, "stage_timings": timings}
764
838
  # 兼容 0.6.x 的扁平键 (P3 分阶段耗时): 由 stage_timings 派生, 单一来源
765
839
  out.update({"reset_ms": timings["power_cycle_ms"],
@@ -1118,21 +1192,22 @@ class ChipSeaFxx:
1118
1192
 
1119
1193
  def flash_bin(self, file_path: str, erase_ms: int = 2000,
1120
1194
  off_ms: int | None = None, rewrite_times: int | None = None,
1121
- progress=None) -> dict:
1195
+ boot_judge: str | None = None, progress=None) -> dict:
1122
1196
  """经 NdtBox IIC 烧录 .bin/.nfw 固件到芯海 Fxx (UpdateFW 语义)。
1123
1197
 
1124
1198
  时序逐行对齐 ChipSea.cs UpdateFW, 进程模型对齐原厂 STDataCollection GUI:
1125
- VBRTOFF(off_ms) 断电复位进 boot -> 100ms -> 擦除魔数 -> erase_ms ->
1126
- 擦除后 0x0000 判据 (F1) -> 128B 页写(30ms) -> 200ms ->
1127
- 256B 窗回读校验(10ms) -> 不一致整片重写 (<= rewrite_times) ->
1128
- 完成魔数 "PASs" -> VBRTOFF(off_ms) 复位加载。
1199
+ [判据基线探测 0x0000 (在窗口之外)] -> VBRTOFF(off_ms) 断电复位进 boot
1200
+ -> 100ms -> 擦除魔数 -> erase_ms -> 差分 boot 判据 ->
1201
+ 128B 页写(30ms) -> 200ms -> 逐 256B 窗回读校验(10ms, 权威判据)
1202
+ -> 不一致整片重写 (<= rewrite_times) -> 完成魔数 "PASs" -> VBRTOFF 复位加载。
1129
1203
  全程经同一 serve 会话连续执行 (同进程同 USB 会话), ~15s 量级。
1130
1204
  帧组装在本层 (可单测); 校验失败 Fail-Closed 抛 NdtBoxError, 信封带
1131
- stage_timings/probe_hex/first_mismatch_* (P1-2)。
1205
+ stage_timings/probe_before_hex/probe_hex/boot_evidence/first_mismatch_* (P1-2)。
1132
1206
 
1133
1207
  off_ms 默认 DEFAULT_OFF_MS=500 (独立看门狗芯片需真 POR); rewrite_times
1134
- 默认 _REWRITE_TIMES=3 (对齐 ChipSea.cs)。progress(phase, done, total)
1135
- verify 阶段末回调。
1208
+ 默认 _REWRITE_TIMES=3 (对齐 ChipSea.cs); boot_judge 默认 "diff" (差分判据,
1209
+ 只在有正面证据表明应答者仍是运行态固件时熔断; "off" = 完全关闭, 仅记录证据)。
1210
+ progress(phase, done, total) 在 verify 阶段末回调。
1136
1211
  """
1137
1212
  import time
1138
1213
  fw_type, fw = load_firmware(file_path)
@@ -1145,6 +1220,7 @@ class ChipSeaFxx:
1145
1220
  "page_gap_ms": PAGE_WRITE_GAP_MS,
1146
1221
  "read_gap_ms": PAGE_READ_GAP_MS,
1147
1222
  "rewrite_times": _REWRITE_TIMES if rewrite_times is None else max(0, int(rewrite_times)),
1223
+ "boot_judge": str(boot_judge or "diff").lower(),
1148
1224
  "writes": [build_page_write_frame(off, fw[off:off + FLASH_WRITE_PAGE]).hex()
1149
1225
  for off in pages],
1150
1226
  "windows": [{"off": off, "expect_hex": expected_read_window(fw, off).hex()}
@@ -1162,6 +1238,10 @@ class ChipSeaFxx:
1162
1238
  "erase_ms": int(erase_ms),
1163
1239
  "off_ms": plan["off_ms"],
1164
1240
  "rewrite_times": plan["rewrite_times"],
1241
+ "boot_judge": plan["boot_judge"],
1242
+ "boot_evidence": res.get("boot_evidence"),
1243
+ "probe_hex": res.get("probe_hex"),
1244
+ "probe_before_hex": res.get("probe_before_hex"),
1165
1245
  "rewrite_count": res.get("rewrite_count", 0),
1166
1246
  "attempts": res.get("attempts", 1),
1167
1247
  "stage_timings": res.get("stage_timings"),
@@ -504,7 +504,8 @@ def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s:
504
504
 
505
505
 
506
506
  def cmd_flash_bin(io: NdtBoxIo, config: dict, file_path: str, erase_ms: int, confirm: bool,
507
- off_ms: int | None = None, rewrite_times: int | None = None) -> None:
507
+ off_ms: int | None = None, rewrite_times: int | None = None,
508
+ boot_judge: str | None = None) -> None:
508
509
  # 烧录 = 整片擦除 + 页写, 破坏性高于 write-reg, 同款双闸门
509
510
  ndt_cfg = config.get("ndtdbg") or {}
510
511
  allowed = ndt_cfg.get("write_allowed", False)
@@ -516,22 +517,27 @@ def cmd_flash_bin(io: NdtBoxIo, config: dict, file_path: str, erase_ms: int, con
516
517
  f"生效配置: {effective_config_desc()}"))
517
518
  # P1-1: off_ms 可配置 (CLI --off-ms > lab.yaml ndtdbg.flash.off_ms > 内置 500ms)。
518
519
  # 独立看门狗芯片 (如 CSA37F72 IWDG_S) 只有真 POR 能清零, 200ms 可能只是深度掉压。
520
+ flash_cfg = ndt_cfg.get("flash") or {}
519
521
  if off_ms is None:
520
- flash_cfg = ndt_cfg.get("flash") or {}
521
522
  off_ms = flash_cfg.get("off_ms")
523
+ # 0.7.1: boot 判据可整体关闭 (差分判据仍是启发式; 现场遇到与本 ROM 行为不符的
524
+ # 目标时, 用 off 让页写后的逐窗校验做唯一判据 —— 即桥内单体 flash 的语义)
525
+ if boot_judge is None:
526
+ boot_judge = flash_cfg.get("boot_judge")
522
527
 
523
528
  def progress(phase: str, done: int, total: int) -> None:
524
529
  print(f"[flash-bin] {phase} {done}/{total}", file=sys.stderr, flush=True)
525
530
 
526
531
  result = ChipSeaFxx(io).flash_bin(file_path, erase_ms=erase_ms, off_ms=off_ms,
527
- rewrite_times=rewrite_times, progress=progress)
532
+ rewrite_times=rewrite_times, boot_judge=boot_judge,
533
+ progress=progress)
528
534
  result["authorization"] = {"write_allowed": True,
529
535
  "granted_by": write_allowed_source() or "(default false)",
530
536
  "chain": list(_CONFIG_FILES)}
531
537
  ok("flash-bin", result,
532
538
  log=f"UpdateFW({file_path}): {result['fw_len']}B, {result['pages_written']} pages, "
533
539
  f"off_ms={result.get('off_ms')}, rewrite={result.get('rewrite_count', 0)}, "
534
- f"verify={result['verify']}")
540
+ f"boot_judge={result.get('boot_judge')}, verify={result['verify']}")
535
541
 
536
542
 
537
543
  # --------------------------------------------------------------------------- main
@@ -582,6 +588,9 @@ def build_parser() -> argparse.ArgumentParser:
582
588
  "独立看门狗芯片需要真 POR)")
583
589
  f.add_argument("--rewrite-times", type=int, default=None,
584
590
  help="校验不一致时整片重写次数上限 (默认对齐 ChipSea.cs 的 3; 0=不重写)")
591
+ f.add_argument("--boot-judge", choices=["diff", "off"], default=None,
592
+ help="boot 判据 (默认 diff: 只有'擦除魔数前后同一条非平凡应答'才熔断; "
593
+ "off: 完全关闭探测判据, 仅记录证据, 由逐窗校验做唯一判据)")
585
594
 
586
595
  st = sub.add_parser("stream", parents=[common],
587
596
  help="高速连续采集寄存器组到 CSV (单会话长驻, 只读)")
@@ -641,7 +650,8 @@ def main(argv=None) -> None:
641
650
  cmd_poll(io, args.duration_ms, args.interval_ms, args.max_frames, args.full)
642
651
  elif args.cmd == "flash-bin":
643
652
  cmd_flash_bin(io, config, args.file, args.erase_ms, args.confirm,
644
- off_ms=args.off_ms, rewrite_times=args.rewrite_times)
653
+ off_ms=args.off_ms, rewrite_times=args.rewrite_times,
654
+ boot_judge=args.boot_judge)
645
655
  elif args.cmd == "stream":
646
656
  specs = [parse_reg_spec(s) for s in args.regs]
647
657
  cmd_stream(io, config, specs, args.interval_ms, args.duration_s,
@@ -1,6 +1,6 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Dual-Tier MCP setup and registration helper (v0.7.0).
3
+ Dual-Tier MCP setup and registration helper (v0.7.1).
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.0)" -ForegroundColor Green
245
+ Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.7.1)" -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