firmwareloop 0.8.0__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.8.0 → firmwareloop-0.9.0}/PKG-INFO +4 -4
  2. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/README.md +3 -3
  3. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/firmwareloop.egg-info/PKG-INFO +4 -4
  4. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/pyproject.toml +1 -1
  5. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/__init__.py +1 -1
  6. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/fw_mcp_server.py +92 -58
  7. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/lib/ndtdbg.py +63 -29
  8. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/ndtdbg_cli.py +33 -8
  9. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/setup-agent-mcp.ps1 +2 -2
  10. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/LICENSE +0 -0
  11. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/firmwareloop.egg-info/SOURCES.txt +0 -0
  12. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/firmwareloop.egg-info/dependency_links.txt +0 -0
  13. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/firmwareloop.egg-info/entry_points.txt +0 -0
  14. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/firmwareloop.egg-info/requires.txt +0 -0
  15. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/firmwareloop.egg-info/top_level.txt +0 -0
  16. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/setup.cfg +0 -0
  17. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/acceptance-scenario.ps1 +0 -0
  18. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/build.ps1 +0 -0
  19. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/can.ps1 +0 -0
  20. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/check-qoder-mcp.ps1 +0 -0
  21. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/common/build-backends.psm1 +0 -0
  22. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/common/fw.psm1 +0 -0
  23. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/common/uart_probe.py +0 -0
  24. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/doctor.ps1 +0 -0
  25. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/flash.ps1 +0 -0
  26. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/instrument_cli.py +0 -0
  27. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/lib/__init__.py +0 -0
  28. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/lib/instruments.py +0 -0
  29. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/lib/serial_assistant.py +0 -0
  30. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/logic_capture.ps1 +0 -0
  31. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/logic_decode.ps1 +0 -0
  32. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/ndtdbg_bridge.ps1 +0 -0
  33. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/reset.ps1 +0 -0
  34. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/serial_cli.py +0 -0
  35. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/sigrok_csv_to_fwloop.py +0 -0
  36. {firmwareloop-0.8.0 → firmwareloop-0.9.0}/tools/test-backends.ps1 +0 -0
  37. {firmwareloop-0.8.0 → 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.8.0
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
@@ -385,7 +385,7 @@ claude mcp remove --scope user agentic-hil
385
385
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
386
386
  ├── demo-make/ 示例固件(Make;构建测试载体)
387
387
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
388
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.8.0)
388
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.9.0)
389
389
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
390
390
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
391
391
  ├── CLAUDE.md Claude Code CLI 指南
@@ -395,7 +395,7 @@ claude mcp remove --scope user agentic-hil
395
395
 
396
396
  ---
397
397
 
398
- ## 能力验证状态(v0.8.0)
398
+ ## 能力验证状态(v0.9.0)
399
399
 
400
400
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
401
401
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -419,7 +419,7 @@ claude mcp remove --scope user agentic-hil
419
419
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
420
420
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
421
421
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
422
- | 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 桥 → 解析信封)尚待真机验收** |
423
423
  | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
424
424
  | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
425
425
  | 抓开机日志(fw_serial_capture + power_cycle_off_ms, 窗口内断电复位) | Implemented(simulator 验证窗口与复位串联)-> Real Hardware Validated 待 NdtBox + DUT |
@@ -357,7 +357,7 @@ claude mcp remove --scope user agentic-hil
357
357
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
358
358
  ├── demo-make/ 示例固件(Make;构建测试载体)
359
359
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
360
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.8.0)
360
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.9.0)
361
361
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
362
362
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
363
363
  ├── CLAUDE.md Claude Code CLI 指南
@@ -367,7 +367,7 @@ claude mcp remove --scope user agentic-hil
367
367
 
368
368
  ---
369
369
 
370
- ## 能力验证状态(v0.8.0)
370
+ ## 能力验证状态(v0.9.0)
371
371
 
