firmwareloop 0.7.4__tar.gz → 0.9.0__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.4 → firmwareloop-0.9.0}/PKG-INFO +7 -4
  2. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/README.md +6 -3
  3. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/firmwareloop.egg-info/PKG-INFO +7 -4
  4. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/pyproject.toml +1 -1
  5. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/__init__.py +1 -1
  6. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/fw_mcp_server.py +366 -64
  7. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/lib/ndtdbg.py +63 -29
  8. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/ndtdbg_cli.py +33 -8
  9. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/setup-agent-mcp.ps1 +2 -2
  10. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/LICENSE +0 -0
  11. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/firmwareloop.egg-info/SOURCES.txt +0 -0
  12. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/firmwareloop.egg-info/dependency_links.txt +0 -0
  13. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/firmwareloop.egg-info/entry_points.txt +0 -0
  14. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/firmwareloop.egg-info/requires.txt +0 -0
  15. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/firmwareloop.egg-info/top_level.txt +0 -0
  16. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/setup.cfg +0 -0
  17. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/acceptance-scenario.ps1 +0 -0
  18. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/build.ps1 +0 -0
  19. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/can.ps1 +0 -0
  20. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/check-qoder-mcp.ps1 +0 -0
  21. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/common/build-backends.psm1 +0 -0
  22. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/common/fw.psm1 +0 -0
  23. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/common/uart_probe.py +0 -0
  24. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/doctor.ps1 +0 -0
  25. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/flash.ps1 +0 -0
  26. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/instrument_cli.py +0 -0
  27. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/lib/__init__.py +0 -0
  28. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/lib/instruments.py +0 -0
  29. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/lib/serial_assistant.py +0 -0
  30. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/logic_capture.ps1 +0 -0
  31. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/logic_decode.ps1 +0 -0
  32. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/ndtdbg_bridge.ps1 +0 -0
  33. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/reset.ps1 +0 -0
  34. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/serial_cli.py +0 -0
  35. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/sigrok_csv_to_fwloop.py +0 -0
  36. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/test-backends.ps1 +0 -0
  37. {firmwareloop-0.7.4 → firmwareloop-0.9.0}/tools/test.ps1 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.7.4
3
+ Version: 0.9.0
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
@@ -177,6 +177,8 @@ fwloop setup
177
177
  | Claude Code CLI | `~/.claude.json` |
178
178
  | Qoder / Qoder CN | `~/.qoder/mcp.json`、`~/.qoder-cn/mcp.json` |
179
179
  | CodeBuddy IDE | `~/.codebuddy/mcp.json` |
180
+ | DeepSeek Harness (DSH) | `~/.dsh/profiles/<已派生 profile>/cordis.patch.yml` |
181
+ | ZCode IDE | `~/.zcode/cli/config.json` |
180
182
 
181
183
  2. **同步技能包**(`firmwareloop`、`fwloop-adapter`)到 Antigravity 与 Claude Code 的全局 Skill 目录。
182
184
  3. **生成机器级全局配置** `%APPDATA%\FirmwareLoop\lab.yaml`(NdtBox 配置模板,已存在则不覆盖)。
@@ -383,7 +385,7 @@ claude mcp remove --scope user agentic-hil
383
385
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
384
386
  ├── demo-make/ 示例固件(Make;构建测试载体)
385
387
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
386
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.4)
388
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.9.0)
387
389
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
388
390
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
389
391
  ├── CLAUDE.md Claude Code CLI 指南
@@ -393,7 +395,7 @@ claude mcp remove --scope user agentic-hil
393
395
 
394
396
  ---
395
397
 
396
- ## 能力验证状态(v0.7.4
398
+ ## 能力验证状态(v0.9.0
397
399
 
398
400
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
399
401
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -401,6 +403,7 @@ claude mcp remove --scope user agentic-hil
401
403
  | 能力 | 状态 |
402
404
  |---|---|
403
405
  | 双层 MCP 服务(13 个工作流与硬件工具) | Implemented + Protocol Validated(31/31 测试通过) |
406
+ | fwloop setup 一键注册(7 Agent:Antigravity / Claude Code / Qoder(+CN) / CodeBuddy / DeepSeek Harness / ZCode) | Implemented + Multi-Agent Validated(DSH / ZCode 真机写入验证) |
404
407
  | 串口助手工具族(fw_serial_* 七工具, 长驻会话/发送序列/帧解析/窗口采集) | Implemented + Simulator Validated(55 例单测: 含残留 serve 自动清理 / close 进程回收 / UTF-8 线路 / 失败落 JSONL)-> Real Hardware Validated 待 DUT |
405
408
  | 一键多 Agent 脚手架(fwloop init / fw_init_project) | Implemented + Multi-Agent Validated |
406
409
  | 自定义/私有调试工具接入 Skill(fwloop-adapter) | Implemented + Multi-Skill Validated |
@@ -416,7 +419,7 @@ claude mcp remove --scope user agentic-hil
416
419
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
417
420
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
418
421
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
419
- | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
422
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(两批真实固件镜像真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
420
423
  | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
421
424
  | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
422
425
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
@@ -149,6 +149,8 @@ fwloop setup
149
149
  | Claude Code CLI | `~/.claude.json` |
150
150
  | Qoder / Qoder CN | `~/.qoder/mcp.json`、`~/.qoder-cn/mcp.json` |
151
151
  | CodeBuddy IDE | `~/.codebuddy/mcp.json` |
152
+ | DeepSeek Harness (DSH) | `~/.dsh/profiles/<已派生 profile>/cordis.patch.yml` |
153
+ | ZCode IDE | `~/.zcode/cli/config.json` |
152
154
 
153
155
  2. **同步技能包**(`firmwareloop`、`fwloop-adapter`)到 Antigravity 与 Claude Code 的全局 Skill 目录。
154
156
  3. **生成机器级全局配置** `%APPDATA%\FirmwareLoop\lab.yaml`(NdtBox 配置模板,已存在则不覆盖)。
@@ -355,7 +357,7 @@ claude mcp remove --scope user agentic-hil
355
357
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
356
358
  ├── demo-make/ 示例固件(Make;构建测试载体)
357
359
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
358
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.4)
360
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.9.0)
359
361
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
360
362
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
361
363
  ├── CLAUDE.md Claude Code CLI 指南
@@ -365,7 +367,7 @@ claude mcp remove --scope user agentic-hil
365
367
 
366
368
  ---
367
369
 
368
- ## 能力验证状态(v0.7.4
370
+ ## 能力验证状态(v0.9.0
369
371
 
370
372
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
371
373
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -373,6 +375,7 @@ claude mcp remove --scope user agentic-hil
373
375
  | 能力 | 状态 |
374
376
  |---|---|
375
377
  | 双层 MCP 服务(13 个工作流与硬件工具) | Implemented + Protocol Validated(31/31 测试通过) |
378
+ | fwloop setup 一键注册(7 Agent:Antigravity / Claude Code / Qoder(+CN) / CodeBuddy / DeepSeek Harness / ZCode) | Implemented + Multi-Agent Validated(DSH / ZCode 真机写入验证) |
376
379
  | 串口助手工具族(fw_serial_* 七工具, 长驻会话/发送序列/帧解析/窗口采集) | Implemented + Simulator Validated(55 例单测: 含残留 serve 自动清理 / close 进程回收 / UTF-8 线路 / 失败落 JSONL)-> Real Hardware Validated 待 DUT |
377
380
  | 一键多 Agent 脚手架(fwloop init / fw_init_project) | Implemented + Multi-Agent Validated |
378
381
  | 自定义/私有调试工具接入 Skill(fwloop-adapter) | Implemented + Multi-Skill Validated |
@@ -388,7 +391,7 @@ claude mcp remove --scope user agentic-hil
388
391
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
389
392
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
390
393
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
391
- | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
394
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(两批真实固件镜像真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
392
395
  | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
393
396
  | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
394
397
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.7.4
3
+ Version: 0.9.0
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
@@ -177,6 +177,8 @@ fwloop setup
177
177
  | Claude Code CLI | `~/.claude.json` |
178
178
  | Qoder / Qoder CN | `~/.qoder/mcp.json`、`~/.qoder-cn/mcp.json` |
179
179
  | CodeBuddy IDE | `~/.codebuddy/mcp.json` |
180
+ | DeepSeek Harness (DSH) | `~/.dsh/profiles/<已派生 profile>/cordis.patch.yml` |
181
+ | ZCode IDE | `~/.zcode/cli/config.json` |
180
182
 
181
183
  2. **同步技能包**(`firmwareloop`、`fwloop-adapter`)到 Antigravity 与 Claude Code 的全局 Skill 目录。
182
184
  3. **生成机器级全局配置** `%APPDATA%\FirmwareLoop\lab.yaml`(NdtBox 配置模板,已存在则不覆盖)。
@@ -383,7 +385,7 @@ claude mcp remove --scope user agentic-hil
383
385
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
384
386
  ├── demo-make/ 示例固件(Make;构建测试载体)
385
387
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
386
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.7.4)
388
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.9.0)
387
389
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
388
390
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
389
391
  ├── CLAUDE.md Claude Code CLI 指南
