firmwareloop 0.7.1__tar.gz → 0.7.2__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.
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/PKG-INFO +8 -5
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/README.md +6 -3
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/firmwareloop.egg-info/PKG-INFO +8 -5
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/firmwareloop.egg-info/requires.txt +1 -1
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/pyproject.toml +2 -2
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/__init__.py +1 -1
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/fw_mcp_server.py +19 -12
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/lib/ndtdbg.py +211 -6
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/lib/serial_assistant.py +43 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/ndtdbg_cli.py +30 -10
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/serial_cli.py +23 -5
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/setup-agent-mcp.ps1 +2 -2
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/LICENSE +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/firmwareloop.egg-info/SOURCES.txt +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/firmwareloop.egg-info/dependency_links.txt +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/firmwareloop.egg-info/entry_points.txt +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/firmwareloop.egg-info/top_level.txt +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/setup.cfg +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/acceptance-scenario.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/build.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/can.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/check-qoder-mcp.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/common/build-backends.psm1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/common/fw.psm1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/common/uart_probe.py +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/doctor.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/flash.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/instrument_cli.py +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/lib/__init__.py +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/lib/instruments.py +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/logic_capture.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/logic_decode.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/ndtdbg_bridge.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/reset.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/sigrok_csv_to_fwloop.py +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/test-backends.ps1 +0 -0
- {firmwareloop-0.7.1 → firmwareloop-0.7.2}/tools/test.ps1 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firmwareloop
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
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.
|
|
21
|
+
Requires-Dist: pyvisa-py>=0.7.2
|
|
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.
|
|
386
|
+
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.2)
|
|
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.
|
|
396
|
+
## 能力验证状态(v0.7.2)
|
|
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
|
|
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 |
|
|
420
420
|
| 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
|
|
421
421
|
|
|
422
422
|
---
|
|
@@ -434,6 +434,9 @@ claude mcp remove --scope user agentic-hil
|
|
|
434
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
|
+
| `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"` |
|
|
439
|
+
| 长调用(capture/measure)客户端超时 | MCP stdio 循环是**单线程**的:长调用会让同会话其它调用排队(含 3s 的小调用)→ 缩短窗口,或用 `fw_ndtdbg_stream_start` job 模式 |
|
|
437
440
|
|
|
438
441
|
完整速查表与手工部署细节见 [`Fwloop_setup_pack/README.md`](Fwloop_setup_pack/README.md)。
|
|
439
442
|
|
|
@@ -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.
|
|
358
|
+
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.2)
|
|
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.
|
|
368
|
+
## 能力验证状态(v0.7.2)
|
|
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
|
|
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 |
|
|
392
392
|
| 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
|
|
393
393
|
|
|
394
394
|
---
|
|
@@ -406,6 +406,9 @@ claude mcp remove --scope user agentic-hil
|
|
|
406
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
|
+
| `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"` |
|
|
411
|
+
| 长调用(capture/measure)客户端超时 | MCP stdio 循环是**单线程**的:长调用会让同会话其它调用排队(含 3s 的小调用)→ 缩短窗口,或用 `fw_ndtdbg_stream_start` job 模式 |
|
|
409
412
|
|
|
410
413
|
完整速查表与手工部署细节见 [`Fwloop_setup_pack/README.md`](Fwloop_setup_pack/README.md)。
|
|
411
414
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firmwareloop
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
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.
|
|
21
|
+
Requires-Dist: pyvisa-py>=0.7.2
|
|
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.
|
|
386
|
+
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.2)
|
|
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.
|
|
396
|
+
## 能力验证状态(v0.7.2)
|
|
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
|
|
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 |
|
|
420
420
|
| 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
|
|
421
421
|
|
|
422
422
|
---
|
|
@@ -434,6 +434,9 @@ claude mcp remove --scope user agentic-hil
|
|
|
434
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
|
+
| `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"` |
|
|
439
|
+
| 长调用(capture/measure)客户端超时 | MCP stdio 循环是**单线程**的:长调用会让同会话其它调用排队(含 3s 的小调用)→ 缩短窗口,或用 `fw_ndtdbg_stream_start` job 模式 |
|
|
437
440
|
|
|
438
441
|
完整速查表与手工部署细节见 [`Fwloop_setup_pack/README.md`](Fwloop_setup_pack/README.md)。
|
|
439
442
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "firmwareloop"
|
|
7
|
-
version = "0.7.
|
|
7
|
+
version = "0.7.2"
|
|
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.
|
|
17
|
+
"pyvisa-py>=0.7.2",
|
|
18
18
|
"pyserial>=3.5",
|
|
19
19
|
"pytest>=8.0.0",
|
|
20
20
|
"pyocd>=0.36.0",
|
|
@@ -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.
|
|
3
|
+
fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.7.2).
|
|
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.
|
|
335
|
+
P1-3 (0.7.2): 去掉按签名重试 —— "TARGET_MISMATCH @0x0000" 正是"根本没进 boot"的
|
|
336
336
|
特征签名 (旧 boot 判据假阳的同一条错误路径), 盲目重试只会重复同一失败路径。
|
|
337
337
|
整片重写 (<= rewrite_times) 现由 flash_exec 内部完成, 结果带 rewrite_count;
|
|
338
338
|
BOOT_NOT_ENTERED 直接返回指引 (需断电重启, 重试无意义)。
|
|
@@ -346,6 +346,8 @@ def handle_fw_ndtdbg_flash(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
346
346
|
cmd.extend(["--rewrite-times", str(max(0, int(args["rewrite_times"])))])
|
|
347
347
|
if args.get("boot_judge"):
|
|
348
348
|
cmd.extend(["--boot-judge", str(args["boot_judge"])])
|
|
349
|
+
if args.get("flash_engine"):
|
|
350
|
+
cmd.extend(["--flash-engine", str(args["flash_engine"])])
|
|
349
351
|
if args.get("confirm"):
|
|
350
352
|
cmd.append("--confirm")
|
|
351
353
|
_ndtdbg_reg_args(args, cmd)
|
|
@@ -1679,7 +1681,7 @@ TOOLS_REGISTRY = {
|
|
|
1679
1681
|
"handler": handle_fw_ndtdbg_status,
|
|
1680
1682
|
},
|
|
1681
1683
|
"fw_ndtdbg_measure": {
|
|
1682
|
-
"description": "Poll NdtBox IIC debug data stream (ChipSea Fxx) for N seconds: frame count, checksum errors, channel samples. Read-only
|
|
1684
|
+
"description": "Poll NdtBox IIC debug data stream (ChipSea Fxx) for N seconds: frame count, checksum errors, channel samples. Read-only. NOTE: the MCP stdio loop is single-threaded, so a long call blocks every other call in the same session (a concurrent short call just waits and may time out client-side) - keep duration_seconds small; use fw_ndtdbg_stream_start for long captures.",
|
|
1683
1685
|
"inputSchema": {
|
|
1684
1686
|
"type": "object",
|
|
1685
1687
|
"properties": {
|
|
@@ -1772,7 +1774,7 @@ TOOLS_REGISTRY = {
|
|
|
1772
1774
|
"handler": handle_fw_ndtdbg_write_reg,
|
|
1773
1775
|
},
|
|
1774
1776
|
"fw_ndtdbg_flash": {
|
|
1775
|
-
"description": "Erase and flash a .bin/.nfw firmware image onto the NdtBox target chip (ChipSea Fxx) over IIC via NdtDebugTool
|
|
1777
|
+
"description": "Erase and flash a .bin/.nfw firmware image onto the NdtBox target chip (ChipSea Fxx) over IIC via NdtDebugTool. Default engine 'bridge' runs the whole UpdateFW sequence in ONE 32-bit process holding both the IIC and power-rail handles (power-cycle into boot -> erase magic AA 55 A5 5A -> 128B page writes -> 256B read-back verify per window = the authoritative criterion -> finish 'PASs'), which is the path validated 6/6 on real hardware; engine 'serve' drives one raw transaction per page from Python (kept for A/B reproduction - field evidence showed it can report erase-effective but write nothing on the same frames). DESTRUCTIVE full-chip erase: requires lab.yaml ndtdbg.write_allowed: true AND confirm: true, otherwise PERMISSION_DENIED.",
|
|
1776
1778
|
"inputSchema": {
|
|
1777
1779
|
"type": "object",
|
|
1778
1780
|
"properties": {
|
|
@@ -1806,7 +1808,12 @@ TOOLS_REGISTRY = {
|
|
|
1806
1808
|
"boot_judge": {
|
|
1807
1809
|
"type": "string",
|
|
1808
1810
|
"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",
|
|
1811
|
+
"description": "Boot-probe criterion (serve engine only). '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",
|
|
1812
|
+
},
|
|
1813
|
+
"flash_engine": {
|
|
1814
|
+
"type": "string",
|
|
1815
|
+
"enum": ["bridge", "serve"],
|
|
1816
|
+
"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",
|
|
1810
1817
|
},
|
|
1811
1818
|
"dll_dir": {
|
|
1812
1819
|
"type": "string",
|
|
@@ -2242,7 +2249,7 @@ TOOLS_REGISTRY = {
|
|
|
2242
2249
|
"handler": handle_fw_serial_parse,
|
|
2243
2250
|
},
|
|
2244
2251
|
"fw_serial_capture": {
|
|
2245
|
-
"description": "Serial assistant: synchronous window capture - record the RX stream for duration_ms into a timestamped CSV (t_rel_ms, dir, hex, printable) for offline analysis/plotting (real-time-curve equivalent; plotting happens in your tools/Agent from the CSV). Pass power_cycle_off_ms to fire an NdtBox power-cycle inside the window and capture boot-time prints (port is opened first). duration_ms <= 90000; long background captures (job mode) are a planned follow-up.",
|
|
2252
|
+
"description": "Serial assistant: synchronous window capture - record the RX stream for duration_ms into a timestamped CSV (t_rel_ms, dir, hex, printable) for offline analysis/plotting (real-time-curve equivalent; plotting happens in your tools/Agent from the CSV). Pass power_cycle_off_ms to fire an NdtBox power-cycle inside the window and capture boot-time prints (port is opened first). duration_ms <= 90000; long background captures (job mode) are a planned follow-up. NOTE: the MCP stdio loop is single-threaded, so this call blocks every other call in the same session for the whole window (your client may time out first) - prefer windows of a few seconds and call it when nothing else needs the box.",
|
|
2246
2253
|
"inputSchema": {
|
|
2247
2254
|
"type": "object",
|
|
2248
2255
|
"properties": {
|
|
@@ -2307,7 +2314,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
2307
2314
|
},
|
|
2308
2315
|
"serverInfo": {
|
|
2309
2316
|
"name": "firmwareloop",
|
|
2310
|
-
"version": "0.7.
|
|
2317
|
+
"version": "0.7.2"
|
|
2311
2318
|
}
|
|
2312
2319
|
}
|
|
2313
2320
|
}
|
|
@@ -2442,7 +2449,7 @@ def _settle_stashed_entrypoints(stashed) -> None:
|
|
|
2442
2449
|
def handle_cli_update() -> int:
|
|
2443
2450
|
"""Handle `firmwareloop update` command."""
|
|
2444
2451
|
print("============================================================")
|
|
2445
|
-
print(" FirmwareLoop Auto-Updater (v0.7.
|
|
2452
|
+
print(" FirmwareLoop Auto-Updater (v0.7.2)")
|
|
2446
2453
|
print("============================================================")
|
|
2447
2454
|
|
|
2448
2455
|
is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
|
|
@@ -2873,7 +2880,7 @@ def set_ndtdbg_dlldir(dll_path: str) -> str:
|
|
|
2873
2880
|
def handle_cli_setup() -> int:
|
|
2874
2881
|
"""Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, and CodeBuddy IDE."""
|
|
2875
2882
|
print("============================================================")
|
|
2876
|
-
print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.7.
|
|
2883
|
+
print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.7.2)")
|
|
2877
2884
|
print("============================================================")
|
|
2878
2885
|
print("")
|
|
2879
2886
|
|
|
@@ -2998,7 +3005,7 @@ def handle_cli_setup() -> int:
|
|
|
2998
3005
|
def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
|
|
2999
3006
|
"""Handle `firmwareloop init [target_chip]` command."""
|
|
3000
3007
|
print("============================================================")
|
|
3001
|
-
print(" FirmwareLoop Multi-Agent Project Initializer (v0.7.
|
|
3008
|
+
print(" FirmwareLoop Multi-Agent Project Initializer (v0.7.2)")
|
|
3002
3009
|
print("============================================================")
|
|
3003
3010
|
cwd = os.getcwd()
|
|
3004
3011
|
print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
|
|
@@ -3031,7 +3038,7 @@ def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
|
|
|
3031
3038
|
|
|
3032
3039
|
|
|
3033
3040
|
def print_cli_help() -> None:
|
|
3034
|
-
print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.7.
|
|
3041
|
+
print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.7.2)
|
|
3035
3042
|
|
|
3036
3043
|
Usage:
|
|
3037
3044
|
fwloop [command] (or: firmwareloop [command])
|
|
@@ -3077,7 +3084,7 @@ def main() -> None:
|
|
|
3077
3084
|
sys.exit(2)
|
|
3078
3085
|
sys.exit(handle_cli_ndtdebugtool(args[1]))
|
|
3079
3086
|
elif cmd in ["version", "-v", "--version"]:
|
|
3080
|
-
print("FirmwareLoop v0.7.
|
|
3087
|
+
print("FirmwareLoop v0.7.2")
|
|
3081
3088
|
sys.exit(0)
|
|
3082
3089
|
elif cmd in ["help", "-h", "--help"]:
|
|
3083
3090
|
print_cli_help()
|
|
@@ -11,6 +11,7 @@ import contextlib
|
|
|
11
11
|
import json
|
|
12
12
|
import os
|
|
13
13
|
import queue
|
|
14
|
+
import re
|
|
14
15
|
import subprocess
|
|
15
16
|
import sys
|
|
16
17
|
import threading
|
|
@@ -402,13 +403,19 @@ class NdtBoxIo:
|
|
|
402
403
|
return self._persistent
|
|
403
404
|
|
|
404
405
|
def _with_sess(self, fn):
|
|
406
|
+
"""在活跃会话上执行单事务。
|
|
407
|
+
|
|
408
|
+
`with io.session():` 块内 self._persistent 已置位 ⇒ 复用同一个 32 位桥进程与
|
|
409
|
+
USB 会话 (事务延迟 = 一次管道往返); 块外为即用即关 (spawn+open+事务+close)。
|
|
410
|
+
(现场曾怀疑此处未复用导致 77ms/页——每笔新 spawn 是 ~1.5s 量级, 与 77ms 不符,
|
|
411
|
+
故复用确实生效; 该现象的真因见 flash_exec 的引擎说明。)
|
|
412
|
+
"""
|
|
405
413
|
sess = self._get_live_sess()
|
|
406
|
-
if self._persistent is sess:
|
|
407
|
-
return fn(sess)
|
|
408
414
|
try:
|
|
409
415
|
return fn(sess)
|
|
410
416
|
finally:
|
|
411
|
-
sess.
|
|
417
|
+
if sess is not self._persistent:
|
|
418
|
+
sess.close()
|
|
412
419
|
|
|
413
420
|
# -- 芯片无关原语 (对齐 IIC.cs / I2CProxy.cs) --
|
|
414
421
|
|
|
@@ -424,7 +431,18 @@ class NdtBoxIo:
|
|
|
424
431
|
return self._with_sess(lambda s: s.read_reg(reg, length))
|
|
425
432
|
|
|
426
433
|
def write_reg(self, reg: int, data: bytes) -> None:
|
|
427
|
-
|
|
434
|
+
try:
|
|
435
|
+
self._with_sess(lambda s: s.write_reg(reg, data))
|
|
436
|
+
except NdtBoxError as exc:
|
|
437
|
+
if exc.error_class != "TARGET_MISMATCH":
|
|
438
|
+
raise
|
|
439
|
+
# P2-1: 写 NACK ≠ 从机死了 —— 现场实证"读正常但写稳定失败"
|
|
440
|
+
raise NdtBoxError(
|
|
441
|
+
exc.error_class, exc.message, exc.detail,
|
|
442
|
+
{"hint": "写事务无应答与'从机整体不应答'不同: 若紧随其后的读事务正常, 更可能是"
|
|
443
|
+
"该寄存器不可写/写保护, 或目标固件未实现该写寄存器 (也可能是写事务"
|
|
444
|
+
"超长/格式不符)。先用 fw_ndtdbg_read_reg 回读邻居寄存器确认链路仍活, "
|
|
445
|
+
"再核对寄存器语义"}) from exc
|
|
428
446
|
|
|
429
447
|
def raw_write_read(self, write: bytes, read_len: int = 0) -> bytes | None:
|
|
430
448
|
"""原始 IIC 事务 (对齐 IIC.WriteRead): 写 write 字节(不含从机地址) 再读 read_len 字节。
|
|
@@ -660,7 +678,190 @@ class NdtBoxIo:
|
|
|
660
678
|
return None
|
|
661
679
|
raise
|
|
662
680
|
|
|
681
|
+
def _bridge_plan_json(self, plan: dict) -> str:
|
|
682
|
+
"""桥 flash action 的 stdin plan (只含桥认识的键)。"""
|
|
683
|
+
return json.dumps({
|
|
684
|
+
"off_ms": int(plan.get("off_ms", DEFAULT_OFF_MS)),
|
|
685
|
+
"erase_ms": int(plan.get("erase_ms", 2000)),
|
|
686
|
+
"page_gap_ms": int(plan.get("page_gap_ms", PAGE_WRITE_GAP_MS)),
|
|
687
|
+
"read_gap_ms": int(plan.get("read_gap_ms", PAGE_READ_GAP_MS)),
|
|
688
|
+
"writes": list(plan.get("writes", []) or []),
|
|
689
|
+
"windows": list(plan.get("windows", []) or []),
|
|
690
|
+
}, ensure_ascii=False)
|
|
691
|
+
|
|
692
|
+
def _bridge_flash_once(self, plan: dict) -> dict:
|
|
693
|
+
"""单次桥内单体烧录 (ndtdbg_bridge.ps1 -Action flash): 返回 data dict, 失败抛 NdtBoxError。
|
|
694
|
+
|
|
695
|
+
桥侧 #progress 行转发到 stderr (agent 可见进度), 末行 JSON 为结果 / 错误信封。
|
|
696
|
+
桥自身无超时 ⇒ 本层按 plan 推预算守护, 超时 kill 并报 BUS_TIMEOUT (不留孤儿进程)。
|
|
697
|
+
"""
|
|
698
|
+
import time
|
|
699
|
+
if not os.path.exists(PS32):
|
|
700
|
+
raise NdtBoxError("CONFIG_ERROR", f"32 位 PowerShell 不存在: {PS32}")
|
|
701
|
+
if not os.path.isfile(os.path.join(self.dll_dir, "USBIOX.DLL")):
|
|
702
|
+
raise NdtBoxError("CONFIG_ERROR", f"USBIOX.DLL 不存在: {self.dll_dir}",
|
|
703
|
+
"lab.yaml 的 ndtdbg.dll_dir 指向 NdtDebugTool 的 lib 目录")
|
|
704
|
+
args = [PS32, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
705
|
+
"-File", self.bridge_ps1, "-Action", "flash",
|
|
706
|
+
"-DllDir", self.dll_dir, "-SlaveAddr", str(self.slave_addr),
|
|
707
|
+
"-Speed", str(self.speed)]
|
|
708
|
+
pages = len(plan.get("writes") or [])
|
|
709
|
+
windows = len(plan.get("windows") or [])
|
|
710
|
+
# 预算: 页写/回读节奏 + 擦除 + 三次断电复位 + 固定裕量; plan["budget_ms"] 可覆盖 (测试用)
|
|
711
|
+
budget_ms = int(plan.get("budget_ms") or (
|
|
712
|
+
pages * (int(plan.get("page_gap_ms", PAGE_WRITE_GAP_MS)) + 60)
|
|
713
|
+
+ windows * (int(plan.get("read_gap_ms", PAGE_READ_GAP_MS)) + 40)
|
|
714
|
+
+ int(plan.get("erase_ms", 2000))
|
|
715
|
+
+ int(plan.get("off_ms", DEFAULT_OFF_MS)) * 3 + 30000))
|
|
716
|
+
t0 = time.monotonic()
|
|
717
|
+
proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
718
|
+
stderr=subprocess.PIPE, text=True, encoding="utf-8",
|
|
719
|
+
errors="replace", bufsize=1)
|
|
720
|
+
lines: list = []
|
|
721
|
+
|
|
722
|
+
def _reader() -> None:
|
|
723
|
+
try:
|
|
724
|
+
for raw in proc.stdout:
|
|
725
|
+
line = (raw or "").strip()
|
|
726
|
+
if not line:
|
|
727
|
+
continue
|
|
728
|
+
if line.startswith("#progress"):
|
|
729
|
+
print(f"[flash] {line[len('#progress'):].strip()}",
|
|
730
|
+
file=sys.stderr, flush=True)
|
|
731
|
+
continue
|
|
732
|
+
if line.startswith("{"):
|
|
733
|
+
lines.append(line)
|
|
734
|
+
except Exception:
|
|
735
|
+
pass
|
|
736
|
+
|
|
737
|
+
reader = threading.Thread(target=_reader, daemon=True)
|
|
738
|
+
reader.start()
|
|
739
|
+
try:
|
|
740
|
+
try:
|
|
741
|
+
proc.stdin.write(self._bridge_plan_json(plan))
|
|
742
|
+
proc.stdin.close()
|
|
743
|
+
except Exception as exc:
|
|
744
|
+
proc.kill()
|
|
745
|
+
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:
|
|
748
|
+
if proc.poll() is not None and not reader.is_alive():
|
|
749
|
+
break
|
|
750
|
+
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
|
+
elapsed_ms = round((time.monotonic() - t0) * 1000)
|
|
759
|
+
if not lines:
|
|
760
|
+
err = ""
|
|
761
|
+
try:
|
|
762
|
+
err = (proc.stderr.read() or "")[-300:]
|
|
763
|
+
except Exception:
|
|
764
|
+
pass
|
|
765
|
+
raise NdtBoxError("UNKNOWN_ERROR",
|
|
766
|
+
f"桥内 flash 未返回 JSON 结果 (exit={proc.returncode})", err or None)
|
|
767
|
+
try:
|
|
768
|
+
body = json.loads(lines[-1])
|
|
769
|
+
except ValueError as exc:
|
|
770
|
+
raise NdtBoxError("UNKNOWN_ERROR", f"桥内 flash 结果解析失败: {exc}",
|
|
771
|
+
lines[-1][:200]) from exc
|
|
772
|
+
if not body.get("ok"):
|
|
773
|
+
cls = body.get("error_class") or "UNKNOWN_ERROR"
|
|
774
|
+
msg = str(body.get("error") or "桥内 flash 失败")
|
|
775
|
+
detail = body.get("detail")
|
|
776
|
+
extra: dict = {"engine": "bridge"}
|
|
777
|
+
m = re.search(r"@0x([0-9A-Fa-f]{4})", msg)
|
|
778
|
+
if m:
|
|
779
|
+
extra["first_mismatch_offset"] = int(m.group(1), 16)
|
|
780
|
+
mm = re.search(r"expect=([0-9A-Fa-f]*) got=([0-9A-Fa-f]*)", str(detail or ""))
|
|
781
|
+
if mm:
|
|
782
|
+
extra["first_mismatch_expect"] = mm.group(1).upper()
|
|
783
|
+
extra["first_mismatch_got"] = mm.group(2).upper()
|
|
784
|
+
raise NdtBoxError(cls, msg, detail, extra)
|
|
785
|
+
data = body.get("data") or {}
|
|
786
|
+
data["elapsed_ms"] = elapsed_ms
|
|
787
|
+
return data
|
|
788
|
+
finally:
|
|
789
|
+
try:
|
|
790
|
+
if proc.poll() is None:
|
|
791
|
+
proc.kill()
|
|
792
|
+
except Exception:
|
|
793
|
+
pass
|
|
794
|
+
|
|
795
|
+
def _flash_exec_bridge(self, plan: dict) -> dict:
|
|
796
|
+
"""桥内单体 flash 引擎 (0.7.2 起为默认): 单进程内 USBIOX(0) 与 USB2IO(1) 句柄同持,
|
|
797
|
+
|
|
798
|
+
帧经 stdin 一次性传入、全程连续无逐笔管道往返 —— 即原厂 GUI 的进程模型, 也是
|
|
799
|
+
现场 (2026-09-11 F72) 6/6 verify pass 的那条路径。校验失败 (桥报
|
|
800
|
+
TARGET_MISMATCH "烧录校验失败") 时按 rewrite_times 整片重跑, 其余错误直接上抛。
|
|
801
|
+
"""
|
|
802
|
+
import time
|
|
803
|
+
rewrite_times = max(0, int(plan.get("rewrite_times", _REWRITE_TIMES)))
|
|
804
|
+
t_all = time.monotonic()
|
|
805
|
+
attempts_detail: list = []
|
|
806
|
+
for attempt in range(rewrite_times + 1):
|
|
807
|
+
try:
|
|
808
|
+
data = self._bridge_flash_once(plan)
|
|
809
|
+
except NdtBoxError as exc:
|
|
810
|
+
attempts_detail.append({
|
|
811
|
+
"attempt": attempt + 1, "ok": False, "error_class": exc.error_class,
|
|
812
|
+
"first_mismatch_offset": exc.extra.get("first_mismatch_offset"),
|
|
813
|
+
"first_mismatch_expect": exc.extra.get("first_mismatch_expect"),
|
|
814
|
+
"first_mismatch_got": exc.extra.get("first_mismatch_got")})
|
|
815
|
+
if (exc.error_class == "TARGET_MISMATCH" and "校验失败" in exc.message
|
|
816
|
+
and attempt < rewrite_times):
|
|
817
|
+
continue # 整片重写 (桥内每次都是完整擦写)
|
|
818
|
+
raise NdtBoxError(
|
|
819
|
+
exc.error_class, exc.message, exc.detail,
|
|
820
|
+
{**exc.extra, "engine": "bridge", "attempts": attempt + 1,
|
|
821
|
+
"attempts_detail": attempts_detail,
|
|
822
|
+
"stage_timings": {"total_ms": round((time.monotonic() - t_all) * 1000)}})
|
|
823
|
+
attempts_detail.append({"attempt": attempt + 1, "ok": True,
|
|
824
|
+
"elapsed_ms": data.get("elapsed_ms"),
|
|
825
|
+
"verify": data.get("verify"),
|
|
826
|
+
"pages_written": data.get("pages_written")})
|
|
827
|
+
total_ms = round((time.monotonic() - t_all) * 1000)
|
|
828
|
+
return {
|
|
829
|
+
"verify": data.get("verify", "pass"),
|
|
830
|
+
"pages_written": data.get("pages_written"),
|
|
831
|
+
"read_windows": data.get("read_windows"),
|
|
832
|
+
"rewrite_count": attempt,
|
|
833
|
+
"attempts": attempt + 1,
|
|
834
|
+
"attempts_detail": attempts_detail,
|
|
835
|
+
"engine": "bridge",
|
|
836
|
+
"boot_judge": "n/a (bridge engine: 桥内无探测事务, 逐窗校验为唯一判据)",
|
|
837
|
+
"boot_evidence": "not_evaluated",
|
|
838
|
+
"probe_hex": None,
|
|
839
|
+
"probe_before_hex": None,
|
|
840
|
+
"off_ms": int(plan.get("off_ms", DEFAULT_OFF_MS)),
|
|
841
|
+
"stage_timings": {"total_ms": total_ms},
|
|
842
|
+
"total_ms": total_ms,
|
|
843
|
+
}
|
|
844
|
+
raise NdtBoxError("UNKNOWN_ERROR", "flash_exec(bridge) 未产出结果", None) # pragma: no cover
|
|
845
|
+
|
|
663
846
|
def flash_exec(self, plan: dict, timeout_ms: int | None = None) -> dict:
|
|
847
|
+
"""烧录引擎分派: "bridge" (默认, 桥内单体 flash) / "serve" (逐笔 Python 驱动)。
|
|
848
|
+
|
|
849
|
+
现场实证 (2026-09-11, F72, 同一批固件 + 同一台设备):
|
|
850
|
+
桥内单体 flash (帧经 stdin 一次传入) -> 6/6 {"ok":true,"verify":"pass"}
|
|
851
|
+
逐笔 serve 路径 (每页一笔 raw 事务) -> 100% "擦除生效但页写不落地"
|
|
852
|
+
(probe FFFFFFFF -> 00000000 说明 7 字节擦除帧到达; 99 页 write_ms 7594ms
|
|
853
|
+
≈ 77ms/页; 校验读回全 FF)
|
|
854
|
+
两条路径的**帧完全相同** (build_page_write_frame, 命令行核对无误), 故差异在
|
|
855
|
+
进程/传输模型而非帧组装 ⇒ 默认走桥内单体, serve 引擎保留供 A/B 复现与排查。
|
|
856
|
+
"""
|
|
857
|
+
engine = str(plan.get("engine", "bridge") or "bridge").lower()
|
|
858
|
+
if engine == "bridge":
|
|
859
|
+
return self._flash_exec_bridge(plan)
|
|
860
|
+
if engine == "serve":
|
|
861
|
+
return self._flash_exec_serve(plan)
|
|
862
|
+
raise NdtBoxError("CONFIG_ERROR", "flash engine 非法 (bridge/serve)", engine)
|
|
863
|
+
|
|
864
|
+
def _flash_exec_serve(self, plan: dict, timeout_ms: int | None = None) -> dict:
|
|
664
865
|
"""Python 驱动整条 UpdateFW 序列 (0.4.0 serve 化): usb2io 断电复位进 boot +
|
|
665
866
|
擦除 -> 页写 -> 回读校验 -> 完成魔数, 全部 I2C 事务经 serve 会话 (同进程同
|
|
666
867
|
会话连续, 复刻原厂 GUI 进程模型)。校验 (逐窗 vs expect_hex) 在本模块完成,
|
|
@@ -1192,7 +1393,8 @@ class ChipSeaFxx:
|
|
|
1192
1393
|
|
|
1193
1394
|
def flash_bin(self, file_path: str, erase_ms: int = 2000,
|
|
1194
1395
|
off_ms: int | None = None, rewrite_times: int | None = None,
|
|
1195
|
-
boot_judge: str | None = None,
|
|
1396
|
+
boot_judge: str | None = None, engine: str | None = None,
|
|
1397
|
+
progress=None) -> dict:
|
|
1196
1398
|
"""经 NdtBox IIC 烧录 .bin/.nfw 固件到芯海 Fxx (UpdateFW 语义)。
|
|
1197
1399
|
|
|
1198
1400
|
时序逐行对齐 ChipSea.cs UpdateFW, 进程模型对齐原厂 STDataCollection GUI:
|
|
@@ -1221,6 +1423,7 @@ class ChipSeaFxx:
|
|
|
1221
1423
|
"read_gap_ms": PAGE_READ_GAP_MS,
|
|
1222
1424
|
"rewrite_times": _REWRITE_TIMES if rewrite_times is None else max(0, int(rewrite_times)),
|
|
1223
1425
|
"boot_judge": str(boot_judge or "diff").lower(),
|
|
1426
|
+
"engine": str(engine or "bridge").lower(),
|
|
1224
1427
|
"writes": [build_page_write_frame(off, fw[off:off + FLASH_WRITE_PAGE]).hex()
|
|
1225
1428
|
for off in pages],
|
|
1226
1429
|
"windows": [{"off": off, "expect_hex": expected_read_window(fw, off).hex()}
|
|
@@ -1238,12 +1441,14 @@ class ChipSeaFxx:
|
|
|
1238
1441
|
"erase_ms": int(erase_ms),
|
|
1239
1442
|
"off_ms": plan["off_ms"],
|
|
1240
1443
|
"rewrite_times": plan["rewrite_times"],
|
|
1241
|
-
"
|
|
1444
|
+
"engine": res.get("engine", plan["engine"]),
|
|
1445
|
+
"boot_judge": res.get("boot_judge", plan["boot_judge"]),
|
|
1242
1446
|
"boot_evidence": res.get("boot_evidence"),
|
|
1243
1447
|
"probe_hex": res.get("probe_hex"),
|
|
1244
1448
|
"probe_before_hex": res.get("probe_before_hex"),
|
|
1245
1449
|
"rewrite_count": res.get("rewrite_count", 0),
|
|
1246
1450
|
"attempts": res.get("attempts", 1),
|
|
1451
|
+
"attempts_detail": res.get("attempts_detail"),
|
|
1247
1452
|
"stage_timings": res.get("stage_timings"),
|
|
1248
1453
|
"verify": res.get("verify", "pass"),
|
|
1249
1454
|
"duration_ms": int((time.monotonic() - started) * 1000),
|
|
@@ -994,6 +994,49 @@ class SerialSession:
|
|
|
994
994
|
return out
|
|
995
995
|
|
|
996
996
|
|
|
997
|
+
_PORT_HOG_HINTS = ("stdatacollection", "ndtdebug", "ndt debug", "serialassistant", "sscom",
|
|
998
|
+
"comassistant", "putty", "xshell", "securecrt", "minicom", "logic", "saleae")
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def suspected_port_holders(session_pid: int | None = None) -> list[dict]:
|
|
1002
|
+
"""列出可能占着串口的本机进程 (tasklist 扫描, best-effort; 失败返回空列表)。
|
|
1003
|
+
|
|
1004
|
+
Windows 没有便宜的"谁持有 COMx"API, 但现场 90% 的 PermissionError(13) 都是
|
|
1005
|
+
NDT Debug Tool GUI (STDataCollection.exe) 或残留的 fwloop serve 会话 —— 直接给
|
|
1006
|
+
出嫌疑名单比只报一句 PermissionError 更有用。session_pid 用于把"本会话 serve"
|
|
1007
|
+
与"其它 python (很可能是 MCP server, 勿盲杀)"区分开。
|
|
1008
|
+
"""
|
|
1009
|
+
out: list[dict] = []
|
|
1010
|
+
if os.name != "nt":
|
|
1011
|
+
return out
|
|
1012
|
+
try:
|
|
1013
|
+
res = subprocess.run(["tasklist", "/FO", "CSV", "/NH"], capture_output=True,
|
|
1014
|
+
text=True, timeout=15, errors="replace")
|
|
1015
|
+
except Exception: # noqa: BLE001
|
|
1016
|
+
return out
|
|
1017
|
+
self_pid = os.getpid()
|
|
1018
|
+
for line in (res.stdout or "").splitlines():
|
|
1019
|
+
cols = [c.strip().strip('"') for c in line.split('","')]
|
|
1020
|
+
if len(cols) < 2:
|
|
1021
|
+
continue
|
|
1022
|
+
name, pid_s = cols[0], cols[1]
|
|
1023
|
+
try:
|
|
1024
|
+
pid = int(pid_s)
|
|
1025
|
+
except ValueError:
|
|
1026
|
+
continue
|
|
1027
|
+
low = name.lower()
|
|
1028
|
+
if any(k in low for k in _PORT_HOG_HINTS):
|
|
1029
|
+
out.append({"name": name, "pid": pid,
|
|
1030
|
+
"why": "GUI/上位机软件可能独占串口 (关闭后再重试)"})
|
|
1031
|
+
elif low.startswith("python") and pid not in (self_pid, session_pid):
|
|
1032
|
+
out.append({"name": name, "pid": pid,
|
|
1033
|
+
"why": "python 进程 (可能是残留 serve 会话; 若为 MCP server 请勿终止)"})
|
|
1034
|
+
if session_pid:
|
|
1035
|
+
out = [h for h in out if h["pid"] != session_pid]
|
|
1036
|
+
out.append({"name": "(本会话 serve)", "pid": session_pid, "why": "正常: fw_serial_close 会回收"})
|
|
1037
|
+
return out
|
|
1038
|
+
|
|
1039
|
+
|
|
997
1040
|
def pid_alive(pid: int | None) -> bool:
|
|
998
1041
|
"""进程存活探测。
|
|
999
1042
|
|
|
@@ -249,9 +249,16 @@ def cmd_status(io: NdtBoxIo) -> None:
|
|
|
249
249
|
# P2-1 分层判据表: 把"全 FF/无应答"的几种成因摊开, 避免一句 hint 把人带偏
|
|
250
250
|
data["diagnosis_table"] = [
|
|
251
251
|
{"symptom": "能读到任一已知寄存器", "likely": "从机在跑, 链路正常 (判活以此为准)"},
|
|
252
|
-
{"symptom": "读回全 FF/FE、无异常",
|
|
252
|
+
{"symptom": "读回全 FF/FE、无异常",
|
|
253
|
+
"likely": "①目标固件未使能 I2C 从机 (最常见, 与掉电现象一致) ②目标掉电 ③从机地址不符"},
|
|
253
254
|
{"symptom": "TARGET_MISMATCH: IIC 事务无应答",
|
|
254
255
|
"likely": "地址未被 ACK (外设未使能/复位中/芯片未上电)"},
|
|
256
|
+
{"symptom": "只有写事务 NACK, 读事务正常",
|
|
257
|
+
"likely": "不是从机死: 该寄存器不可写/写保护, 或目标固件未实现该写寄存器 "
|
|
258
|
+
"(此时应回读邻近寄存器确认链路仍活)"},
|
|
259
|
+
{"symptom": "原本可读, 新增静态寄存器地址后整机读不到",
|
|
260
|
+
"likely": "新增地址破坏了从机寄存器表约定 (必须按 addr 升序且不与擦除魔数 0x0000 "
|
|
261
|
+
"冲突) ⇒ 建议优先复用已有寄存器的空闲字, 避免新增地址"},
|
|
255
262
|
{"symptom": "BUS_TIMEOUT", "likely": "时钟拉伸、关中断死循环 (fault handler 自旋饿死 I2C 中断)"},
|
|
256
263
|
]
|
|
257
264
|
ok("status", data, log="Open + ReadFwVersion(0x10) + ReadModuleConnect(0x29)")
|
|
@@ -288,9 +295,15 @@ def cmd_read_reg(io: NdtBoxIo, reg: int, length: int, retry: int = 0) -> None:
|
|
|
288
295
|
"io_ms": round((time.monotonic() - t0) * 1000, 1),
|
|
289
296
|
"retries": attempts - 1}
|
|
290
297
|
if data and all(b in (0xFF, 0xFE) for b in data):
|
|
291
|
-
# 总线无应答时上拉/悬空读回默认电平,
|
|
292
|
-
|
|
293
|
-
|
|
298
|
+
# 总线无应答时上拉/悬空读回默认电平, 不能当作真实寄存器值误导调用方。
|
|
299
|
+
# P2-1: 第一候选是"目标固件没使能从机"(现象与掉电完全一样), 而不是硬件掉了
|
|
300
|
+
body["warning"] = ("读回全部 0xFF/0xFE: 疑似 IIC 目标无应答。第一候选是【目标固件"
|
|
301
|
+
"根本没使能 I2C 从机】(从机表初始化提前 return / 未注册), 现象与掉电"
|
|
302
|
+
"完全一样; 其次才是掉电/接线/上拉/从机地址。可参考 fw_ndtdbg_status "
|
|
303
|
+
"的 diagnosis_table 逐条排除")
|
|
304
|
+
body["candidates"] = ["目标固件未使能 I2C 从机 (最常见, 现象与掉电一致)",
|
|
305
|
+
"目标掉电 / 复位中", "接线/上拉/供电异常",
|
|
306
|
+
"从机地址不符 (lab.yaml ndtdbg.slave_addr)"]
|
|
294
307
|
ok("read-reg", body, log=f"ReadReg(0x{reg:02X}, {length})")
|
|
295
308
|
|
|
296
309
|
|
|
@@ -505,7 +518,7 @@ def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s:
|
|
|
505
518
|
|
|
506
519
|
def cmd_flash_bin(io: NdtBoxIo, config: dict, file_path: str, erase_ms: int, confirm: bool,
|
|
507
520
|
off_ms: int | None = None, rewrite_times: int | None = None,
|
|
508
|
-
boot_judge: str | None = None) -> None:
|
|
521
|
+
boot_judge: str | None = None, engine: str | None = None) -> None:
|
|
509
522
|
# 烧录 = 整片擦除 + 页写, 破坏性高于 write-reg, 同款双闸门
|
|
510
523
|
ndt_cfg = config.get("ndtdbg") or {}
|
|
511
524
|
allowed = ndt_cfg.get("write_allowed", False)
|
|
@@ -524,20 +537,23 @@ def cmd_flash_bin(io: NdtBoxIo, config: dict, file_path: str, erase_ms: int, con
|
|
|
524
537
|
# 目标时, 用 off 让页写后的逐窗校验做唯一判据 —— 即桥内单体 flash 的语义)
|
|
525
538
|
if boot_judge is None:
|
|
526
539
|
boot_judge = flash_cfg.get("boot_judge")
|
|
540
|
+
# 0.7.2: 烧录引擎 (bridge 默认 = 桥内单体 flash; serve = 逐笔 Python 驱动, 供 A/B 复现)
|
|
541
|
+
if engine is None:
|
|
542
|
+
engine = flash_cfg.get("engine")
|
|
527
543
|
|
|
528
544
|
def progress(phase: str, done: int, total: int) -> None:
|
|
529
545
|
print(f"[flash-bin] {phase} {done}/{total}", file=sys.stderr, flush=True)
|
|
530
546
|
|
|
531
547
|
result = ChipSeaFxx(io).flash_bin(file_path, erase_ms=erase_ms, off_ms=off_ms,
|
|
532
548
|
rewrite_times=rewrite_times, boot_judge=boot_judge,
|
|
533
|
-
progress=progress)
|
|
549
|
+
engine=engine, progress=progress)
|
|
534
550
|
result["authorization"] = {"write_allowed": True,
|
|
535
551
|
"granted_by": write_allowed_source() or "(default false)",
|
|
536
552
|
"chain": list(_CONFIG_FILES)}
|
|
537
553
|
ok("flash-bin", result,
|
|
538
554
|
log=f"UpdateFW({file_path}): {result['fw_len']}B, {result['pages_written']} pages, "
|
|
539
|
-
f"
|
|
540
|
-
f"
|
|
555
|
+
f"engine={result.get('engine')}, off_ms={result.get('off_ms')}, "
|
|
556
|
+
f"rewrite={result.get('rewrite_count', 0)}, verify={result['verify']}")
|
|
541
557
|
|
|
542
558
|
|
|
543
559
|
# --------------------------------------------------------------------------- main
|
|
@@ -590,7 +606,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
590
606
|
help="校验不一致时整片重写次数上限 (默认对齐 ChipSea.cs 的 3; 0=不重写)")
|
|
591
607
|
f.add_argument("--boot-judge", choices=["diff", "off"], default=None,
|
|
592
608
|
help="boot 判据 (默认 diff: 只有'擦除魔数前后同一条非平凡应答'才熔断; "
|
|
593
|
-
"off: 完全关闭探测判据, 仅记录证据, 由逐窗校验做唯一判据)"
|
|
609
|
+
"off: 完全关闭探测判据, 仅记录证据, 由逐窗校验做唯一判据); "
|
|
610
|
+
"仅 serve 引擎适用")
|
|
611
|
+
f.add_argument("--flash-engine", choices=["bridge", "serve"], default=None,
|
|
612
|
+
help="烧录引擎 (默认 bridge = 桥内单体 flash: 单进程、帧经 stdin 一次传入、"
|
|
613
|
+
"现场 6/6 verify pass; serve = 逐笔 raw 事务的 Python 驱动, 供 A/B 复现)")
|
|
594
614
|
|
|
595
615
|
st = sub.add_parser("stream", parents=[common],
|
|
596
616
|
help="高速连续采集寄存器组到 CSV (单会话长驻, 只读)")
|
|
@@ -651,7 +671,7 @@ def main(argv=None) -> None:
|
|
|
651
671
|
elif args.cmd == "flash-bin":
|
|
652
672
|
cmd_flash_bin(io, config, args.file, args.erase_ms, args.confirm,
|
|
653
673
|
off_ms=args.off_ms, rewrite_times=args.rewrite_times,
|
|
654
|
-
boot_judge=args.boot_judge)
|
|
674
|
+
boot_judge=args.boot_judge, engine=args.flash_engine)
|
|
655
675
|
elif args.cmd == "stream":
|
|
656
676
|
specs = [parse_reg_spec(s) for s in args.regs]
|
|
657
677
|
cmd_stream(io, config, specs, args.interval_ms, args.duration_s,
|
|
@@ -36,6 +36,7 @@ from lib.serial_assistant import ( # noqa: E402
|
|
|
36
36
|
parse_hex,
|
|
37
37
|
probe_session,
|
|
38
38
|
to_hex,
|
|
39
|
+
suspected_port_holders,
|
|
39
40
|
wait_pid_exit,
|
|
40
41
|
)
|
|
41
42
|
|
|
@@ -122,6 +123,8 @@ def cmd_status(_args: argparse.Namespace) -> None:
|
|
|
122
123
|
"started_at": stats.get("started_at")},
|
|
123
124
|
"conclusion": f"会话存活 (port={sess.get('port') or stats.get('port')}, "
|
|
124
125
|
f"opened={alive})" if alive else "serve 进程存活但串口未打开",
|
|
126
|
+
# P1-4: 串口被谁占着 —— 直接给嫌疑名单 (GUI/上位机 + 残留 python serve)
|
|
127
|
+
"suspected_holders": suspected_port_holders(sess.get("pid")),
|
|
125
128
|
})
|
|
126
129
|
|
|
127
130
|
|
|
@@ -149,6 +152,7 @@ def cmd_open(args: argparse.Namespace) -> None:
|
|
|
149
152
|
"holder_hint",
|
|
150
153
|
"端口可能被 GUI 上位机占用: NDT Debug Tool (STDataCollection.exe) 打开时会同时占用 "
|
|
151
154
|
"Box 串口与 USB-IIC 通道; 请关闭占用方后重试, 残留会话可用 fw_serial_close 清理")
|
|
155
|
+
extra.setdefault("suspected_holders", suspected_port_holders())
|
|
152
156
|
fail(exc.error_class, exc.detail, extra=extra)
|
|
153
157
|
data = {k: info.get(k) for k in
|
|
154
158
|
("port", "baud", "simulated", "log_path", "reused", "pid", "started_at")}
|
|
@@ -212,7 +216,7 @@ def cmd_send(args: argparse.Namespace) -> None:
|
|
|
212
216
|
time.sleep(delay / 1000.0)
|
|
213
217
|
stats = session.stats()
|
|
214
218
|
except SerialAssistantError as exc:
|
|
215
|
-
fail(exc.error_class, exc.detail, extra=exc
|
|
219
|
+
fail(exc.error_class, exc.detail, extra=_port_error_extra(exc))
|
|
216
220
|
ok("send", {"frames_sent": sent, "frames_defined": len(frames),
|
|
217
221
|
"elapsed_ms": round((time.monotonic() - t0) * 1000, 2),
|
|
218
222
|
"tx_bytes": stats.get("tx_bytes"), "rx_bytes": stats.get("rx_bytes")},
|
|
@@ -229,7 +233,7 @@ def cmd_read(args: argparse.Namespace) -> None:
|
|
|
229
233
|
else:
|
|
230
234
|
result = session.read(max_bytes=RX_BUFFER_LIMIT if args.drain else args.max_bytes)
|
|
231
235
|
except SerialAssistantError as exc:
|
|
232
|
-
fail(exc.error_class, exc.detail, extra=exc
|
|
236
|
+
fail(exc.error_class, exc.detail, extra=_port_error_extra(exc))
|
|
233
237
|
|
|
234
238
|
if args.drain and not args.expect:
|
|
235
239
|
# P2-3 (0909 提案): 单次排空整个缓冲; 大 payload 落盘而非进 MCP 响应,
|
|
@@ -321,7 +325,7 @@ def cmd_parse(args: argparse.Namespace) -> None:
|
|
|
321
325
|
frames = parser.feed(data_bytes)
|
|
322
326
|
frames += parser.finalize()
|
|
323
327
|
except SerialAssistantError as exc:
|
|
324
|
-
fail(exc.error_class, exc.detail, extra=exc
|
|
328
|
+
fail(exc.error_class, exc.detail, extra=_port_error_extra(exc))
|
|
325
329
|
valid_n = sum(1 for f in frames if f["valid"])
|
|
326
330
|
ok("parse", {"source": src, "frames_n": len(frames), "valid_n": valid_n,
|
|
327
331
|
"invalid_n": len(frames) - valid_n, "bytes": len(data_bytes),
|
|
@@ -329,6 +333,14 @@ def cmd_parse(args: argparse.Namespace) -> None:
|
|
|
329
333
|
log=f"SerialParse({src}): {len(frames)} frames, {valid_n} valid")
|
|
330
334
|
|
|
331
335
|
|
|
336
|
+
def _port_error_extra(exc: SerialAssistantError) -> dict:
|
|
337
|
+
"""串口类错误的统一附加信息: 嫌疑占用进程名单 (P1-4)。"""
|
|
338
|
+
extra = dict(exc.extra)
|
|
339
|
+
if exc.error_class in ("SERIAL_PORT_ERROR", "UART_BUSY"):
|
|
340
|
+
extra.setdefault("suspected_holders", suspected_port_holders())
|
|
341
|
+
return extra
|
|
342
|
+
|
|
343
|
+
|
|
332
344
|
def _run_power_cycle(off_ms: int) -> dict:
|
|
333
345
|
"""P2-4: 触发一次 NdtBox 断电复位 (复用 ndtdbg_cli power-cycle, 同 lab.yaml 解析链)。"""
|
|
334
346
|
cli = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ndtdbg_cli.py")
|
|
@@ -389,9 +401,15 @@ def cmd_capture(args: argparse.Namespace) -> None:
|
|
|
389
401
|
result = box.get("r") or {}
|
|
390
402
|
stats = session.stats()
|
|
391
403
|
except SerialAssistantError as exc:
|
|
392
|
-
fail(exc.error_class, exc.detail, extra=exc
|
|
404
|
+
fail(exc.error_class, exc.detail, extra=_port_error_extra(exc))
|
|
393
405
|
payload = {**result, "tx_bytes": stats.get("tx_bytes"),
|
|
394
406
|
"simulated": stats.get("simulated")}
|
|
407
|
+
if args.duration_ms >= 10000:
|
|
408
|
+
# P2: MCP stdio 循环单线程 —— 长调用会让同会话的其它调用排队, 客户端可能先超时
|
|
409
|
+
payload["blocking_notice"] = (
|
|
410
|
+
f"服务端本次阻塞 {args.duration_ms}ms: MCP stdio 是单线程的, 同一会话内其它工具"
|
|
411
|
+
"调用会排在它后面 (客户端超时往往先到)。长采集建议缩短窗口, 或用 ndtdbg stream "
|
|
412
|
+
"job 模式 (fw_ndtdbg_stream_start/status/stop) 让采集不占调用通道")
|
|
395
413
|
if power_off_ms is not None:
|
|
396
414
|
payload["power_cycle"] = pc_result
|
|
397
415
|
if not (pc_result or {}).get("ok"):
|
|
@@ -498,7 +516,7 @@ def main() -> None:
|
|
|
498
516
|
try:
|
|
499
517
|
handler(args)
|
|
500
518
|
except SerialAssistantError as exc:
|
|
501
|
-
fail(exc.error_class, exc.detail, extra=exc
|
|
519
|
+
fail(exc.error_class, exc.detail, extra=_port_error_extra(exc))
|
|
502
520
|
|
|
503
521
|
|
|
504
522
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
Dual-Tier MCP setup and registration helper (v0.7.
|
|
3
|
+
Dual-Tier MCP setup and registration helper (v0.7.2).
|
|
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.
|
|
245
|
+
Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.7.2)" -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|