372
372
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
373
373
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -391,7 +391,7 @@ claude mcp remove --scope user agentic-hil
391
391
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
392
392
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
393
393
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
394
- | 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 桥 → 解析信封)尚待真机验收** |
395
395
  | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
396
396
  | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
397
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.8.0
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
@@ -385,7 +385,7 @@ claude mcp remove --scope user agentic-hil
385
385
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
386
386
  ├── demo-make/ 示例固件(Make;构建测试载体)
387
387
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
388
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.8.0)
388
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.9.0)
389
389
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
390
390
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
391
391
  ├── CLAUDE.md Claude Code CLI 指南
@@ -395,7 +395,7 @@ claude mcp remove --scope user agentic-hil
395
395
 
396
396
  ---
397
397
 
398
- ## 能力验证状态(v0.8.0)
398
+ ## 能力验证状态(v0.9.0)
399
399
 
400
400
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
401
401
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -419,7 +419,7 @@ claude mcp remove --scope user agentic-hil
419
419
  | 自动修复闭环(M6 载体) | Simulator Validated -> Real Hardware Validated 待 DUT |
420
420
  | NdtBox 数据采集/调试(fw_ndtdbg_status / fw_ndtdbg_measure, ChipSea Fxx IIC) | Implemented(无硬件 Fail-Closed 已验证)-> Real Hardware Validated 待 NdtBox |
421
421
  | NdtBox 目标断电复位(fw_ndtdbg_power_cycle, 仅 io10 断电复位, 无写闸门) | Implemented(单测覆盖时序/探针缺失指引)-> Real Hardware Validated 待 NdtBox |
422
- | 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 桥 → 解析信封)尚待真机验收** |
423
423
  | 烧后冷启动冒烟(fw_ndtdbg_flash 默认开, `cold_boot_check`) | Implemented(单测覆盖通过/全 FF/期望不符/关闭;`COLD_BOOT_VERIFY_FAILED` 错误类)-> Real Hardware Validated 待真机 |
424
424
  | 串口占用取证(`port_probe` 试开硬结论 + `holder_candidates` 候选名单) | Implemented(试开结论可复现;候选按 cmdline/name 标注置信度) |