@@ -393,7 +395,7 @@ claude mcp remove --scope user agentic-hil
393
395
 
394
396
  ---
395
397
 
396
- ## 能力验证状态(v0.7.4
398
+ ## 能力验证状态(v0.9.0
397
399
 
398
400
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
399
401
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -401,6 +403,7 @@ claude mcp remove --scope user agentic-hil
401
403
  | 能力 | 状态 |
402
404
  |---|---|
403
405
  | 双层 MCP 服务(13 个工作流与硬件工具) | Implemented + Protocol Validated(31/31 测试通过) |
406
+ | fwloop setup 一键注册(7 Agent:Antigravity / Claude Code / Qoder(+CN) / CodeBuddy / DeepSeek Harness / ZCode) | Implemented + Multi-Agent Validated(DSH / ZCode 真机写入验证) |
404
407
  | 串口助手工具族(fw_serial_* 七工具, 长驻会话/发送序列/帧解析/窗口采集) | Implemented + Simulator Validated(55 例单测: 含残留 serve 自动清理 / close 进程回收 / UTF-8 线路 / 失败落 JSONL)-> Real Hardware Validated 待 DUT |
405
408
  | 一键多 Agent 脚手架(fwloop init / fw_init_project) | Implemented + Multi-Agent Validated |
406
409
  | 自定义/私有调试工具接入 Skill(fwloop-adapter) | Implemented + Multi-Skill Validated |
