firmwareloop 0.2.1__tar.gz → 0.3.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 (35) hide show
  1. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/PKG-INFO +3 -3
  2. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/README.md +2 -2
  3. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/firmwareloop.egg-info/PKG-INFO +3 -3
  4. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/pyproject.toml +1 -1
  5. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/__init__.py +1 -1
  6. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/fw_mcp_server.py +15 -8
  7. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/lib/ndtdbg.py +34 -4
  8. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/ndtdbg_bridge.ps1 +72 -29
  9. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/ndtdbg_cli.py +28 -4
  10. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/setup-agent-mcp.ps1 +2 -2
  11. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/LICENSE +0 -0
  12. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/firmwareloop.egg-info/SOURCES.txt +0 -0
  13. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/firmwareloop.egg-info/dependency_links.txt +0 -0
  14. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/firmwareloop.egg-info/entry_points.txt +0 -0
  15. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/firmwareloop.egg-info/requires.txt +0 -0
  16. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/firmwareloop.egg-info/top_level.txt +0 -0
  17. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/setup.cfg +0 -0
  18. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/acceptance-scenario.ps1 +0 -0
  19. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/build.ps1 +0 -0
  20. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/can.ps1 +0 -0
  21. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/check-qoder-mcp.ps1 +0 -0
  22. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/common/build-backends.psm1 +0 -0
  23. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/common/fw.psm1 +0 -0
  24. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/common/uart_probe.py +0 -0
  25. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/doctor.ps1 +0 -0
  26. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/flash.ps1 +0 -0
  27. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/instrument_cli.py +0 -0
  28. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/lib/__init__.py +0 -0
  29. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/lib/instruments.py +0 -0
  30. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/logic_capture.ps1 +0 -0
  31. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/logic_decode.ps1 +0 -0
  32. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/reset.ps1 +0 -0
  33. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/sigrok_csv_to_fwloop.py +0 -0
  34. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/test-backends.ps1 +0 -0
  35. {firmwareloop-0.2.1 → firmwareloop-0.3.0}/tools/test.ps1 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.2.1
3
+ Version: 0.3.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
@@ -381,7 +381,7 @@ claude mcp remove --scope user agentic-hil
381
381
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
382
382
  ├── demo-make/ 示例固件(Make;构建测试载体)
383
383
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
384
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.2.1)
384
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.3.0)
385
385
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
386
386
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
387
387
  ├── CLAUDE.md Claude Code CLI 指南
@@ -391,7 +391,7 @@ claude mcp remove --scope user agentic-hil
391
391
 
392
392
  ---
393
393
 
394
- ## 能力验证状态(v0.2.1
394
+ ## 能力验证状态(v0.3.0
395
395
 
396
396
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
397
397
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -353,7 +353,7 @@ claude mcp remove --scope user agentic-hil
353
353
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
354
354
  ├── demo-make/ 示例固件(Make;构建测试载体)
355
355
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
356
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.2.1)
356
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.3.0)
357
357
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
358
358
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
359
359
  ├── CLAUDE.md Claude Code CLI 指南
@@ -363,7 +363,7 @@ claude mcp remove --scope user agentic-hil
363
363
 
364
364
  ---
365
365
 
366
- ## 能力验证状态(v0.2.1
366
+ ## 能力验证状态(v0.3.0
367
367
 
368
368
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
369
369
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.2.1
3
+ Version: 0.3.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
@@ -381,7 +381,7 @@ claude mcp remove --scope user agentic-hil
381
381
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
382
382
  ├── demo-make/ 示例固件(Make;构建测试载体)
383
383
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
384
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.2.1)
384
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.3.0)
385
385
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
386
386
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
387
387
  ├── CLAUDE.md Claude Code CLI 指南
@@ -391,7 +391,7 @@ claude mcp remove --scope user agentic-hil
391
391
 
392
392
  ---
393
393
 
394
- ## 能力验证状态(v0.2.1
394
+ ## 能力验证状态(v0.3.0
395
395
 
396
396
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
397
397
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "firmwareloop"
7
- version = "0.2.1"
7
+ version = "0.3.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.2.1"
5
+ __version__ = "0.3.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.2.1).
3
+ fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.3.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, Cursor) via the Model Context
@@ -360,6 +360,8 @@ def handle_fw_ndtdbg_stream(args: Dict[str, Any]) -> Dict[str, Any]:
360
360
  cmd.extend(["--fail-threshold", str(int(args["fail_threshold"]))])