425
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.8.0"
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.8.0"
5
+ __version__ = "0.9.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.8.0).
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
6
  Agents (Antigravity CLI, Claude Code CLI, Qoder IDE, CodeBuddy IDE, DeepSeek Harness,
@@ -384,13 +384,16 @@ def handle_fw_ndtdbg_power_cycle(args: Dict[str, Any]) -> Dict[str, Any]:
384
384
  def _stream_cli_cmd(args: Dict[str, Any], out_path: str, duration_cap: float) -> Optional[list]:
385
385
  """构造 stream CLI 命令 (同步/job 共用); 返回 None 表示参数非法。"""
386
386
  cli_script = os.path.join(REPO_ROOT, "tools", "ndtdbg_cli.py")
387
- regs = args.get("regs") or ["0xEE:8"]
388
387
  duration_s = min(max(float(args.get("duration_s", 10)), 0.0), duration_cap)
389
388
  max_frames = int(args.get("max_frames", 0))
390
389
  if duration_s <= 0 and max_frames <= 0:
391
390
  return None
392
- cmd = [sys.executable, cli_script, "stream", "--regs", *[str(r) for r in regs],
393
- "--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]])
394
397
  if args.get("interval_ms") is not None:
395
398
  cmd.extend(["--interval-ms", str(int(args["interval_ms"]))])
396
399
  if duration_s > 0:
@@ -567,10 +570,13 @@ def handle_fw_ndtdbg_capture_seq(args: Dict[str, Any]) -> Dict[str, Any]:
567
570
  cli_script = os.path.join(REPO_ROOT, "tools", "ndtdbg_cli.py")
568
571
  cap_script = os.path.join(REPO_ROOT, "tools", "logic_capture.ps1")
569
572
  duration_ms = int(args.get("duration_ms", 8000))
570
- writes = args.get("writes", [{"reg": "0xBE", "data": "01"}, {"reg": "0x01", "data": "BE 42"}])
571
- 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")
572
578
  poll_attempts = int(args.get("poll_attempts", 5))
573
- readback_reg = args.get("readback_reg", "0xBE")
579
+ readback_reg = args.get("readback_reg")
574
580
  poll_expect = " ".join(str(args.get("poll_expect", "00 00")).split()).upper()
575
581
  confirm = bool(args.get("confirm", True))
576
582
 
@@ -902,7 +908,7 @@ def handle_fw_init_project(args: Dict[str, Any]) -> Dict[str, Any]:
902
908
  return {
903
909
  "ok": False,
904
910
  "error_class": "CONFIG_ERROR",
905
- "error": "target_chip 必填 (如 STM32F103C8 / CSA37F71)",
911
+ "error": "target_chip 必填 (如 STM32F103C8 / STM32F407ZG)",
906
912
  "detail": "fwloop init <target_chip> 或 MCP fw_init_project(target_chip=...)",
907
913
  }
908
914
 
@@ -1936,8 +1942,7 @@ TOOLS_REGISTRY = {
1936
1942
  "regs": {
1937
1943
  "type": "array",
1938
1944
  "items": {"type": "string"},
1939
- "description": "Register specs 'addr:len[:name]', e.g. ['0xEE:8:ch', '0x20:138:event']. Default ['0xEE:8'].",
1940
- "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.",
1941
1946
  },
1942
1947
  "interval_ms": {
1943
1948
  "type": "integer",
@@ -1961,7 +1966,7 @@ TOOLS_REGISTRY = {
1961
1966
  "decode": {
1962
1967
  "type": "string",
1963
1968
  "enum": ["hex", "int16le", "uint16le", "int8", "uint8"],
1964
- "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).",
1965
1970
  "default": "hex",
1966
1971
  },
1967
1972
  "retry": {
@@ -1987,7 +1992,7 @@ TOOLS_REGISTRY = {
1987
1992
  "interleave_writes": {
1988
1993
  "type": "array",
1989
1994
  "items": {"type": "string"},
1990
- "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).",
1991
1996
  },
1992
1997
  "confirm": {
1993
1998
  "type": "boolean",
@@ -1997,7 +2002,6 @@ TOOLS_REGISTRY = {
1997
2002
  "dll_dir": {"type": "string", "description": "NdtDebugTool lib dir; default from lab.yaml ndtdbg.dll_dir"},
1998
2003
  "slave_addr": {"type": "string", "description": "IIC slave address, e.g. 0xA0; default from lab.yaml"},
1999
2004
  },
2000
- "required": ["regs"],
2001
2005
  "additionalProperties": False,
2002
2006
  },
2003
2007
  "handler": handle_fw_ndtdbg_stream,
@@ -2010,8 +2014,7 @@ TOOLS_REGISTRY = {
2010
2014
  "regs": {
2011
2015
  "type": "array",
2012
2016
  "items": {"type": "string"},
2013
- "description": "Register specs 'addr:len[:name]'. Default ['0xEE:8'].",
2014
- "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.",
2015
2018
  },
2016
2019
  "interval_ms": {"type": "integer", "description": "Target sampling interval in ms.", "default": 100},
2017
2020
  "duration_s": {"type": "number", "description": "Total capture duration in seconds (cap 3600).", "default": 10},
@@ -2056,7 +2059,7 @@ TOOLS_REGISTRY = {
2056
2059
  "handler": handle_fw_ndtdbg_stream_stop,
2057
2060
  },
2058
2061
  "fw_ndtdbg_capture_seq": {
2059
- "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.",
2060
2063
  "inputSchema": {
2061
2064
  "type": "object",
2062
2065
  "properties": {
@@ -2074,20 +2077,21 @@ TOOLS_REGISTRY = {
2074
2077
  "data": {"type": "string"},
2075
2078
  },
2076
2079
  },
2077
- "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.",
2078
2081
  },
2079
2082
  "confirm": {
2080
2083
  "type": "boolean",
2081
2084
  "description": "Acknowledge the register write intent; handler auto-passes --confirm to the CLI (double-gated with lab.yaml ndtdbg.write_allowed)",
2082
2085
  "default": True,
2083
2086
  },
2084
- "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)"},
2085
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"},
2086
2089
  "poll_attempts": {"type": "integer", "description": "Max poll attempts", "default": 5},
2087
- "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)"},
2088
2091
  "dll_dir": {"type": "string", "description": "NdtDebugTool lib directory; default from lab.yaml"},
2089
2092
  "slave_addr": {"type": "string", "description": "IIC slave address (e.g. 0xA0)"},
2090
2093
  },
2094
+ "required": ["writes"],
2091
2095
  "additionalProperties": False,
2092
2096
  },
2093
2097
  "handler": handle_fw_ndtdbg_capture_seq,
@@ -2174,7 +2178,7 @@ TOOLS_REGISTRY = {
2174
2178
  },
2175
2179
  "target_chip": {
2176
2180
  "type": "string",
2177
- "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",
2178
2182
  },
2179
2183
  "build_backend": {
2180
2184
  "type": "string",
@@ -2387,7 +2391,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
2387
2391
  },
2388
2392
  "serverInfo": {
2389
2393
  "name": "firmwareloop",
2390
- "version": "0.8.0"
2394
+ "version": "0.9.0"
2391
2395
  }
2392
2396
  }
2393
2397
  }
@@ -2522,7 +2526,7 @@ def _settle_stashed_entrypoints(stashed) -> None:
2522
2526
  def handle_cli_update() -> int:
2523
2527
  """Handle `firmwareloop update` command."""
2524
2528
  print("============================================================")
2525
- print(" FirmwareLoop Auto-Updater (v0.8.0)")
2529
+ print(" FirmwareLoop Auto-Updater (v0.9.0)")
2526
2530
  print("============================================================")
2527
2531
 
2528
2532
  is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
@@ -2659,6 +2663,18 @@ def handle_cli_selftest(target: str = "ndtdbg") -> int:
2659
2663
  def report(name, status, detail=""):
2660
2664
  results.append((name, status, detail))
2661
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
+
2662
2678
  # 1. status 分层探测
2663
2679
  print("\n[1/4] status 探测 (Box / IIC bus / target chip) ...")
2664
2680
  rc, body = run_cli("status", timeout=30)
@@ -2675,40 +2691,58 @@ def handle_cli_selftest(target: str = "ndtdbg") -> int:
2675
2691
  report("protocol.desync", _st, _detail)
2676
2692
 
2677
2693
  # 2. stream 冒烟 (100ms x 10s)
2678
- print("[2/4] stream 冒烟 (100ms x 10s, int16 解码) ...")
2679
- rc, body = run_cli("stream", "--regs", "0xEE:8:ch", "--interval-ms", "100",
2680
- "--duration-s", "10", "--decode", "int16le", timeout=60)
2681
- stream_data = (body or {}).get("data") if isinstance(body, dict) else None
2682
- if rc == 0 and stream_data:
2683
- total = stream_data.get("frames_total", 0)
2684
- conserved = (stream_data.get("frames_ok", 0) + stream_data.get("frames_ff_gap", 0)
2685
- + stream_data.get("frames_io_fail", 0)) == total
2686
- avg = stream_data.get("interval_actual_avg_ms")
2687
- report("stream.frames_conserved", "PASS" if conserved else "FAIL",
2688
- f"total={total} (ok={stream_data.get('frames_ok')}, "
2689
- f"ff_gap={stream_data.get('frames_ff_gap')}, io_fail={stream_data.get('frames_io_fail')})")
2690
- report("stream.interval_perf", "PASS" if (avg is not None and avg <= 110) else "FAIL",
2691
- f"avg={avg}ms (target 100ms, 验收线 <=110ms)")
2692
- report("stream.io_overhead", "PASS" if stream_data.get("io_overhead_avg_ms") is not None else "FAIL",
2693
- f"io_overhead_avg_ms={stream_data.get('io_overhead_avg_ms')}")
2694
- report("stream.gaps", "PASS", f"gaps_n={stream_data.get('gaps_n', 0)} (仅 >2x target 计数)")
2695
- else:
2696
- report("stream", "FAIL", (body or {}).get("error_class", f"exit {rc}"))
2697
-
2698
- # 3. 写读回环 (write_allowed 双门内; 未授权 SKIP, 不算 FAIL)
2699
- print("[3/4] 写读回环 (0xBE debug_mode, lab.yaml write_allowed) ...")
2700
- rc, body = run_cli("write-reg", "--addr", "0xBE", "--data", "01", "--confirm")
2701
- if rc == 0 and body and body.get("ok"):
2702
- auth = (body.get("data") or {}).get("authorization") or {}
2703
- report("write.readback", "PASS", f"0xBE=01 写入成功 (授权来源: {auth.get('granted_by', '?')})")
2704
- run_cli("write-reg", "--addr", "0xBE", "--data", "00", "--confirm")
2705
- 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}"))
2706
2721
  else:
2707
- err_class = (body or {}).get("error_class", f"exit {rc}")
2708
- if err_class == "PERMISSION_DENIED":
2709
- 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} 已恢复")
2710
2737
  else:
2711
- 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),写回环自验跳过")
2712
2746
 