@@ -416,7 +419,7 @@ claude mcp remove --scope user agentic-hil
416
419
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
417
420
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
418
421
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
419
- | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(84.87.1.1 / 84.87.1.2 真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
422
+ | NdtBox IIC 固件烧录(fw_ndtdbg_flash, .bin/.nfw 整片擦写, 芯海 Fxx) | Real Hardware Validated(两批真实固件镜像真机整片擦写 verify pass; 2026-09-11 同批文件桥内单体 flash 6/6 pass;双闸门 Fail-Closed)。**v0.7.2 起默认引擎 = 桥内单体 flash**(逐笔 serve 路径现场实测"擦除生效但页写不落地",保留 `flash_engine: serve` 供 A/B);差分 boot 判据 + `boot_judge: off` 逃生开关见 0.7.1;**待验收声明:默认引擎路径(Python 组 plan → Popen 桥 → 解析信封)尚待真机验收** |
420
423
  | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
421
424
  | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
422
425
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "firmwareloop"
7
- version = "0.7.4"
7
+ version = "0.9.0"
8
8
  description = "AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -2,4 +2,4 @@
2
2
  FirmwareLoop - AI Agent Firmware Development and Lab Automation Platform.
3
3
  """
4
4
 
5
- __version__ = "0.7.4"
5
+ __version__ = "0.9.0"
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.7.4).
3
+ fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.9.0).
4
4
 
5
5
  Exposes high-level firmware engineering and hardware management tools to AI Coding
6
- Agents (Antigravity CLI, Claude Code CLI, Qoder IDE, CodeBuddy IDE, Cursor) via the Model Context
6
+ Agents (Antigravity CLI, Claude Code CLI, Qoder IDE, CodeBuddy IDE, DeepSeek Harness,
7
+ ZCode IDE, Cursor) via the Model Context
7
8
  Protocol (JSON-RPC 2.0 stdio).
8
9
 
9
10
  Tools provided:
@@ -383,13 +384,16 @@ def handle_fw_ndtdbg_power_cycle(args: Dict[str, Any]) -> Dict[str, Any]:
383
384
  def _stream_cli_cmd(args: Dict[str, Any], out_path: str, duration_cap: float) -> Optional[list]:
384
385
  """构造 stream CLI 命令 (同步/job 共用); 返回 None 表示参数非法。"""
385
386
  cli_script = os.path.join(REPO_ROOT, "tools", "ndtdbg_cli.py")
386
- regs = args.get("regs") or ["0xEE:8"]
387
387
  duration_s = min(max(float(args.get("duration_s", 10)), 0.0), duration_cap)
388
388
  max_frames = int(args.get("max_frames", 0))
389
389
  if duration_s <= 0 and max_frames <= 0:
390
390
  return None
391
- cmd = [sys.executable, cli_script, "stream", "--regs", *[str(r) for r in regs],
392
- "--out", out_path]
391
+ cmd = [sys.executable, cli_script, "stream", "--out", out_path]
392
+ regs = args.get("regs")
393
+ if regs:
394
+ if isinstance(regs, str):
395
+ regs = [regs]
396
+ cmd.extend(["--regs", *[str(r) for r in regs]])
393
397
  if args.get("interval_ms") is not None:
394
398
  cmd.extend(["--interval-ms", str(int(args["interval_ms"]))])
395
399
  if duration_s > 0:
@@ -566,10 +570,13 @@ def handle_fw_ndtdbg_capture_seq(args: Dict[str, Any]) -> Dict[str, Any]:
566
570
  cli_script = os.path.join(REPO_ROOT, "tools", "ndtdbg_cli.py")
567
571
  cap_script = os.path.join(REPO_ROOT, "tools", "logic_capture.ps1")
568
572
  duration_ms = int(args.get("duration_ms", 8000))
569
- writes = args.get("writes", [{"reg": "0xBE", "data": "01"}, {"reg": "0x01", "data": "BE 42"}])
570
- poll_reg = args.get("poll_reg", "0x01")
573
+ writes = args.get("writes")
574
+ if not writes:
575
+ return {"ok": False, "error_class": "CONFIG_ERROR",
576
+ "error": "fw_ndtdbg_capture_seq 必须传入非空 writes 序列 (如 [{'reg': '0xBE', 'data': '01'}]), 无默认序列; 参见目标固件协议文档"}
577
+ poll_reg = args.get("poll_reg")
571
578
  poll_attempts = int(args.get("poll_attempts", 5))
572
- readback_reg = args.get("readback_reg", "0xBE")
579
+ readback_reg = args.get("readback_reg")
573
580
  poll_expect = " ".join(str(args.get("poll_expect", "00 00")).split()).upper()
574
581
  confirm = bool(args.get("confirm", True))
575
582
 
@@ -901,7 +908,7 @@ def handle_fw_init_project(args: Dict[str, Any]) -> Dict[str, Any]:
901
908
  return {
902
909
  "ok": False,
903
910
  "error_class": "CONFIG_ERROR",
904
- "error": "target_chip 必填 (如 STM32F103C8 / CSA37F71)",
911
+ "error": "target_chip 必填 (如 STM32F103C8 / STM32F407ZG)",
905
912
  "detail": "fwloop init <target_chip> 或 MCP fw_init_project(target_chip=...)",
906
913
  }
907
914
 
@@ -1156,6 +1163,57 @@ custom_tools:
1156
1163
  向用户展示配置位置,并说明后续在对话中可以直接使用自然语言调度:
1157
1164
  - “*使用我们公司的烧录工具把固件刷入芯片*”
1158
1165
  - “*启动自研抓包工具采集 5 秒串口日志*”
1166
+
1167
+ ---
1168
+
1169
+ ## 可选手法:目标侧"只读诊断插桩"(通用模式)
1170
+
1171
+ **适用**:目标固件"挂死 / 反复重启",没有可用串口输出(或写通道不可用),但工具族能读寄存器。
1172
+ 思路:让固件把**运行痕迹暴露成只读寄存器**,Agent 一次读回即可定位阶段与崩溃现场,
1173
+ 不必"改一处 → 烧一次 → 猜一次"。
1174
+
1175
+ > **⚠️ 归属规则(先看这条)**:本 skill 会**随包分发**(`fwloop init` / `fwloop setup` 会把它
1176
+ > 复制到用户工程与各 Agent 的 skills 目录),因此**这里只放与具体项目、具体芯片无关的通用内容**。
1177
+ > 每个项目的**地址约定、寄存器含义、现场案例属于项目资产**,请记录到:
1178
+ > - 该项目仓库内的 `docs/<project>-probe-convention.md`,或该项目自建的 project skill;
1179
+ >
1180
+ > 本 skill 负责教你**怎么设计**这套插桩、以及**记录到哪里**,不承载任何单一项目的约定。
1181
+
1182
+ ### 通用模式(由粗到细四件套)
1183
+
1184
+ 1. **阶段编号**:最后到达的启动边界;
1185
+ 2. **阶段追踪环**:最近 N 步的环形缓冲(看清"路径",而不只是终点);
1186
+ 3. **计数器组**:主循环迭代数、可疑等待循环的自旋计数、崩溃现场(fault 状态字 / PC / LR / xPSR);
1187
+ 4. **运行时可调"停靠级别"**:写后断电即可逐级二分崩溃点。
1188
+
1189
+ ### 通用约束(与芯片无关,都是踩出来的)
1190
+
1191
+ 1. 插桩数组放 **NOINIT** 段:复位/软复位不清零,才看得到"重启前最后执行到哪";
1192
+ 2. 尽量**只读注册**,不要占用可写寄存器语义;
1193
+ 3. 注册表必须按**地址升序**:乱序会导致从机自检失败 / 初始化提前 `return`
1194
+ ⇒ 通信通道根本没起来,现象与"目标掉电"完全一样(极易误判);
1195
+ 4. **避开厂商协议占用的地址**:例——烧录工具会往某个固定地址写擦除/完成魔数,
1196
+ 插桩变量放在那里会污染工具的判据;
1197
+ 5. 采集遵守工具族的协议约束(升序读、单次长度上限、关键字节双读校验——见各工具描述);
1198
+ 6. 插桩代码用 `#if` 开关包裹:关闭时零成本、零 RAM。
1199
+
1200
+ ### 固件侧两条"已知良方"(与芯片无关)
1201
+
1202
+ 1. **"通道先行"**:把"能被工具读到的通道"(I2C 从机 / 串口)初始化提到所有可疑初始化**之前**
1203
+ ⇒ 之后无论哪里崩,设备仍然可读(把它写在最后,往往会被插桩本身锁死);
1204
+ 2. **fault handler 禁止裸自旋**(`B .`):其优先级高于一切可配置中断,死等会饿死通信中断,
1205
+ 目标"从总线上消失"(工具侧表现为 `BUS_TIMEOUT`)⇒ 应"记录现场 + 复位"。
1206
+ (进阶手法"改写异常返回地址 → 恢复函数 → 重建通道 → 停靠"与芯片强相关,换目标需复测。)
1207
+
1208
+ ### 最小闭环(通用)
1209
+
1210
+ ```text
1211
+ fw_ndtdbg_power_cycle(off_ms=500) # 复位(不需 write_allowed)
1212
+ fw_ndtdbg_status # 分级结论 + 判据表
1213
+ fw_ndtdbg_read_reg(reg=..., len=...) # 读"项目约定文档"里定义的诊断寄存器
1214
+ fw_ndtdbg_flash(...) # 改完固件自助烧录(含烧后冷启动冒烟)
1215
+ fw_serial_capture(power_cycle_off_ms=500) # 抓开机日志(窗口内自动复位)
1216
+ ```
1159
1217
  """
1160
1218
  if not os.path.exists(adapter_skill_path) or overwrite:
1161
1219
  with open(adapter_skill_path, "w", encoding="utf-8") as fh:
@@ -1884,8 +1942,7 @@ TOOLS_REGISTRY = {
1884
1942
  "regs": {
1885
1943
  "type": "array",
1886
1944
  "items": {"type": "string"},
1887
- "description": "Register specs 'addr:len[:name]', e.g. ['0xEE:8:ch', '0x20:138:event']. Default ['0xEE:8'].",
1888
- "default": ["0xEE:8"],
1945
+ "description": "Register specs 'addr:len[:name]', e.g. ['0xEE:8:ch'] (EXAMPLE). Required unless configured in lab.yaml ndtdbg.stream.regs or ndtdbg.target_profile.stream_regs.",
1889
1946
  },
1890
1947
  "interval_ms": {
1891
1948
  "type": "integer",
@@ -1909,7 +1966,7 @@ TOOLS_REGISTRY = {
1909
1966
  "decode": {
1910
1967
  "type": "string",
1911
1968
  "enum": ["hex", "int16le", "uint16le", "int8", "uint8"],
1912
- "description": "Optional decoded columns besides raw hex (int16le for PT488 0xEE channels).",
1969
+ "description": "Optional decoded columns besides raw hex (e.g. int16le for 4-channel little-endian sensor frames).",
1913
1970
  "default": "hex",
1914
1971
  },
1915
1972
  "retry": {
@@ -1935,7 +1992,7 @@ TOOLS_REGISTRY = {
1935
1992
  "interleave_writes": {
1936
1993
  "type": "array",
1937
1994
  "items": {"type": "string"},
1938
- "description": "Interleave-write stress profile (P1-1): 'addr:cycle:v1,v2,...:every=N' (rotate values every N frames) or 'addr:once:v:frame=M' (single write at frame M). Written in-frame gaps over the SAME USB session (no dual-open). Requires write_allowed: true AND confirm: true. e.g. ['0x56:cycle:00,01,02,03:every=8'] reproduces the PT488 schedule-race stress profile.",
1995
+ "description": "Interleave-write stress profile (P1-1): 'addr:cycle:v1,v2,...:every=N' (rotate values every N frames) or 'addr:once:v:frame=M' (single write at frame M). Written in-frame gaps over the SAME USB session (no dual-open). Requires write_allowed: true AND confirm: true. e.g. ['0x56:cycle:00,01,02,03:every=8'] reproduces schedule-race style concurrent-write stress profiles (example from a real product debugging session).",
1939
1996
  },
1940
1997
  "confirm": {
1941
1998
  "type": "boolean",
@@ -1945,7 +2002,6 @@ TOOLS_REGISTRY = {
1945
2002
  "dll_dir": {"type": "string", "description": "NdtDebugTool lib dir; default from lab.yaml ndtdbg.dll_dir"},
1946
2003
  "slave_addr": {"type": "string", "description": "IIC slave address, e.g. 0xA0; default from lab.yaml"},
1947
2004
  },
1948
- "required": ["regs"],
1949
2005
  "additionalProperties": False,
1950
2006
  },
1951
2007
  "handler": handle_fw_ndtdbg_stream,
@@ -1958,8 +2014,7 @@ TOOLS_REGISTRY = {
1958
2014
  "regs": {
1959
2015
  "type": "array",
1960
2016
  "items": {"type": "string"},
1961
- "description": "Register specs 'addr:len[:name]'. Default ['0xEE:8'].",
1962
- "default": ["0xEE:8"],
2017
+ "description": "Register specs 'addr:len[:name]', e.g. ['0xEE:8:ch'] (EXAMPLE). Required unless configured in lab.yaml ndtdbg.stream.regs or ndtdbg.target_profile.stream_regs.",
1963
2018
  },
1964
2019
  "interval_ms": {"type": "integer", "description": "Target sampling interval in ms.", "default": 100},
1965
2020
  "duration_s": {"type": "number", "description": "Total capture duration in seconds (cap 3600).", "default": 10},
@@ -2004,7 +2059,7 @@ TOOLS_REGISTRY = {
2004
2059
  "handler": handle_fw_ndtdbg_stream_stop,
2005
2060
  },
2006
2061
  "fw_ndtdbg_capture_seq": {
2007
- "description": "One-call protocol evidence: concurrently runs a NdtBox register write sequence and a sigrok I2C capture, returning the capture path for fw_logic_decode. NOTE: the default sequence (0xBE=01, 0x01=BE 42, poll 0x01, readback 0xBE) is an EXAMPLE only - always pass your target firmware's real register protocol via writes/poll_reg/poll_expect. Writes are double-gated (confirm: true + lab.yaml ndtdbg.write_allowed); degrades to sequence-only execution (capture_skipped) when sigrok-cli is missing.",
2062
+ "description": "One-call protocol evidence: concurrently runs a target-specific register write sequence and a sigrok I2C capture, returning the capture path for fw_logic_decode. REQUIRED: pass your target firmware's real register protocol via writes (e.g. [{'reg': '0xBE', 'data': '01'}]). Optional poll_reg/poll_expect/readback_reg for handshake & verification. Writes are double-gated (confirm: true + lab.yaml ndtdbg.write_allowed); degrades to sequence-only execution (capture_skipped) when sigrok-cli is missing.",
2008
2063
  "inputSchema": {
2009
2064
  "type": "object",
2010
2065
  "properties": {
@@ -2022,20 +2077,21 @@ TOOLS_REGISTRY = {
2022
2077
  "data": {"type": "string"},
2023
2078
  },
2024
2079
  },
2025
- "description": "Register write sequence per your firmware protocol (default 0xBE=01, 0x01=BE 42 is an EXAMPLE, may not match real firmware)",
2080
+ "description": "Target-specific register write sequence (e.g. [{'reg': '0xBE', 'data': '01'}]). Required, no hardcoded default.",
2026
2081
  },
2027
2082
  "confirm": {
2028
2083
  "type": "boolean",
2029
2084
  "description": "Acknowledge the register write intent; handler auto-passes --confirm to the CLI (double-gated with lab.yaml ndtdbg.write_allowed)",
2030
2085
  "default": True,
2031
2086
  },
2032
- "poll_reg": {"type": "string", "description": "Poll register until it matches poll_expect (default 0x01)"},
2087
+ "poll_reg": {"type": "string", "description": "Optional poll register until it matches poll_expect (no default; skipped if omitted)"},
2033
2088
  "poll_expect": {"type": "string", "description": "Expected poll register value (hex) that ends the poll loop; default '00 00' - set to match your firmware protocol", "default": "00 00"},
2034
2089
  "poll_attempts": {"type": "integer", "description": "Max poll attempts", "default": 5},
2035
- "readback_reg": {"type": "string", "description": "Read-back verification register (default 0xBE)"},
2090
+ "readback_reg": {"type": "string", "description": "Optional read-back verification register (no default; skipped if omitted)"},
2036
2091
  "dll_dir": {"type": "string", "description": "NdtDebugTool lib directory; default from lab.yaml"},
2037
2092
  "slave_addr": {"type": "string", "description": "IIC slave address (e.g. 0xA0)"},
2038
2093
  },
2094
+ "required": ["writes"],
2039
2095
  "additionalProperties": False,
2040
2096
  },
2041
2097
  "handler": handle_fw_ndtdbg_capture_seq,
@@ -2122,7 +2178,7 @@ TOOLS_REGISTRY = {
2122
2178
  },
2123
2179
  "target_chip": {
2124
2180
  "type": "string",
2125
- "description": "Target MCU chip model (e.g. STM32F103C8, CSA37F71) - REQUIRED, no hardcoded default",
2181
+ "description": "Target MCU chip model (e.g. STM32F103C8, STM32F407ZG) - REQUIRED, no hardcoded default",
2126
2182
  },
2127
2183
  "build_backend": {
2128
2184
  "type": "string",
@@ -2335,7 +2391,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
2335
2391
  },
2336
2392
  "serverInfo": {
2337
2393
  "name": "firmwareloop",
2338
- "version": "0.7.4"
2394
+ "version": "0.9.0"
2339
2395
  }
2340
2396
  }
2341
2397
  }
@@ -2470,7 +2526,7 @@ def _settle_stashed_entrypoints(stashed) -> None:
2470
2526
  def handle_cli_update() -> int:
2471
2527
  """Handle `firmwareloop update` command."""
2472
2528
  print("============================================================")
2473
- print(" FirmwareLoop Auto-Updater (v0.7.4)")
2529
+ print(" FirmwareLoop Auto-Updater (v0.9.0)")
2474
2530
  print("============================================================")
2475
2531
 
2476
2532
  is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
@@ -2607,6 +2663,18 @@ def handle_cli_selftest(target: str = "ndtdbg") -> int:
2607
2663
  def report(name, status, detail=""):
2608
2664
  results.append((name, status, detail))
2609
2665
 
2666
+ # P2-2: 从生效配置加载产品协议 profile (避免硬编码 0xEE 与 0xBE)
2667
+ try:
2668
+ from tools import ndtdbg_cli
2669
+ _cfg = ndtdbg_cli.load_config(None)
2670
+ except Exception:
2671
+ _cfg = {}
2672
+ ndt_cfg = (_cfg.get("ndtdbg") or {}) if isinstance(_cfg, dict) else {}
2673
+ target_profile = ndt_cfg.get("target_profile") or {}
2674
+ stream_cfg = ndt_cfg.get("stream") or {}
2675
+ stream_regs = target_profile.get("stream_regs") or stream_cfg.get("regs")
2676
+ loop_cfg = target_profile.get("write_loop") or target_profile.get("selftest_sequence")
2677
+
2610
2678
  # 1. status 分层探测
2611
2679
  print("\n[1/4] status 探测 (Box / IIC bus / target chip) ...")
2612
2680
  rc, body = run_cli("status", timeout=30)
@@ -2623,40 +2691,58 @@ def handle_cli_selftest(target: str = "ndtdbg") -> int:
2623
2691
  report("protocol.desync", _st, _detail)
2624
2692
 
2625
2693
  # 2. stream 冒烟 (100ms x 10s)
2626
- print("[2/4] stream 冒烟 (100ms x 10s, int16 解码) ...")
2627
- rc, body = run_cli("stream", "--regs", "0xEE:8:ch", "--interval-ms", "100",
2628
- "--duration-s", "10", "--decode", "int16le", timeout=60)
2629
- stream_data = (body or {}).get("data") if isinstance(body, dict) else None
2630
- if rc == 0 and stream_data:
2631
- total = stream_data.get("frames_total", 0)
2632
- conserved = (stream_data.get("frames_ok", 0) + stream_data.get("frames_ff_gap", 0)
2633
- + stream_data.get("frames_io_fail", 0)) == total
2634
- avg = stream_data.get("interval_actual_avg_ms")
2635
- report("stream.frames_conserved", "PASS" if conserved else "FAIL",
2636
- f"total={total} (ok={stream_data.get('frames_ok')}, "
2637
- f"ff_gap={stream_data.get('frames_ff_gap')}, io_fail={stream_data.get('frames_io_fail')})")
2638
- report("stream.interval_perf", "PASS" if (avg is not None and avg <= 110) else "FAIL",
2639
- f"avg={avg}ms (target 100ms, 验收线 <=110ms)")
2640
- report("stream.io_overhead", "PASS" if stream_data.get("io_overhead_avg_ms") is not None else "FAIL",
2641
- f"io_overhead_avg_ms={stream_data.get('io_overhead_avg_ms')}")
2642
- report("stream.gaps", "PASS", f"gaps_n={stream_data.get('gaps_n', 0)} (仅 >2x target 计数)")
2643
- else:
2644
- report("stream", "FAIL", (body or {}).get("error_class", f"exit {rc}"))
2645
-
2646
- # 3. 写读回环 (write_allowed 双门内; 未授权 SKIP, 不算 FAIL)
2647
- print("[3/4] 写读回环 (0xBE debug_mode, lab.yaml write_allowed) ...")
2648
- rc, body = run_cli("write-reg", "--addr", "0xBE", "--data", "01", "--confirm")
2649
- if rc == 0 and body and body.get("ok"):
2650
- auth = (body.get("data") or {}).get("authorization") or {}
2651
- report("write.readback", "PASS", f"0xBE=01 写入成功 (授权来源: {auth.get('granted_by', '?')})")
2652
- run_cli("write-reg", "--addr", "0xBE", "--data", "00", "--confirm")
2653
- report("write.restore", "PASS", "0xBE=00 已恢复")
2694
+ if stream_regs:
2695
+ if isinstance(stream_regs, str):
2696
+ stream_regs = [stream_regs]
2697
+ stream_cmd = ["stream", "--interval-ms", "100", "--duration-s", "10",
2698
+ "--regs", *[str(r) for r in stream_regs]]
2699
+ frame_cfg = target_profile.get("measure_frame") or {}
2700
+ dec = frame_cfg.get("decode")
2701
+ if dec and dec != "raw":
2702
+ stream_cmd.extend(["--decode", dec])
2703
+ print(f"[2/4] stream 冒烟 (100ms x 10s, regs={stream_regs}) ...")
2704
+ rc, body = run_cli(*stream_cmd, timeout=60)
2705
+ stream_data = (body or {}).get("data") if isinstance(body, dict) else None
2706
+ if rc == 0 and stream_data:
2707
+ total = stream_data.get("frames_total", 0)
2708
+ conserved = (stream_data.get("frames_ok", 0) + stream_data.get("frames_ff_gap", 0)
2709
+ + stream_data.get("frames_io_fail", 0)) == total
2710
+ avg = stream_data.get("interval_actual_avg_ms")
2711
+ report("stream.frames_conserved", "PASS" if conserved else "FAIL",
2712
+ f"total={total} (ok={stream_data.get('frames_ok')}, "
2713
+ f"ff_gap={stream_data.get('frames_ff_gap')}, io_fail={stream_data.get('frames_io_fail')})")
2714
+ report("stream.interval_perf", "PASS" if (avg is not None and avg <= 110) else "FAIL",
2715
+ f"avg={avg}ms (target 100ms, 验收线 <=110ms)")
2716
+ report("stream.io_overhead", "PASS" if stream_data.get("io_overhead_avg_ms") is not None else "FAIL",
2717
+ f"io_overhead_avg_ms={stream_data.get('io_overhead_avg_ms')}")
2718
+ report("stream.gaps", "PASS", f"gaps_n={stream_data.get('gaps_n', 0)} (仅 >2x target 计数)")
2719
+ else:
2720
+ report("stream", "FAIL", (body or {}).get("error_class", f"exit {rc}"))
2654
2721
  else:
2655
- err_class = (body or {}).get("error_class", f"exit {rc}")
2656
- if err_class == "PERMISSION_DENIED":
2657
- report("write.readback", "SKIP", "write_allowed 未开启 (双门保护), 跳过写回环")
2722
+ print("[2/4] stream 冒烟 (未配置 target_profile.stream_regs, 跳过) ...")
2723
+ report("stream", "SKIP", "未配置产品协议 profile (target_profile.stream_regs),寄存器级自验跳过")
2724
+
2725
+ # 3. 写读回环 (write_allowed 双门内; 未授权或未配 profile 时 SKIP, 不算 FAIL)
2726
+ if loop_cfg:
2727
+ loop_addr = str(loop_cfg.get("addr", "0xBE"))
2728
+ w_data = str(loop_cfg.get("write_data", "01"))
2729
+ r_data = str(loop_cfg.get("restore_data", "00"))
2730
+ print(f"[3/4] 写读回环 ({loop_addr}={w_data}->{r_data}, 需 lab.yaml write_allowed) ...")
2731
+ rc, body = run_cli("write-reg", "--addr", loop_addr, "--data", w_data, "--confirm")
2732
+ if rc == 0 and body and body.get("ok"):
2733
+ auth = (body.get("data") or {}).get("authorization") or {}
2734
+ report("write.readback", "PASS", f"{loop_addr}={w_data} 写入成功 (授权来源: {auth.get('granted_by', '?')})")
2735
+ run_cli("write-reg", "--addr", loop_addr, "--data", r_data, "--confirm")
2736
+ report("write.restore", "PASS", f"{loop_addr}={r_data} 已恢复")
2658
2737
  else:
2659
- report("write.readback", "FAIL", err_class)
2738
+ err_class = (body or {}).get("error_class", f"exit {rc}")
2739
+ if err_class == "PERMISSION_DENIED":
2740
+ report("write.readback", "SKIP", "write_allowed 未开启 (双门保护), 跳过写回环")
2741
+ else:
2742
+ report("write.readback", "FAIL", err_class)
2743
+ else:
2744
+ print("[3/4] 写读回环 (未配置 target_profile.write_loop, 跳过) ...")
2745
+ report("write.readback", "SKIP", "未配置产品协议 profile (target_profile.write_loop),写回环自验跳过")
2660
2746
 
2661
2747
  # 3.5 write_allowed 卫生检查 (P1-2): 任何层级为 true 即 WARNING (会话级临时态)
2662
2748
  print("[3.5/4] write_allowed 卫生检查 ...")
@@ -2813,6 +2899,192 @@ def configure_codebuddy_user_mcp(user_home: str) -> Dict[str, Any]:
2813
2899
  return {"ok": False, "path": mcp_file, "reason": str(exc)}
2814
2900
 
2815
2901
 
2902
+ DSH_MCP_CLIENT_PLUGIN = "@deepseek-ai/dsh-mcp-client"
2903
+
2904
+ # DSH (DeepSeek Harness) profile patch entries. Tools surface as
2905
+ # mcp__<serverName>__<tool> (same shape as Claude Code / Codex), e.g.
2906
+ # mcp__fwloop__fw_build. Bare command names resolve via PATH: the DSH
2907
+ # stdio transport only scrubs credential-shaped / DSH_* env vars, so the
2908
+ # parent PATH survives (same convention as the Qoder / Claude entries).
2909
+ DSH_MCP_SERVER_ENTRIES = (
2910
+ ("mcp-fwloop", "fwloop", {"command": "fwloop", "args": []}),
2911
+ ("mcp-agentic-hil", "agentic-hil", {"command": "agentic-hil", "args": ["mcp-stdio"]}),
2912
+ )
2913
+
2914
+ DSH_PATCH_HEADER = (
2915
+ "# FirmwareLoop dual-tier MCP (added by fwloop setup).\n"
2916
+ "# Bridged by @deepseek-ai/dsh-mcp-client; tools surface as mcp__<serverName>__<tool>.\n"
2917
+ )
2918
+
2919
+
2920
+ def resolve_dsh_home(user_home: str) -> str:
2921
+ """Resolve the DeepSeek Harness home: non-blank $DSH_HOME wins, else ~/.dsh.
2922
+
2923
+ Mirrors @deepseek-ai/dsh-home-paths precedence (an in-harness configured
2924
+ home override is invisible to external tools and therefore not honored).
2925
+ """
2926
+ env_home = (os.environ.get("DSH_HOME") or "").strip()
2927
+ return env_home if env_home else os.path.join(user_home, ".dsh")
2928
+
2929
+
2930
+ def discover_dsh_profiles(dsh_home: str) -> List[str]:
2931
+ """List derived DSH profile names under <home>/profiles.
2932
+
2933
+ A directory counts as a profile only when it has a profile-root cordis.yml,
2934
+ which excludes the shared <home>/profiles/node_modules dependency tree.
2935
+ Built-in profiles (web/tui/headless) appear here once the user has booted
2936
+ or derived them; never-derived ones do not exist on disk and are skipped.
2937
+ """
2938
+ profiles_dir = os.path.join(dsh_home, "profiles")
2939
+ found: List[str] = []
2940
+ if os.path.isdir(profiles_dir):
2941
+ for name in sorted(os.listdir(profiles_dir)):
2942
+ cand = os.path.join(profiles_dir, name)
2943
+ if os.path.isdir(cand) and os.path.isfile(os.path.join(cand, "cordis.yml")):
2944
+ found.append(name)
2945
+ return found
2946
+
2947
+
2948
+ def _dsh_entry_yaml(entry_id: str, server_name: str, spec: Dict[str, Any]) -> str:
2949
+ args_repr = ", ".join(f"'{a}'" for a in spec.get("args", []))
2950
+ return (
2951
+ f" - id: {entry_id}\n"
2952
+ f" name: '{DSH_MCP_CLIENT_PLUGIN}'\n"
2953
+ f" config:\n"
2954
+ f" serverName: {server_name}\n"
2955
+ f" transport: stdio\n"
2956
+ f" command: {spec['command']}\n"
2957
+ f" args: [{args_repr}]\n"
2958
+ )
2959
+
2960
+
2961
+ def upsert_dsh_patch_file(patch_path: str) -> str:
2962
+ """Idempotently add the FirmwareLoop MCP entries to a profile's cordis.patch.yml.
2963
+
2964
+ Returns "written" or "already_configured". Text-level upsert on purpose:
2965
+ a yaml.safe_dump round-trip would strip the user's guide comments, so an
2966
+ existing file is only ever appended to. Membership is decided by a plain
2967
+ substring check on our fixed ids (fail-safe side: a false positive merely
2968
+ skips the write), and entries that already exist are never rewritten so
2969
+ user overrides (e.g. a custom command) survive setup re-runs.
2970
+ """
2971
+ present = ""
2972
+ if os.path.exists(patch_path):
2973
+ with open(patch_path, "r", encoding="utf-8-sig") as f:
2974
+ present = f.read()
2975
+ missing = [eid for eid, _, _ in DSH_MCP_SERVER_ENTRIES if f"id: {eid}" not in present]
2976
+ if present and not missing:
2977
+ return "already_configured"
2978
+ # Single "- insert:" block holding the missing entries, matching the shape
2979
+ # of the official dsh-mcp-client sample (one block, several entries).
2980
+ new_text = "- insert:\n" + "".join(
2981
+ _dsh_entry_yaml(eid, name, spec)
2982
+ for eid, name, spec in DSH_MCP_SERVER_ENTRIES
2983
+ if eid in missing
2984
+ )
2985
+ parent = os.path.dirname(patch_path)
2986
+ if parent:
2987
+ os.makedirs(parent, exist_ok=True)
2988
+ if not present:
2989
+ with open(patch_path, "w", encoding="utf-8", newline="\n") as f:
2990
+ f.write(DSH_PATCH_HEADER)
2991
+ f.write(new_text)
2992
+ return "written"
2993
+ stripped = present.strip()
2994
+ with open(patch_path, "w", encoding="utf-8", newline="\n") as f:
2995
+ if not stripped or stripped == "[]":
2996
+ # Empty document (blank or literal []): keep leading comment lines,
2997
+ # then start the entries array.
2998
+ comments = "\n".join(
2999
+ line for line in present.splitlines() if line.strip().startswith("#")
3000
+ )
3001
+ if comments:
3002
+ f.write(comments + "\n")
3003
+ f.write(new_text)
3004
+ else:
3005
+ f.write(present)
3006
+ if not present.endswith("\n"):
3007
+ f.write("\n")
3008
+ f.write("\n" + new_text)
3009
+ return "written"
3010
+
3011
+
3012
+ def configure_dsh_profiles(user_home: str) -> List[Dict[str, Any]]:
3013
+ """Add FirmwareLoop MCP entries to every derived DSH profile's cordis.patch.yml."""
3014
+ dsh_home = resolve_dsh_home(user_home)
3015
+ results: List[Dict[str, Any]] = []
3016
+ for profile in discover_dsh_profiles(dsh_home):
3017
+ patch_path = os.path.join(dsh_home, "profiles", profile, "cordis.patch.yml")
3018
+ try:
3019
+ status = upsert_dsh_patch_file(patch_path)
3020
+ results.append({"profile": profile, "ok": True, "path": patch_path, "status": status})
3021
+ except Exception as exc:
3022
+ results.append({"profile": profile, "ok": False, "path": patch_path, "reason": str(exc)})
3023
+ return results
3024
+
3025
+
3026
+ ZCODE_MCP_SERVERS = {
3027
+ "fwloop": {"command": "fwloop", "type": "stdio"},
3028
+ "agentic-hil": {"command": "agentic-hil", "args": ["mcp-stdio"], "type": "stdio"},
3029
+ }
3030
+
3031
+
3032
+ def upsert_zcode_mcp_json(mcp_path: str, servers: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
3033
+ """Merge MCP servers into ZCode's ~/.zcode/cli/config.json without dropping
3034
+ existing entries.
3035
+
3036
+ ZCode (GLM desktop IDE) nests its MCP registry under "mcp"."servers" —
3037
+ the per-entry shape (type/command/args) matches Qoder's mcpServers, so the
3038
+ same per-entry merge (keep user extras like env) applies.
3039
+ """
3040
+ payload = dict(servers or ZCODE_MCP_SERVERS)
3041
+ data: Dict[str, Any] = {}
3042
+ if os.path.exists(mcp_path):
3043
+ try:
3044
+ with open(mcp_path, "r", encoding="utf-8-sig") as f:
3045
+ loaded = json.load(f)
3046
+ if isinstance(loaded, dict):
3047
+ data = loaded
3048
+ except Exception:
3049
+ data = {}
3050
+ mcp_root = data.get("mcp")
3051
+ if not isinstance(mcp_root, dict):
3052
+ mcp_root = {}
3053
+ mcp_servers = mcp_root.get("servers")
3054
+ if not isinstance(mcp_servers, dict):
3055
+ mcp_servers = {}
3056
+ for name, entry in payload.items():
3057
+ old = mcp_servers.get(name)
3058
+ if isinstance(old, dict) and isinstance(entry, dict):
3059
+ merged = dict(old)
3060
+ merged.update(entry)
3061
+ mcp_servers[name] = merged
3062
+ else:
3063
+ mcp_servers[name] = entry
3064
+ mcp_root["servers"] = mcp_servers
3065
+ data["mcp"] = mcp_root
3066
+ parent = os.path.dirname(mcp_path)
3067
+ if parent:
3068
+ os.makedirs(parent, exist_ok=True)
3069
+ with open(mcp_path, "w", encoding="utf-8") as f:
3070
+ json.dump(data, f, indent=2)
3071
+ f.write("\n")
3072
+ return data
3073
+
3074
+
3075
+ def configure_zcode_user_mcp(user_home: str) -> Dict[str, Any]:
3076
+ """Write FirmwareLoop MCP entries into ZCode's ~/.zcode/cli/config.json."""
3077
+ zcode_dir = os.path.join(user_home, ".zcode")
3078
+ mcp_file = os.path.join(zcode_dir, "cli", "config.json")
3079
+ if not os.path.isdir(zcode_dir):
3080
+ return {"ok": False, "path": mcp_file, "reason": "not_installed"}
3081
+ try:
3082
+ upsert_zcode_mcp_json(mcp_file)
3083
+ return {"ok": True, "path": mcp_file}
3084
+ except Exception as exc:
3085
+ return {"ok": False, "path": mcp_file, "reason": str(exc)}
3086
+
3087
+
2816
3088
  def resolve_ndtdebugtool_lib(tool_root: str) -> str:
2817
3089
  """Resolve the NdtDebugTool lib directory (must contain USBIOX.DLL)."""
2818
3090
  if not tool_root or not os.path.isdir(tool_root):
@@ -2899,9 +3171,9 @@ def set_ndtdbg_dlldir(dll_path: str) -> str:
2899
3171
 
2900
3172
 
2901
3173
  def handle_cli_setup() -> int:
2902
- """Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, and CodeBuddy IDE."""
3174
+ """Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, CodeBuddy IDE, DeepSeek Harness (DSH), and ZCode IDE."""
2903
3175
  print("============================================================")
2904
- print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.7.4)")
3176
+ print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.9.0)")
2905
3177
  print("============================================================")
2906
3178
  print("")
2907
3179
 
@@ -2985,7 +3257,37 @@ def handle_cli_setup() -> int:
2985
3257
  else:
2986
3258
  print(f" - User MCP: [Warning] Could not write {cb_result['path']}: {cb_result.get('reason')}")
2987
3259
 
2988
- # 5. Global Skills Sync for Antigravity and Claude Code
3260
+ # 5. DeepSeek Harness (DSH) per-profile cordis.patch.yml upsert
3261
+ # Only profiles already derived under <home>/profiles (a booted or
3262
+ # --from-default-profile'd profile has a profile-root cordis.yml) are
3263
+ # configured; never-derived built-ins do not exist on disk and are skipped.
3264
+ print("\n[5] DeepSeek Harness (DSH):")
3265
+ dsh_results = configure_dsh_profiles(user_home)
3266
+ dsh_ok = [item for item in dsh_results if item.get("ok")]
3267
+ if dsh_ok:
3268
+ for item in dsh_ok:
3269
+ state = "already configured" if item["status"] == "already_configured" else "written"
3270
+ print(f" - profile {item['profile']}: [OK] {state}: {item['path']}")
3271
+ configured_agents.append("DeepSeek Harness (DSH)")
3272
+ else:
3273
+ print(" - ~/.dsh/profiles has no derived profile (DSH not installed, or no profile booted yet)")
3274
+ print(" - Boot/derive a profile in DSH first, then re-run: fwloop setup")
3275
+ for item in dsh_results:
3276
+ if not item.get("ok"):
3277
+ print(f" - profile {item['profile']}: [Warning] Could not write {item['path']}: {item.get('reason')}")
3278
+
3279
+ # 6. ZCode IDE user-level MCP (~/.zcode/cli/config.json, {"mcp": {"servers": ...}})
3280
+ print("\n[6] ZCode IDE:")
3281
+ zc_result = configure_zcode_user_mcp(user_home)
3282
+ if zc_result.get("ok"):
3283
+ print(f" - User MCP: [OK] Written to {zc_result['path']}")
3284
+ configured_agents.append("ZCode")
3285
+ elif zc_result.get("reason") == "not_installed":
3286
+ print(" - User MCP: ~/.zcode not found (Workspace .mcp.json still supported)")
3287
+ else:
3288
+ print(f" - User MCP: [Warning] Could not write {zc_result['path']}: {zc_result.get('reason')}")
3289
+
3290
+ # 7. Global Skills Sync for Antigravity and Claude Code
2989
3291
  ag_skills_dir = os.path.join(user_home, ".gemini", "antigravity-cli", "skills")
2990
3292
  claude_skills_dir = os.path.join(user_home, ".claude", "skills")
2991
3293
  source_skills_dir = os.path.join(REPO_ROOT, "skills")
@@ -3006,14 +3308,14 @@ def handle_cli_setup() -> int:
3006
3308
  except Exception:
3007
3309
  pass
3008
3310
 
3009
- print(f"\n[5] Global Skills:")
3311
+ print(f"\n[7] Global Skills:")
3010
3312
  print(f" - Synchronized: [OK] {skills_synced} skills (firmwareloop, fwloop-adapter) to Antigravity & Claude Code")
3011
3313
 
3012
- # 5. Machine-level global lab.yaml (ndtdbg config for the globally installed tool)
3314
+ # 8. Machine-level global lab.yaml (ndtdbg config for the globally installed tool)
3013
3315
  global_cfg = ensure_global_lab_yaml()
3014
3316
  if global_cfg:
3015
3317
  exists = " [已存在, 未覆盖]" if os.path.isfile(global_cfg) else " [已创建]"
3016
- print("\n[6] NdtBox 全局配置:")
3318
+ print("\n[8] NdtBox 全局配置:")
3017
3319
  print(f" - {global_cfg}{exists}")
3018
3320
  print(" - 若未配置, 请运行: fwloop ndtdebugtool \"<NdtDebugTool 根目录>\"")
3019
3321
 
@@ -3026,7 +3328,7 @@ def handle_cli_setup() -> int:
3026
3328
  def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
3027
3329
  """Handle `firmwareloop init [target_chip]` command."""
3028
3330
  print("============================================================")
3029
- print(" FirmwareLoop Multi-Agent Project Initializer (v0.7.4)")
3331
+ print(" FirmwareLoop Multi-Agent Project Initializer (v0.9.0)")
3030
3332
  print("============================================================")
3031
3333
  cwd = os.getcwd()
3032
3334
  print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
@@ -3059,7 +3361,7 @@ def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
3059
3361
 
3060
3362
 
3061
3363
  def print_cli_help() -> None:
3062
- print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.7.4)
3364
+ print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.9.0)
3063
3365
 
3064
3366
  Usage:
3065
3367
  fwloop [command] (or: firmwareloop [command])
@@ -3105,7 +3407,7 @@ def main() -> None:
3105
3407
  sys.exit(2)
3106
3408
  sys.exit(handle_cli_ndtdebugtool(args[1]))
3107
3409
  elif cmd in ["version", "-v", "--version"]:
3108
- print("FirmwareLoop v0.7.4")
3410
+ print("FirmwareLoop v0.9.0")
3109
3411
  sys.exit(0)
3110
3412
  elif cmd in ["help", "-h", "--help"]:
3111
3413
  print_cli_help()
@@ -64,18 +64,53 @@ def build_frame(slave_addr: int, payload: bytes) -> bytes:
64
64
  return bytes([slave_addr & 0xFF]) + payload
65
65
 
66
66
 
67
- def parse_getdata(data: bytes) -> list[int] | None:
68
- """解析 0xEE 数据帧: 末 2 字节负和校验, 返回 LE int16 通道数组 (C# (short) 有符号); 校验失败返回 None。"""
69
- if len(data) < 4 or len(data) % 2 != 0:
70
- return None
71
- expected = int.from_bytes(data[-2:], "little")
72
- if expected != checksum_two_bytes(data[:-2]):
67
+ def parse_getdata(data: bytes, profile: dict | None = None) -> list[int] | None:
68
+ """ profile 参数化解析数据帧 (P1-1):
69
+
70
+ profile:
71
+ checksum: "negated-sum-le16" (默认) | "none"
72
+ decode: "int16le" (默认) | "uint16le" | "int8" | "uint8" | "raw"
73
+ min_len: 最短字节数 (默认: negated-sum-le16 为 4, 其余为 1)
74
+ 校验失败、长度不足或格式不合法返回 None。
75
+ """
76
+ cfg = profile or {}
77
+ checksum_algo = cfg.get("checksum", "negated-sum-le16")
78
+ decode_mode = cfg.get("decode", "int16le")
79
+ min_len = int(cfg.get("min_len", 4 if checksum_algo == "negated-sum-le16" else 1))
80
+
81
+ if checksum_algo not in ("negated-sum-le16", "none", None):
82
+ raise NdtBoxError("CONFIG_ERROR", f"不支持的 measure_frame 校验算法: {checksum_algo}")
83
+ if decode_mode not in ("int16le", "uint16le", "int8", "uint8", "raw"):
84
+ raise NdtBoxError("CONFIG_ERROR", f"不支持的 measure_frame 解码模式: {decode_mode}")
85
+
86
+ if len(data) < min_len:
73
87
  return None
74
- channels = []
75
- for i in range(len(data[:-2]) // 2):
76
- raw = int.from_bytes(data[i * 2:i * 2 + 2], "little")
77
- channels.append(raw - 0x10000 if raw >= 0x8000 else raw)
78
- return channels
88
+
89
+ if checksum_algo == "negated-sum-le16":
90
+ if len(data) < 2 or len(data) % 2 != 0:
91
+ return None
92
+ payload = data[:-2]
93
+ expected = int.from_bytes(data[-2:], "little")
94
+ if expected != checksum_two_bytes(payload):
95
+ return None
96
+ else:
97
+ payload = data
98
+
99
+ if decode_mode == "int16le":
100
+ channels = []
101
+ for i in range(0, len(payload) - len(payload) % 2, 2):
102
+ raw = int.from_bytes(payload[i:i + 2], "little")
103
+ channels.append(raw - 0x10000 if raw >= 0x8000 else raw)
104
+ return channels
105
+ elif decode_mode == "uint16le":
106
+ return [int.from_bytes(payload[i:i + 2], "little")
107
+ for i in range(0, len(payload) - len(payload) % 2, 2)]
108
+ elif decode_mode == "int8":
109
+ return [b - 256 if b >= 0x80 else b for b in payload]
110
+ elif decode_mode in ("uint8", "raw"):
111
+ return list(payload)
112
+ else:
113
+ raise NdtBoxError("CONFIG_ERROR", f"不支持的 measure_frame 解码模式: {decode_mode}")
79
114
 
80
115
 
81
116
  def parse_version(data: bytes) -> str:
@@ -1364,15 +1399,23 @@ class ChipSeaFxx:
1364
1399
 
1365
1400
  # -- 调试数据流 (对齐 ReadModuleConnectDynamic + GetData) --
1366
1401
 
1367
- def poll(self, duration_ms: int, interval_ms: int = 10, ready_timeout_ms: int = 2000) -> dict:
1402
+ def poll(self, duration_ms: int, interval_ms: int = 10, ready_timeout_ms: int = 2000,
1403
+ frame_profile: dict | None = None) -> dict:
1368
1404
  """连续轮询一帧或多帧, 返回 {frame_count, checksum_errors, ready_timeouts, frames}。
1369
1405
 
1370
- frames: [{"t": epoch_ms, "ch": [int16...]}, ...]——0.4.0 Python 驱动: serve
1406
+ frames: [{"t": epoch_ms, "ch": [...]}, ...]——0.4.0 Python 驱动: serve
1371
1407
  会话长驻, 序列对齐 ChipSea.cs ReadModuleConnectDynamic + GetData
1372
1408
  (SetDebugMode(0) -> ClearDataReady -> SetDebugMode(0xA4) -> 读 ready -> 读数据)。
1409
+ P1-1: 数据寄存器与帧解析由 frame_profile (lab.yaml target_profile.measure_frame) 参数化。
1373
1410
  """
1374
1411
  if duration_ms <= 0:
1375
1412
  raise NdtBoxError("CONFIG_ERROR", "poll 时长必须 > 0", str(duration_ms))
1413
+ cfg = frame_profile or {}
1414
+ data_reg = int(str(cfg.get("data_reg", 0xEE)), 0) if "data_reg" in cfg else 0xEE
1415
+ checksum_algo = cfg.get("checksum", "negated-sum-le16")
1416
+ decode_mode = cfg.get("decode", "int16le")
1417
+ needs_even = (checksum_algo == "negated-sum-le16" or decode_mode in ("int16le", "uint16le"))
1418
+
1376
1419
  import time
1377
1420
  frames: list[dict] = []
1378
1421
  checksum_errors = 0
@@ -1397,26 +1440,17 @@ class ChipSeaFxx:
1397
1440
  time.sleep(interval_ms / 1000.0)
1398
1441
  continue
1399
1442
  length = ready
1400
- if length % 2 != 0:
1443
+ if needs_even and length % 2 != 0:
1401
1444
  checksum_errors += 1
1402
1445
  time.sleep(interval_ms / 1000.0)
1403
1446
  continue
1404
- rb = self._io.read_reg(0xEE, length)
1405
- if length >= 4:
1406
- expected = int.from_bytes(rb[-2:], "little")
1407
- if expected != checksum_two_bytes(rb[:-2]):
1408
- checksum_errors += 1
1409
- else:
1410
- samples = []
1411
- for i in range(length // 2 - 1):
1412
- v = int.from_bytes(rb[i * 2:i * 2 + 2], "little")
1413
- if v > 32767:
1414
- v -= 65536 # C# (short) 有符号语义
1415
- samples.append(v)
1416
- if samples:
1417
- frames.append({"t": time.time() * 1000, "ch": samples})
1418
- else:
1447
+ rb = self._io.read_reg(data_reg, length)
1448
+ samples = parse_getdata(rb, cfg)
1449
+ if samples is None:
1419
1450
  checksum_errors += 1
1451
+ else:
1452
+ if samples:
1453
+ frames.append({"t": time.time() * 1000, "ch": samples})
1420
1454
  time.sleep(interval_ms / 1000.0)
1421
1455
  return {"frame_count": len(frames), "checksum_errors": checksum_errors,
1422
1456
  "ready_timeouts": dead_frames, "interval_ms": interval_ms,
@@ -350,10 +350,19 @@ def cmd_write_reg(io: NdtBoxIo, config: dict, reg: int, data_hex: str, confirm:
350
350
  }, log=f"WriteReg(0x{reg:02X})")
351
351
 
352
352
 
353
- def cmd_poll(io: NdtBoxIo, duration_ms: int, interval_ms: int, max_frames: int, full: bool) -> None:
353
+ def cmd_poll(io: NdtBoxIo, config: dict, duration_ms: int, interval_ms: int, max_frames: int, full: bool) -> None:
354
354
  if duration_ms <= 0:
355
355
  fail("CONFIG_ERROR", "poll 时长必须 > 0", str(duration_ms))
356
- result = ChipSeaFxx(io).poll(duration_ms, interval_ms=interval_ms)
356
+ ndtdbg_cfg = config.get("ndtdbg") or {}
357
+ target_profile = ndtdbg_cfg.get("target_profile") or {}
358
+ frame_profile = target_profile.get("measure_frame") or ndtdbg_cfg.get("measure_frame")
359
+ if not frame_profile:
360
+ fail("CONFIG_ERROR",
361
+ "measure/poll 缺失目标产品帧格式配置 (target_profile.measure_frame)",
362
+ detail=(f"生效配置: {effective_config_desc()}; "
363
+ "请在 lab.yaml ndtdbg.target_profile.measure_frame 配置帧格式 "
364
+ "(如 data_reg: '0xEE', checksum: 'negated-sum-le16', decode: 'int16le', min_len: 4)"))
365
+ result = ChipSeaFxx(io).poll(duration_ms, interval_ms=interval_ms, frame_profile=frame_profile)
357
366
  frames = result.get("frames", [])
358
367
  if not full and len(frames) > max_frames:
359
368
  kept = frames[:max_frames // 2 + 1] + frames[-(max_frames // 2 + 1):]
@@ -373,6 +382,8 @@ def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s:
373
382
  IO_FAIL (重试后仍无应答, 连续计数达 fail_threshold 熔断)。Ctrl+C 优雅收尾,
374
383
  CSV 已采数据保留, summary 标记 aborted。
375
384
  """
385
+ if not specs:
386
+ fail("CONFIG_ERROR", "stream 缺失目标寄存器表 (specs 不能为空)")
376
387
  # 默认值: lab.yaml ndtdbg.stream 段可覆盖内置默认 (均有默认值, 非必填)
377
388
  stream_cfg = (config.get("ndtdbg") or {}).get("stream") or {}
378
389
  if interval_ms is None:
@@ -463,7 +474,7 @@ def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s:
463
474
  fh.close()
464
475
  hints = {
465
476
  "PROBE_NOT_FOUND": "检查 NdtBox USB 连接与 CH341 驱动; 退出 STDataCollection.exe (USB 独占)",
466
- "TARGET_MISMATCH": "检查 IIC 接线/上拉/供电/从机地址; 请求-响应式通道需先 write_reg 开启 debug_mode (如 0xBE=01)",
477
+ "TARGET_MISMATCH": "检查 IIC 接线/上拉/供电/从机地址; 请求-响应式通道需先 write_reg 开启 debug_mode (参见目标固件协议文档)",
467
478
  }
468
479
  hint = hints.get(exc.error_class)
469
480
  detail = exc.detail or ""
@@ -601,7 +612,7 @@ def build_parser() -> argparse.ArgumentParser:
601
612
  w.add_argument("--addr", type=lambda x: int(x, 0), required=True)
602
613
  w.add_argument("--data", required=True, help="十六进制数据, 如 'A5 01 02'")
603
614
 
604
- p_poll = sub.add_parser("poll", parents=[common], help="轮询采集 (0xEC/0xED/0xEE 调试流)")
615
+ p_poll = sub.add_parser("poll", parents=[common], help="轮询采集 (调试数据流, 依 lab.yaml target_profile.measure_frame 解析)")
605
616
  p_poll.add_argument("--duration-ms", type=int, default=2000)
606
617
  p_poll.add_argument("--interval-ms", type=int, default=10)
607
618
  p_poll.add_argument("--max-frames", type=int, default=4, help="非 --full 时保留的帧样本数")
@@ -636,8 +647,8 @@ def build_parser() -> argparse.ArgumentParser:
636
647
 
637
648
  st = sub.add_parser("stream", parents=[common],
638
649
  help="高速连续采集寄存器组到 CSV (单会话长驻, 只读)")
639
- st.add_argument("--regs", nargs="+", default=["0xEE:8"],
640
- help="寄存器 spec addr:len[:name], 如 0xEE:8:ch 0x20:138:event")
650
+ st.add_argument("--regs", nargs="+", default=None,
651
+ help="寄存器 spec addr:len[:name], 如 0xEE:8:ch (默认读 lab.yaml ndtdbg.stream.regs 或 target_profile.stream_regs)")
641
652
  st.add_argument("--interval-ms", type=int, default=None,
642
653
  help="目标采样间隔 ms (默认 lab.yaml ndtdbg.stream.default_interval_ms 或 100)")
643
654
  st.add_argument("--duration-s", type=float, default=0,
@@ -689,7 +700,7 @@ def main(argv=None) -> None:
689
700
  fail("CONFIG_ERROR", "write-reg 参数非法")
690
701
  cmd_write_reg(io, config, args.addr, args.data, args.confirm)
691
702
  elif args.cmd == "poll":
692
- cmd_poll(io, args.duration_ms, args.interval_ms, args.max_frames, args.full)
703
+ cmd_poll(io, config, args.duration_ms, args.interval_ms, args.max_frames, args.full)
693
704
  elif args.cmd == "flash-bin":
694
705
  cmd_flash_bin(io, config, args.file, args.erase_ms, args.confirm,
695
706
  off_ms=args.off_ms, rewrite_times=args.rewrite_times,
@@ -698,7 +709,21 @@ def main(argv=None) -> None:
698
709
  cold_boot_reg=args.cold_boot_reg,
699
710
  cold_boot_expect=args.cold_boot_expect)
700
711
  elif args.cmd == "stream":
701
- specs = [parse_reg_spec(s) for s in args.regs]
712
+ raw_regs = args.regs
713
+ if not raw_regs:
714
+ ndtdbg_cfg = config.get("ndtdbg") or {}
715
+ stream_cfg = ndtdbg_cfg.get("stream") or {}
716
+ target_profile = ndtdbg_cfg.get("target_profile") or {}
717
+ raw_regs = stream_cfg.get("regs") or target_profile.get("stream_regs")
718
+ if not raw_regs:
719
+ fail("CONFIG_ERROR",
720
+ "stream 缺失目标寄存器表 (regs 必填且未配置兜底)",
721
+ detail=(f"生效配置: {effective_config_desc()}; "
722
+ "请传 --regs 参数 (如 --regs 0xEE:8:ch), "
723
+ "或在 lab.yaml ndtdbg.stream.regs / ndtdbg.target_profile.stream_regs 配置"))
724
+ if isinstance(raw_regs, str):
725
+ raw_regs = [raw_regs]
726
+ specs = [parse_reg_spec(s) for s in raw_regs]
702
727
  cmd_stream(io, config, specs, args.interval_ms, args.duration_s,
703
728
  args.max_frames, args.out, args.decode, args.retry,
704
729
  args.fail_threshold, args.require_target, args.pre_write,
@@ -1,6 +1,6 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Dual-Tier MCP setup and registration helper (v0.7.4).
3
+ Dual-Tier MCP setup and registration helper (v0.9.0).
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.4)" -ForegroundColor Green
245
+ Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.9.0)" -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