361
361
  if args.get("require_target"):
362
362
  cmd.append("--require-target")
363
+ if args.get("pre_write"):
364
+ cmd.extend(["--pre-write", *[str(p) for p in args["pre_write"]]])
363
365
  _ndtdbg_reg_args(args, cmd)
364
366
  return _ndtdbg_run(cmd, timeout=int(duration_s) + 120)
365
367
 
@@ -1387,7 +1389,7 @@ TOOLS_REGISTRY = {
1387
1389
  "handler": handle_fw_ndtdbg_flash,
1388
1390
  },
1389
1391
  "fw_ndtdbg_stream": {
1390
- "description": "Stream-read a set of IIC registers from the NdtBox target chip (ChipSea Fxx) at a fixed interval via one long-lived USB session. Read-only. Writes CSV with timestamps; returns JSON summary (frames, FF_GAP/IO_FAIL counts, interval stats). For capturing sensor/debug data streams (e.g. PT488 0xEE 4-channel data). Typical combo: write_reg(0xBE=01, confirm) to start debug_mode, then stream(0xEE).",
1392
+ "description": "Stream-read a set of IIC registers from the NdtBox target chip (ChipSea Fxx) at a fixed interval via one long-lived USB session. Read-only. Writes CSV with timestamps; returns JSON summary (frames, FF_GAP/IO_FAIL counts, interval stats, io_overhead). Supports two protocol profiles: continuous (pure-read polling) and request-response (pre_write command written before every frame, e.g. PT488 0x10/0x14 channels). Per-frame IO overhead is reported separately in io_overhead_avg_ms.",
1391
1393
  "inputSchema": {
1392
1394
  "type": "object",
1393
1395
  "properties": {
@@ -1437,6 +1439,11 @@ TOOLS_REGISTRY = {
1437
1439
  "description": "Probe reg 0x10 before capture; abort with TARGET_MISMATCH when the chip does not answer.",
1438
1440
  "default": False,
1439
1441
  },
1442
+ "pre_write": {
1443
+ "type": "array",
1444
+ "items": {"type": "string"},
1445
+ "description": "Request-response protocol profile: commands 'addr:data' written before EVERY frame (e.g. ['0xEC:10']) so request-response channels refresh each read. Omit for continuous (pure-read) streams. Defaults from lab.yaml ndtdbg.stream.pre_write when omitted.",
1446
+ },
1440
1447
  "dll_dir": {"type": "string", "description": "NdtDebugTool lib dir; default from lab.yaml ndtdbg.dll_dir"},
1441
1448
  "slave_addr": {"type": "string", "description": "IIC slave address, e.g. 0xA0; default from lab.yaml"},
1442
1449
  },
@@ -1619,7 +1626,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
1619
1626
  },
1620
1627
  "serverInfo": {
1621
1628
  "name": "firmwareloop",
1622
- "version": "0.2.1"
1629
+ "version": "0.3.0"
1623
1630
  }
1624
1631
  }
1625
1632
  }
@@ -1754,7 +1761,7 @@ def _settle_stashed_entrypoints(stashed) -> None:
1754
1761
  def handle_cli_update() -> int:
1755
1762
  """Handle `firmwareloop update` command."""
1756
1763
  print("============================================================")
1757
- print(" FirmwareLoop Auto-Updater (v0.2.1)")
1764
+ print(" FirmwareLoop Auto-Updater (v0.3.0)")
1758
1765
  print("============================================================")
1759
1766
 
1760
1767
  is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
@@ -2029,7 +2036,7 @@ def set_ndtdbg_dlldir(dll_path: str) -> str:
2029
2036
  def handle_cli_setup() -> int:
2030
2037
  """Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, and CodeBuddy IDE."""
2031
2038
  print("============================================================")
2032
- print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.2.1)")
2039
+ print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.3.0)")
2033
2040
  print("============================================================")
2034
2041
  print("")
2035
2042
 
@@ -2154,7 +2161,7 @@ def handle_cli_setup() -> int:
2154
2161
  def handle_cli_init() -> int:
2155
2162
  """Handle `firmwareloop init` command."""
2156
2163
  print("============================================================")