2713
2747
  # 3.5 write_allowed 卫生检查 (P1-2): 任何层级为 true 即 WARNING (会话级临时态)
2714
2748
  print("[3.5/4] write_allowed 卫生检查 ...")
@@ -3139,7 +3173,7 @@ def set_ndtdbg_dlldir(dll_path: str) -> str:
3139
3173
  def handle_cli_setup() -> int:
3140
3174
  """Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, CodeBuddy IDE, DeepSeek Harness (DSH), and ZCode IDE."""
3141
3175
  print("============================================================")
3142
- print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.8.0)")
3176
+ print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.9.0)")
3143
3177
  print("============================================================")
3144
3178
  print("")
3145
3179
 
@@ -3294,7 +3328,7 @@ def handle_cli_setup() -> int:
3294
3328
  def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
3295
3329
  """Handle `firmwareloop init [target_chip]` command."""
3296
3330
  print("============================================================")
3297
- print(" FirmwareLoop Multi-Agent Project Initializer (v0.8.0)")
3331
+ print(" FirmwareLoop Multi-Agent Project Initializer (v0.9.0)")
3298
3332
  print("============================================================")
3299
3333
  cwd = os.getcwd()
3300
3334
  print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
@@ -3327,7 +3361,7 @@ def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
3327
3361
 
3328
3362
 
3329
3363
  def print_cli_help() -> None:
3330
- print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.8.0)
3364
+ print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.9.0)
3331
3365
 
3332
3366
  Usage:
3333
3367
  fwloop [command] (or: firmwareloop [command])
@@ -3373,7 +3407,7 @@ def main() -> None:
3373
3407
  sys.exit(2)
3374
3408
  sys.exit(handle_cli_ndtdebugtool(args[1]))
3375
3409
  elif cmd in ["version", "-v", "--version"]:
3376
- print("FirmwareLoop v0.8.0")
3410
+ print("FirmwareLoop v0.9.0")
3377
3411
  sys.exit(0)
3378
3412
  elif cmd in ["help", "-h", "--help"]:
3379
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.8.0).
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.8.0)" -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