2157
- print(" FirmwareLoop Multi-Agent Project Initializer (v0.2.1)")
2164
+ print(" FirmwareLoop Multi-Agent Project Initializer (v0.3.0)")
2158
2165
  print("============================================================")
2159
2166
  cwd = os.getcwd()
2160
2167
  print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
@@ -2175,7 +2182,7 @@ def handle_cli_init() -> int:
2175
2182
 
2176
2183
 
2177
2184
  def print_cli_help() -> None:
2178
- print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.2.1)
2185
+ print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.3.0)
2179
2186
 
2180
2187
  Usage:
2181
2188
  fwloop [command] (or: firmwareloop [command])
@@ -2215,7 +2222,7 @@ def main() -> None:
2215
2222
  sys.exit(2)
2216
2223
  sys.exit(handle_cli_ndtdebugtool(args[1]))
2217
2224
  elif cmd in ["version", "-v", "--version"]:
2218
- print("FirmwareLoop v0.2.1")
2225
+ print("FirmwareLoop v0.3.0")
2219
2226
  sys.exit(0)
2220
2227
  elif cmd in ["help", "-h", "--help"]:
2221
2228
  print_cli_help()
@@ -106,6 +106,30 @@ def parse_reg_spec(spec: str) -> dict:
106
106
  return {"addr": addr, "len": length, "name": name or f"reg_{addr:02X}"}
107
107
 
108
108
 
109
+ def parse_pre_write_spec(spec: str) -> dict:
110
+ """解析 pre-write spec 'addr:data' -> {'addr', 'data'} (请求-响应式协议每帧先写的命令)。
111
+
112
+ data 为 hex 载荷 (可含空格)。Fail-Closed: 格式/越界抛 CONFIG_ERROR。
113
+ """
114
+ parts = str(spec).split(":", 1)
115
+ if len(parts) != 2 or not parts[0] or not parts[1].strip():
116
+ raise NdtBoxError("CONFIG_ERROR", "pre-write spec 格式非法 (addr:data)", str(spec))
117
+ try:
118
+ addr = int(parts[0], 0)
119
+ except ValueError as exc:
120
+ raise NdtBoxError("CONFIG_ERROR", "pre-write spec addr 不是合法整数", str(spec)) from exc
121
+ if not 0 <= addr <= 0xFF:
122
+ raise NdtBoxError("CONFIG_ERROR", "pre-write spec addr 越界 (0..0xFF)", hex(addr))
123
+ data = parts[1].strip()
124
+ if len(data.replace(" ", "")) % 2 != 0:
125
+ raise NdtBoxError("CONFIG_ERROR", "pre-write spec data 不是偶数长度 hex", data)
126
+ try:
127
+ bytes.fromhex(data.replace(" ", ""))
128
+ except ValueError as exc:
129
+ raise NdtBoxError("CONFIG_ERROR", "pre-write spec data 含非法 hex 字符", data) from exc
130
+ return {"addr": addr, "data": data}
131
+
132
+
109
133
  def decode_payload(hex_str: str | None, mode: str) -> str | None:
110
134
  """hex 载荷按模式解码为 '|' 分隔数值串; mode=hex 或空载荷返回 None。
111
135
 
@@ -132,17 +156,23 @@ def decode_payload(hex_str: str | None, mode: str) -> str | None:
132
156
  return "|".join(str(v) for v in vals) if vals else None
133
157
 
134
158
 
135
- def interval_stats(frame_times_ms: list[float]) -> dict:
136
- """帧完成时间序列 -> 实际间隔统计 {avg_ms, p95_ms, gaps_n}; 帧数 < 2 时返回空 dict。"""
159
+ def interval_stats(frame_times_ms: list[float], target_ms: float | None = None) -> dict:
160
+ """帧开始时间序列 -> 实际间隔统计; 帧数 < 2 时返回空 dict。
161
+
162
+ gaps_n 语义 (实战修正): 仅统计间隔 > 2x 目标间隔的帧数 (真实缺帧/卡顿);
163
+ 正常 IO 开销 (~10ms/帧) 不计入, 否则该指标永远接近 100% 失真。
164
+ """
137
165
  gaps = [round(b - a, 1) for a, b in zip(frame_times_ms, frame_times_ms[1:])]
138
166
  if not gaps:
139
167
  return {}
140
168
  ordered = sorted(gaps)
141
- return {
142
- "gaps_n": len(gaps),
169
+ out = {
143
170
  "interval_actual_avg_ms": round(sum(gaps) / len(gaps), 1),
144
171
  "interval_actual_p95_ms": ordered[min(len(ordered) - 1, int(len(ordered) * 0.95))],
145
172
  }
173
+ if target_ms is not None:
174
+ out["gaps_n"] = sum(1 for g in gaps if g > 2 * target_ms)
175
+ return out
146
176
 
147
177
 
148
178
  def strip_nfw(file_bytes: bytes, header_len: int = 256) -> bytes:
@@ -463,11 +463,17 @@ try {
463
463
  $sRetry = if ($null -ne $cfg.retry) { [int]$cfg.retry } else { 2 }
464
464
  $sFailThr = if ($null -ne $cfg.fail_threshold) { [int]$cfg.fail_threshold } else { 50 }
465
465
  $sRequireTarget = [bool]$cfg.require_target
466
+ $sPreWrites = if ($cfg.pre_writes) { @($cfg.pre_writes) } else { @() }
466
467
  if ($sDurationS -le 0 -and $sMaxFrames -le 0) { Fail "CONFIG_ERROR" "stream 需要 duration_s 或 max_frames 之一 > 0" $null }
467
468
  if ($sInterval -lt 1) { Fail "CONFIG_ERROR" "stream interval_ms 必须 >= 1" $sInterval }
468
469
  foreach ($r in $sRegs) {
469
470
  if (([int]$r.len) -lt 1 -or ([int]$r.len) -gt 256) { Fail "CONFIG_ERROR" "stream 读长度越界 (1..256)" $r.len }
470
471
  }
472
+ foreach ($pw in $sPreWrites) {
473
+ if ($null -eq $pw.addr -or -not $pw.data) { Fail "CONFIG_ERROR" "pre_writes 项需要 {addr, data}" $null }
474
+ if (([int]$pw.addr) -lt 0 -or ([int]$pw.addr) -gt 0xFF) { Fail "CONFIG_ERROR" "pre_writes addr 越界 (0..0xFF)" $pw.addr }
475
+ if (((Parse-Hex ([string]$pw.data)).Count + 2) -gt 256) { Fail "CONFIG_ERROR" "pre_writes 写事务过长 (含从机地址+寄存器地址须 <=256)" $pw.data }
476
+ }
471
477
 
472
478
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
473
479
  $sw = [System.Diagnostics.Stopwatch]::StartNew()
@@ -495,48 +501,83 @@ try {
495
501
  $framesOk = 0; $framesGap = 0; $framesIoFail = 0
496
502
  $frameNo = 0
497
503
  $consecIoFail = 0
504
+ $frameIoMsSum = 0.0 # 帧内 IO 耗时累计 (帧完成 - 帧开始), 供 io_overhead_avg_ms
498
505
  $aborted = $false; $abortReason = ""
499
506
  $durationMsTotal = [int]($sDurationS * 1000)
500
507
  while ($true) {
501
508
  $frameNo++
502
509
  $frameStartMs = $sw.Elapsed.TotalMilliseconds
503
510
  $hasIoFail = $false; $hasGap = $false
504
- foreach ($r in $sRegs) {
505
- $reg = [int]$r.addr; $rlen = [int]$r.len
506
- $hexOut = "-"; $tag = "OK"
511
+
512
+ # --- pre-write (请求-响应式协议): 每帧先写请求寄存器再读数据 ---
513
+ # 写失败 -> IO_FAIL 行并跳过本帧读 (请求未送达, 读到的是旧快照)
514
+ $preOk = $true
515
+ foreach ($pw in $sPreWrites) {
516
+ $pAddr = [int]$pw.addr
517
+ $pData = Parse-Hex ([string]$pw.data)
518
+ $pwOk = $false
507
519
  try {
508
- for ($attempt = 0; $attempt -le $sRetry; $attempt++) {
509
- $tx = New-Object byte[] 2
510
- $tx[0] = [byte]$SlaveAddr; $tx[1] = [byte]$reg
511
- $rb = New-Object byte[] $rlen
512
- if (Invoke-WriteRead $h $tx $rb) {
513
- # ForEach-Object 将管道元素绑定到 $_——scriptblock param()
514
- # 形式收参恒为 null (Null 方法调用), 必须内联 $_
515
- $hexStr = (($rb | ForEach-Object { $_.ToString("X2") }) -join " ")
516
- $allFF = $true
517
- foreach ($b in $rb) { if ($b -ne 0xFF) { $allFF = $false; break } }
518
- if ($allFF) {
519
- $tag = "FF_GAP"; $hexOut = $hexStr # 总线高阻特征: 记录原值, 重试
520
- if ($attempt -lt $sRetry) { Start-Sleep -Milliseconds 5 }
520
+ $tx = New-Object byte[] (2 + $pData.Count)
521
+ $tx[0] = [byte]$SlaveAddr; $tx[1] = [byte]$pAddr
522
+ [Array]::Copy($pData, 0, $tx, 2, $pData.Count)
523
+ for ($pwA = 0; $pwA -le $sRetry; $pwA++) {
524
+ if (Invoke-WriteRead $h $tx (New-Object byte[] 0)) { $pwOk = $true; break }
525
+ if ($pwA -lt $sRetry) { Start-Sleep -Milliseconds 5 }
526
+ }
527
+ } catch {
528
+ $pwOk = $false
529
+ }
530
+ if (-not $pwOk) {
531
+ $preOk = $false
532
+ $tRel = [math]::Round($frameStartMs, 1)
533
+ [Console]::WriteLine(("#F`t{0}`t{1}`t0x{2:X2}`t-`tIO_FAIL" -f $frameNo, $tRel, $pAddr))
534
+ [Console]::Out.Flush()
535
+ }
536
+ }
537
+
538
+ if ($preOk) {
539
+ foreach ($r in $sRegs) {
540
+ $reg = [int]$r.addr; $rlen = [int]$r.len
541
+ $hexOut = "-"; $tag = "OK"
542
+ try {
543
+ for ($attempt = 0; $attempt -le $sRetry; $attempt++) {
544
+ $tx = New-Object byte[] 2
545
+ $tx[0] = [byte]$SlaveAddr; $tx[1] = [byte]$reg
546
+ $rb = New-Object byte[] $rlen
547
+ if (Invoke-WriteRead $h $tx $rb) {
548
+ # ForEach-Object 将管道元素绑定到 $_——scriptblock 用 param()
549
+ # 形式收参恒为 null (Null 方法调用), 必须内联 $_
550
+ $hexStr = (($rb | ForEach-Object { $_.ToString("X2") }) -join " ")
551
+ $allFF = $true
552
+ foreach ($b in $rb) { if ($b -ne 0xFF) { $allFF = $false; break } }
553
+ if ($allFF) {
554
+ $tag = "FF_GAP"; $hexOut = $hexStr # 总线高阻特征: 记录原值, 重试
555
+ if ($attempt -lt $sRetry) { Start-Sleep -Milliseconds 5 }
556
+ } else {
557
+ $tag = "OK"; $hexOut = $hexStr
558
+ break
559
+ }
521
560
  } else {
522
- $tag = "OK"; $hexOut = $hexStr
523
- break
561
+ $tag = "IO_FAIL"; $hexOut = "-"
562
+ if ($attempt -lt $sRetry) { Start-Sleep -Milliseconds 5 }
524
563
  }
525
- } else {
526
- $tag = "IO_FAIL"; $hexOut = "-"
527
- if ($attempt -lt $sRetry) { Start-Sleep -Milliseconds 5 }
528
564
  }
565
+ } catch {
566
+ # 单寄存器异常降级为 IO_FAIL 行继续采集, 不作废整个 stream (fail-closed 韧性)
567
+ $tag = "IO_FAIL"; $hexOut = "-"
529
568
  }
530
- } catch {
531
- # 单寄存器异常降级为 IO_FAIL 行继续采集, 不作废整个 stream (fail-closed 韧性)
532
- $tag = "IO_FAIL"; $hexOut = "-"
569
+ if ($tag -eq "IO_FAIL") { $hasIoFail = $true }
570
+ elseif ($tag -eq "FF_GAP") { $hasGap = $true }
571
+ # t_rel 取帧开始时刻: 帧间隔统计不含 IO 开销 (否则 p95 永远略超 target);
572
+ # 帧内 IO 耗时由 summary 的 io_overhead_avg_ms 单独暴露
573
+ $tRel = [math]::Round($frameStartMs, 1)
574
+ # 方法调用参数列表里逗号是参数分隔符, 必须括号把整个 -f 表达式包成单参数,
575
+ # 否则 -f 只拿到 1 个参数, {1} 起索引越界 (FormatException)
576
+ [Console]::WriteLine(("#F`t{0}`t{1}`t0x{2:X2}`t{3}`t{4}" -f $frameNo, $tRel, $reg, $hexOut, $tag))
577
+ [Console]::Out.Flush()
533
578
  }
534
- if ($tag -eq "IO_FAIL") { $hasIoFail = $true }
535
- elseif ($tag -eq "FF_GAP") { $hasGap = $true }
536
- $tRel = [math]::Round($sw.Elapsed.TotalMilliseconds, 1)
537
- [Console]::WriteLine("#F`t{0}`t{1}`t0x{2:X2}`t{3}`t{4}" -f $frameNo, $tRel, $reg, $hexOut, $tag)
538
- [Console]::Out.Flush()
539
579
  }
580
+ $frameIoMsSum += ($sw.Elapsed.TotalMilliseconds - $frameStartMs)
540
581
  if ($hasIoFail) { $framesIoFail++; $consecIoFail++ }
541
582
  else {
542
583
  if ($hasGap) { $framesGap++ } else { $framesOk++ }
@@ -552,9 +593,11 @@ try {
552
593
  }
553
594
  }
554
595
  [STDataCollection.Icb]::USBIO_CloseDevice([uint32]0) | Out-Null
596
+ $ioOverhead = if ($frameNo -gt 0) { [math]::Round($frameIoMsSum / $frameNo, 1) } else { 0 }
555
597
  $sum = @{ ok = $true; action = "stream"; data = @{
556
598
  frames_ok = $framesOk; frames_ff_gap = $framesGap; frames_io_fail = $framesIoFail;
557
599
  frames_total = $frameNo; aborted = $aborted; abort_reason = $abortReason;
600
+ io_overhead_avg_ms = $ioOverhead;
558
601
  duration_actual_ms = [int]$sw.Elapsed.TotalMilliseconds } }
559
602
  [Console]::WriteLine(($sum | ConvertTo-Json -Compress -Depth 5))
560
603
  [Console]::Out.Flush()
@@ -13,7 +13,7 @@ from datetime import datetime
13
13
 
14
14
  sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
15
15
  from lib.ndtdbg import (ChipSeaFxx, NdtBoxError, NdtBoxIo, decode_payload, # noqa: E402
16
- interval_stats, parse_reg_spec)
16
+ interval_stats, parse_pre_write_spec, parse_reg_spec)
17
17
 
18
18
  sys.stdout.reconfigure(encoding="utf-8", errors="replace")
19
19
 
@@ -199,7 +199,7 @@ def cmd_poll(io: NdtBoxIo, duration_ms: int, interval_ms: int, max_frames: int,
199
199
 
200
200
  def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s: float,
201
201
  max_frames: int, out_path, decode: str, retry: int, fail_threshold,
202
- require_target: bool) -> None:
202
+ require_target: bool, pre_write_specs=None) -> None:
203
203
  """高速连续采集 (只读): 桥进程内长驻 USB 会话, 单帧开销 = 一次 I2C 事务。
204
204
 
205
205
  帧语义: OK / FF_GAP (读成功但全 FF, 总线高阻特征, 只统计不熔断) /
@@ -227,8 +227,15 @@ def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s:
227
227
  if out_dir:
228
228
  os.makedirs(out_dir, exist_ok=True)
229
229
 
230
+ # pre-write (请求-响应式协议): CLI --pre-write 优先, 未传时回落 lab.yaml
231
+ # ndtdbg.stream.pre_write (产品协议 profile 沉淀); 两处均未配置 = 纯读 continuous
232
+ if pre_write_specs is None:
233
+ pre_write_specs = stream_cfg.get("pre_write") or []
234
+ pre_writes = [parse_pre_write_spec(s) for s in pre_write_specs]
235
+
230
236
  spec = {
231
237
  "regs": [{"addr": s["addr"], "len": s["len"], "name": s["name"]} for s in specs],
238
+ "pre_writes": [{"addr": p["addr"], "data": p["data"]} for p in pre_writes],
232
239
  "interval_ms": interval_ms, "duration_s": float(duration_s), "max_frames": max_frames,
233
240
  "retry": retry, "fail_threshold": fail_threshold, "require_target": require_target,
234
241
  }
@@ -266,6 +273,17 @@ def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s:
266
273
  data = io.stream_run(spec, on_row=on_row)
267
274
  except KeyboardInterrupt:
268
275
  data = None # 桥被终止; 行级计数近似, CSV 已写数据保留
276
+ except NdtBoxError as exc:
277
+ fh.close()
278
+ hints = {
279
+ "PROBE_NOT_FOUND": "检查 NdtBox USB 连接与 CH341 驱动; 退出 STDataCollection.exe (USB 独占)",
280
+ "TARGET_MISMATCH": "检查 IIC 接线/上拉/供电/从机地址; 请求-响应式通道需先 write_reg 开启 debug_mode (如 0xBE=01)",
281
+ }
282
+ hint = hints.get(exc.error_class)
283
+ detail = exc.detail or ""
284
+ if hint:
285
+ detail = f"{detail}; 建议: {hint}" if detail else f"建议: {hint}"
286
+ fail(exc.error_class, exc.message, detail=detail or None)
269
287
  finally:
270
288
  fh.close()
271
289
 
@@ -279,14 +297,17 @@ def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s:
279
297
  "frames_io_fail": data.get("frames_io_fail", rows["IO_FAIL"]) if data else rows["IO_FAIL"],
280
298
  "frames_total": total if total else sum(rows.values()),
281
299
  "interval_target_ms": interval_ms,
300
+ "io_overhead_avg_ms": data.get("io_overhead_avg_ms") if data else None,
282
301
  "duration_actual_s": round((data.get("duration_actual_ms") or 0) / 1000.0, 2) if data
283
302
  else round((state["t"] or 0) / 1000.0, 2),
284
303
  "aborted": bool(data.get("aborted")) if data else True,
285
304
  "abort_reason": data.get("abort_reason") or ("interrupted" if data is None else ""),
286
305
  }
287
- summary.update(interval_stats(frame_times))
306
+ summary.update(interval_stats(frame_times, target_ms=interval_ms))
288
307
  if decode != "hex":
289
308
  summary["decode"] = decode
309
+ if pre_writes:
310
+ summary["pre_write"] = [f"{p['addr']:#x}:{p['data']}" for p in pre_writes]
290
311
 
291
312
  if summary["aborted"] and summary["abort_reason"] == "fail_threshold":
292
313
  # 熔断: Fail-Closed 错误信封, 但 data 携带已采数据 summary
@@ -377,6 +398,9 @@ def build_parser() -> argparse.ArgumentParser:
377
398
  help="连续 IO_FAIL 熔断阈值 (默认 lab.yaml ndtdbg.stream.fail_threshold 或 50; 0=不熔断)")
378
399
  st.add_argument("--require-target", action="store_true",
379
400
  help="采集前探测 0x10, 无有效芯片应答则 TARGET_MISMATCH")
401
+ st.add_argument("--pre-write", nargs="+", default=None,
402
+ help="请求-响应式协议: 每帧先写的命令 spec addr:data (如 0xEC:10); "
403
+ "默认读 lab.yaml ndtdbg.stream.pre_write, 均未配置 = 纯读 continuous")
380
404
  return p
381
405
 
382
406
 
@@ -405,7 +429,7 @@ def main(argv=None) -> None:
405
429
  specs = [parse_reg_spec(s) for s in args.regs]
406
430
  cmd_stream(io, config, specs, args.interval_ms, args.duration_s,
407
431
  args.max_frames, args.out, args.decode, args.retry,
408
- args.fail_threshold, args.require_target)
432
+ args.fail_threshold, args.require_target, args.pre_write)
409
433
  else: # pragma: no cover
410
434
  fail("CONFIG_ERROR", f"unknown cmd: {args.cmd}")
411
435
  except NdtBoxError as exc:
@@ -1,6 +1,6 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Dual-Tier MCP setup and registration helper (v0.2.1).
3
+ Dual-Tier MCP setup and registration helper (v0.3.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.2.1)" -ForegroundColor Green
245
+ Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.3.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