firmwareloop 0.6.0__tar.gz → 0.6.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/PKG-INFO +3 -3
  2. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/README.md +2 -2
  3. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/PKG-INFO +3 -3
  4. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/pyproject.toml +1 -1
  5. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/__init__.py +1 -1
  6. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/doctor.ps1 +87 -5
  7. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/fw_mcp_server.py +275 -67
  8. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/lib/ndtdbg.py +99 -7
  9. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/ndtdbg_bridge.ps1 +6 -2
  10. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/ndtdbg_cli.py +78 -4
  11. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/serial_cli.py +31 -1
  12. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/setup-agent-mcp.ps1 +2 -2
  13. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/LICENSE +0 -0
  14. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/SOURCES.txt +0 -0
  15. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/dependency_links.txt +0 -0
  16. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/entry_points.txt +0 -0
  17. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/requires.txt +0 -0
  18. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/top_level.txt +0 -0
  19. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/setup.cfg +0 -0
  20. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/acceptance-scenario.ps1 +0 -0
  21. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/build.ps1 +0 -0
  22. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/can.ps1 +0 -0
  23. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/check-qoder-mcp.ps1 +0 -0
  24. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/common/build-backends.psm1 +0 -0
  25. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/common/fw.psm1 +0 -0
  26. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/common/uart_probe.py +0 -0
  27. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/flash.ps1 +0 -0
  28. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/instrument_cli.py +0 -0
  29. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/lib/__init__.py +0 -0
  30. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/lib/instruments.py +0 -0
  31. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/lib/serial_assistant.py +0 -0
  32. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/logic_capture.ps1 +0 -0
  33. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/logic_decode.ps1 +0 -0
  34. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/reset.ps1 +0 -0
  35. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/sigrok_csv_to_fwloop.py +0 -0
  36. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/test-backends.ps1 +0 -0
  37. {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/test.ps1 +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)
5
5
  Author-email: WeilinZhang <19523330249@163.com>
6
6
  License: MIT
@@ -382,7 +382,7 @@ claude mcp remove --scope user agentic-hil
382
382
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
383
383
  ├── demo-make/ 示例固件(Make;构建测试载体)
384
384
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
385
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.6.0)
385
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.6.1)
386
386
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
387
387
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
388
388
  ├── CLAUDE.md Claude Code CLI 指南
@@ -392,7 +392,7 @@ claude mcp remove --scope user agentic-hil
392
392
 
393
393
  ---
394
394
 
395
- ## 能力验证状态(v0.6.0
395
+ ## 能力验证状态(v0.6.1
396
396
 
397
397
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
398
398
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -354,7 +354,7 @@ claude mcp remove --scope user agentic-hil
354
354
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
355
355
  ├── demo-make/ 示例固件(Make;构建测试载体)
356
356
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
357
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.6.0)
357
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.6.1)
358
358
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
359
359
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
360
360
  ├── CLAUDE.md Claude Code CLI 指南
@@ -364,7 +364,7 @@ claude mcp remove --scope user agentic-hil
364
364
 
365
365
  ---
366
366
 
367
- ## 能力验证状态(v0.6.0
367
+ ## 能力验证状态(v0.6.1
368
368
 
369
369
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
370
370
  > `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: firmwareloop
3
- Version: 0.6.0
3
+ Version: 0.6.1
4
4
  Summary: AI Agent Firmware Development and Lab Automation Platform (Dual-Tier MCP Server)
5
5
  Author-email: WeilinZhang <19523330249@163.com>
6
6
  License: MIT
@@ -382,7 +382,7 @@ claude mcp remove --scope user agentic-hil
382
382
  ├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
383
383
  ├── demo-make/ 示例固件(Make;构建测试载体)
384
384
  ├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
385
- ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.6.0)
385
+ ├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.6.1)
386
386
  ├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
387
387
  ├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
388
388
  ├── CLAUDE.md Claude Code CLI 指南
@@ -392,7 +392,7 @@ claude mcp remove --scope user agentic-hil
392
392
 
393
393
  ---
394
394
 
395
- ## 能力验证状态(v0.6.0
395
+ ## 能力验证状态(v0.6.1
396
396
 
397
397
  > 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
398
398
  > `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.6.0"
7
+ version = "0.6.1"
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.6.0"
5
+ __version__ = "0.6.1"
@@ -129,28 +129,110 @@ Add-FwCheck 'pyvisa' {
129
129
  }
130
130
 
131
131
  # COM ports present (UART candidates) - informational, never hardcoded.
132
- # Prefer Agentic HIL's enumeration (hardware-aware) when installed, else WMI.
132
+ # P2-1: agentic-hil 优先, 兜底改用 pyserial comports (SetupAPI, 覆盖 CH341/CP210x/
133
+ # FTDI 等 USB 适配器; Win32_SerialPort 在 Windows 上常漏报 USB CDC/厂商适配器,
134
+ # 实战漏掉真实 DUT 口 COM3 (USB-SERIAL CH341) 导致 agent 误开蓝牙虚拟口)。
135
+ # 输出带 description 并对蓝牙/虚拟口打标记排序 (真实口在前, 防 agent 首选错口)。
133
136
  Add-FwCheck 'com_ports' {
134
137
  $comPorts = @()
138
+ $portDetails = @()
139
+ $source = 'none'
135
140
  $ahil = if ($venvBin) { Join-Path $venvBin 'agentic-hil.exe' } else { $null }
136
141
  if ($ahil -and (Test-Path -LiteralPath $ahil)) {
137
142
  try {
138
143
  $raw = & $ahil com-ports 2>$null | Out-String
139
144
  $parsed = $raw | ConvertFrom-Json -ErrorAction SilentlyContinue
140
145
  if ($parsed -and $parsed.ports) {
141
- $comPorts = @($parsed.ports | ForEach-Object { $_.device } | Where-Object { $_ })
146
+ $portDetails = @($parsed.ports | ForEach-Object {
147
+ [pscustomobject]@{ device = [string]$_.device; description = [string]$_.description }
148
+ } | Where-Object { $_.device })
149
+ $comPorts = @($portDetails | ForEach-Object { $_.device })
150
+ $source = 'agentic-hil'
151
+ }
152
+ } catch { }
153
+ }
154
+ if ($comPorts.Count -eq 0 -and $python) {
155
+ try {
156
+ $pyCode = "import json; from serial.tools.list_ports import comports; print(json.dumps([{'device': p.device, 'description': (p.description or '')} for p in comports()]))"
157
+ $raw = & $python -c $pyCode 2>$null | Out-String
158
+ $parsed = $raw | ConvertFrom-Json -ErrorAction SilentlyContinue
159
+ if ($parsed) {
160
+ $portDetails = @($parsed | ForEach-Object {
161
+ [pscustomobject]@{ device = [string]$_.device; description = [string]$_.description }
162
+ } | Where-Object { $_.device })
163
+ $comPorts = @($portDetails | ForEach-Object { $_.device })
164
+ $source = 'pyserial'
142
165
  }
143
166
  } catch { }
144
167
  }
145
168
  if ($comPorts.Count -eq 0) {
146
169
  try {
147
170
  $comPorts = @((Get-CimInstance Win32_SerialPort -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DeviceID))
171
+ $source = 'wmi'
148
172
  } catch { }
149
173
  }
174
+ # 虚拟/蓝牙口标记 + 真实口优先排序
175
+ $isVirtual = { param($d) $d -match '(?i)bluetooth|蓝牙|virtual|modem|debug port' }
176
+ $real = @($portDetails | Where-Object { -not (& $isVirtual $_.description) })
177
+ $virt = @($portDetails | Where-Object { & $isVirtual $_.description })
178
+ $ordered = @(@($real | ForEach-Object { $_.device }) + @($virt | ForEach-Object { $_.device }))
179
+ if ($ordered.Count -gt 0) { $comPorts = $ordered }
180
+ [pscustomobject]@{
181
+ status = if ($comPorts.Count -gt 0) { 'ok' } else { 'none' }
182
+ source = $source
183
+ ports = $comPorts
184
+ port_details = @($portDetails)
185
+ virtual_ports = @($virt | ForEach-Object { $_.device })
186
+ note = '排序: 真实串口在前; 蓝牙/虚拟口仅供参考 (无法访问 DUT)'
187
+ }
188
+ }
189
+
190
+ # P1-4: lab.yaml 配置链透明度——枚举全局/项目两层 ndtdbg 段存在性与
191
+ # write_allowed 状态; 任何层 write_allowed: true 输出 WARNING
192
+ # (global 层常驻 true 属危险默认; 项目层 false 不代表安全——合并链会回退全局层)。
193
+ Add-FwCheck 'lab_yaml' {
194
+ if (-not $python) {
195
+ return [pscustomobject]@{ status = 'missing'; detail = 'no python' }
196
+ }
197
+ $pyCode = @'
198
+ import json, os
199
+ try:
200
+ import yaml
201
+ except ImportError:
202
+ yaml = None
203
+ def sec(p):
204
+ try:
205
+ with open(p, encoding="utf-8") as fh:
206
+ d = (yaml.safe_load(fh) if yaml else json.load(fh)) or {}
207
+ except Exception:
208
+ return {"parse_error": True}
209
+ s = d.get("ndtdbg") if isinstance(d, dict) else None
210
+ return s if isinstance(s, dict) else {}
211
+ out = []
212
+ ad = os.environ.get("APPDATA")
213
+ if ad:
214
+ p = os.path.join(ad, "FirmwareLoop", "lab.yaml")
215
+ n = sec(p)
216
+ out.append({"layer": "global", "path": p, "exists": os.path.isfile(p),
217
+ "ndtdbg": bool(n), "write_allowed": n.get("write_allowed")})
218
+ pp = os.path.join(os.getcwd(), "lab", "lab.yaml")
219
+ n2 = sec(pp)
220
+ out.append({"layer": "project", "path": pp, "exists": os.path.isfile(pp),
221
+ "ndtdbg": bool(n2), "write_allowed": n2.get("write_allowed")})
222
+ print(json.dumps(out))
223
+ '@
224
+ $raw = & $python -c $pyCode 2>$null | Out-String
225
+ $layers = @($raw | ConvertFrom-Json -ErrorAction SilentlyContinue)
226
+ $warned = @($layers | Where-Object { $_.write_allowed -eq $true })
150
227
  [pscustomobject]@{
151
- status = if ($comPorts.Count -gt 0) { 'ok' } else { 'none' }
152
- source = if ($ahil -and (Test-Path -LiteralPath $ahil)) { 'agentic-hil' } else { 'wmi' }
153
- ports = $comPorts
228
+ status = if ($warned.Count -gt 0) { 'warn' } else { 'ok' }
229
+ detail = if ($warned.Count -gt 0) {
230
+ ($warned | ForEach-Object {
231
+ $tip = if ($_.layer -eq 'global') { '常驻 true 属危险默认 (会话级临时态, 用完即关)' } else { '项目层开启须用完即关' }
232
+ "$($_.layer) 层 write_allowed=true ($tip)"
233
+ }) -join '; '
234
+ } else { '任何层均未开启 write_allowed (写保护生效)' }
235
+ layers = @($layers)
154
236
  }
155
237
  }
156
238
 
@@ -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.6.0).
3
+ fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.6.1).
4
4
 
5
5
  Exposes high-level firmware engineering and hardware management tools to AI Coding
6
6
  Agents (Antigravity CLI, Claude Code CLI, Qoder IDE, CodeBuddy IDE, Cursor) via the Model Context
@@ -329,7 +329,11 @@ def handle_fw_ndtdbg_write_reg(args: Dict[str, Any]) -> Dict[str, Any]:
329
329
 
330
330
 
331
331
  def handle_fw_ndtdbg_flash(args: Dict[str, Any]) -> Dict[str, Any]:
332
- """Erase + flash a .bin/.nfw image onto the NdtBox target chip (ChipSea Fxx) over IIC."""
332
+ """Erase + flash a .bin/.nfw image onto the NdtBox target chip (ChipSea Fxx) over IIC.
333
+
334
+ P1-1: retry 参数 (默认 1) —— 命中 "TARGET_MISMATCH @0x0000" (未进 boot 的特征
335
+ 签名: 校验读到旧向量表) 自动重试; BOOT_NOT_ENTERED 不重试 (需人工断电, 直接给指引)。
336
+ """
333
337
  cli_script = os.path.join(REPO_ROOT, "tools", "ndtdbg_cli.py")
334
338
  cmd = [PYTHON_EXE, cli_script, "flash-bin", "--file", str(args.get("file_path")),
335
339
  "--erase-ms", str(int(args.get("erase_ms", 2000)))]
@@ -337,7 +341,25 @@ def handle_fw_ndtdbg_flash(args: Dict[str, Any]) -> Dict[str, Any]:
337
341
  cmd.append("--confirm")
338
342
  _ndtdbg_reg_args(args, cmd)
339
343
  timeout_s = min(max(int(args.get("timeout_seconds", 300)), 30), 900)
340
- return _ndtdbg_run(cmd, timeout_s)
344
+ retry = max(0, int(args.get("retry", 1)))
345
+ result: Dict[str, Any] = {}
346
+ for attempt in range(retry + 1):
347
+ result = _ndtdbg_run(cmd, timeout_s)
348
+ data = result.get("data") if isinstance(result.get("data"), dict) else None
349
+ if not data:
350
+ return result # 无 CLI JSON body: 原样返回 (兼容 mock/异常输出)
351
+ if data.get("ok"):
352
+ if attempt:
353
+ data["retry_count"] = attempt
354
+ return data
355
+ err_text = f"{data.get('error', '')} {data.get('detail', '')}"
356
+ err_cls = data.get("error_class")
357
+ if err_cls == "BOOT_NOT_ENTERED":
358
+ return data # 需人工断电重启, 重试无意义
359
+ if err_cls == "TARGET_MISMATCH" and "@0x0000" in err_text and attempt < retry:
360
+ continue # 未进 boot 特征签名: 自动重试
361
+ return data
362
+ return result
341
363
 
342
364
 
343
365
  def _stream_cli_cmd(args: Dict[str, Any], out_path: str, duration_cap: float) -> Optional[list]:
@@ -694,36 +716,171 @@ def handle_fw_get_evidence(args: Dict[str, Any]) -> Dict[str, Any]:
694
716
  }
695
717
 
696
718
 
719
+ def _render_fwloop_tools_markdown(indent: str = " ") -> str:
720
+ """P1-5: AGENTS.md/SKILL.md 工具清单从 TOOLS_REGISTRY 自动渲染 (消除手写漂移)。
721
+ 渲染条数恒等于注册表条数 (当前 30), 与 tools/list 完全一致。"""
722
+ lines = []
723
+ for name, info in TOOLS_REGISTRY.items():
724
+ desc = (info.get("description") or "").strip()
725
+ first = desc.split(". ")[0].strip().rstrip(".")
726
+ lines.append(f"{indent}- `{name}()`: {first}.")
727
+ return "\n".join(lines)
728
+
729
+
730
+ def _detect_hardware() -> Dict[str, Any]:
731
+ """P1-5: init 硬件探测 (全部 best-effort, 探测不到不猜——fail-closed)。
732
+
733
+ - COM 口: pyserial comports (SetupAPI, 带 description, CH341/CP210x/FTDI 全覆盖);
734
+ - NdtBox USB: hidapi 枚举 WCH(0x1A86) 厂商设备 (agentic-hil 依赖链自带 hidapi);
735
+ - NdtDebugTool 路径: 常见位置 (用户根/Desktop/Downloads/C:\\/D:\\ 1-2 层)
736
+ 扫描 NdtDebugTool*/lib/USBIOX.DLL, 命中即预填 ndtdbg.dll_dir。
737
+ """
738
+ detected: Dict[str, Any] = {"com_ports": [], "ndtbox_hid": None,
739
+ "ndtdbg_dll_dir": None, "notes": []}
740
+ try:
741
+ from serial.tools.list_ports import comports
742
+ detected["com_ports"] = [{"device": p.device, "description": (p.description or "")}
743
+ for p in comports()]
744
+ except Exception as exc: # noqa: BLE001
745
+ detected["notes"].append(f"pyserial COM 枚举不可用: {exc}")
746
+ try:
747
+ import hid
748
+ for d in hid.enumerate(vendor_id=0x1A86): # WCH (CH341) 厂商 ID
749
+ prod = str(d.get("product") or "")
750
+ if "CH341" in prod.upper() or "NDT" in prod.upper():
751
+ detected["ndtbox_hid"] = {"vendor_id": "0x1A86", "product": prod,
752
+ "serial": d.get("serial_number")}
753
+ break
754
+ except Exception: # noqa: BLE001
755
+ detected["notes"].append("hidapi 不可用, 跳过 NdtBox USB 枚举 (不影响其余探测)")
756
+ import glob as _glob
757
+ cands: List[str] = []
758
+ roots: List[str] = []
759
+ profile = os.environ.get("USERPROFILE")
760
+ if profile:
761
+ roots += [profile, os.path.join(profile, "Desktop"), os.path.join(profile, "Downloads")]
762
+ roots += ["C:\\", "D:\\"]
763
+ for root in roots:
764
+ for pat in (os.path.join(root, "NdtDebugTool*", "lib", "USBIOX.DLL"),
765
+ os.path.join(root, "*", "NdtDebugTool*", "lib", "USBIOX.DLL")):
766
+ try:
767
+ cands += _glob.glob(pat)
768
+ except Exception: # noqa: BLE001
769
+ pass
770
+ if cands:
771
+ detected["ndtdbg_dll_dir"] = os.path.dirname(os.path.abspath(cands[0]))
772
+ else:
773
+ detected["notes"].append(
774
+ "常见位置未发现 NdtDebugTool*/lib/USBIOX.DLL——ndtdbg.dll_dir 需人工提供"
775
+ " (运行 fwloop ndtdebugtool \"<NdtDebugTool 根目录>\" 写入)")
776
+ return detected
777
+
778
+
779
+ def _build_lab_yaml_from_example(target_dir: str, target_chip: str, build_backend: str,
780
+ detected_dll_dir: Optional[str]) -> str:
781
+ """P1-5: lab/lab.example.yaml 为唯一模板 (example 加段 init 自动跟上);
782
+ 仅替换 project.name/target_chip/build_backend/source_dir 与 dut.uart.port
783
+ (留空 + 注释 "用 fw_scan_hardware 探测", 去掉硬编码 COM3);
784
+ ndtdbg.dll_dir 按探测结果预填, 探测不到保持注释态 + 人工指引。"""
785
+ example_path = os.path.join(REPO_ROOT, "lab", "lab.example.yaml")
786
+ with open(example_path, encoding="utf-8") as fh:
787
+ lines = fh.read().splitlines()
788
+ out: List[str] = []
789
+ in_project = False
790
+ for ln in lines:
791
+ if ln.startswith("project:"):
792
+ in_project = True
793
+ out.append(ln)
794
+ continue
795
+ if in_project:
796
+ if ln.startswith(" "):
797
+ stripped = ln.lstrip()
798
+ if stripped.startswith("name:"):
799
+ out.append(f' name: "{os.path.basename(target_dir)}"')
800
+ out.append(f' target_chip: "{target_chip}"')
801
+ elif stripped.startswith("build_backend:"):
802
+ out.append(f' build_backend: "{build_backend}"')
803
+ elif stripped.startswith("source_dir:"):
804
+ out.append(' source_dir: "."')
805
+ else:
806
+ out.append(ln)
807
+ continue
808
+ in_project = False
809
+ if ln.startswith(" port: COM5"):
810
+ out.append(" port: null # 用 fw_scan_hardware 探测后填入 (勿猜)")
811
+ continue
812
+ if ln.startswith(' dll_dir: "C:/path/to/NdtDebugTool/lib"'):
813
+ if detected_dll_dir:
814
+ # YAML 双引号标量内反斜杠是转义字符, 统一正斜杠
815
+ posix = str(detected_dll_dir).replace("\\", "/")
816
+ out.append(f' dll_dir: "{posix}" # init 探测预填, 请核对')
817
+ else:
818
+ out.append(' dll_dir: null # TODO: 需人工提供 (fwloop ndtdebugtool "<NdtDebugTool 根目录>")')
819
+ continue
820
+ out.append(ln)
821
+ return "\n".join(out) + "\n"
822
+
823
+
824
+ def _upgrade_lab_yaml(existing_path: str) -> List[str]:
825
+ """P1-5 upgrade: 非破坏合并——补齐 example 有而项目缺的顶层段与段内缺失键,
826
+ 已有键一律不动 (治本: Demo_F71_4CH 这类旧项目停在旧形态的问题)。
827
+ 返回新增段/键清单 (无新增返回空表, 文件不动)。"""
828
+ import yaml as _yaml
829
+ with open(existing_path, encoding="utf-8") as fh:
830
+ existing = _yaml.safe_load(fh) or {}
831
+ example_path = os.path.join(REPO_ROOT, "lab", "lab.example.yaml")
832
+ with open(example_path, encoding="utf-8") as fh:
833
+ example = _yaml.safe_load(fh) or {}
834
+ added: List[str] = []
835
+ for k, v in example.items():
836
+ if k not in existing:
837
+ existing[k] = v
838
+ added.append(k)
839
+ elif isinstance(v, dict) and isinstance(existing.get(k), dict):
840
+ for sk, sv in v.items():
841
+ if sk not in existing[k]:
842
+ existing[k][sk] = sv
843
+ added.append(f"{k}.{sk}")
844
+ if added:
845
+ body = _yaml.safe_dump(existing, allow_unicode=True, sort_keys=False,
846
+ default_flow_style=False)
847
+ with open(existing_path, "w", encoding="utf-8") as fh:
848
+ fh.write("# FirmwareLoop lab.yaml (经 fwloop init upgrade 合并: 新增段/键取自\n")
849
+ fh.write("# lab.example.yaml 模板, 值请人工确认; 既有键保持原值未动)\n")
850
+ fh.write(body)
851
+ return added
852
+
853
+
697
854
  def handle_fw_init_project(args: Dict[str, Any]) -> Dict[str, Any]:
698
855
  """Scaffold multi-agent instruction files (AGENTS.md, CLAUDE.md, GEMINI.md), lab.yaml, and .mcp.json."""
699
856
  target_dir = args.get("target_dir", ".")
700
857
  target_dir = os.path.abspath(target_dir)
701
858
  os.makedirs(target_dir, exist_ok=True)
702
- target_chip = args.get("target_chip", "STM32F103C8")
859
+ target_chip = args.get("target_chip")
703
860
  build_backend = args.get("build_backend", "keil")
704
861
  overwrite = args.get("overwrite", False)
862
+ upgrade = args.get("upgrade", False)
863
+ # P1-5: target_chip 必填 (fail-closed), 不再硬编码默认芯片
864
+ if not target_chip:
865
+ return {
866
+ "ok": False,
867
+ "error_class": "CONFIG_ERROR",
868
+ "error": "target_chip 必填 (如 STM32F103C8 / CSA37F71)",
869
+ "detail": "fwloop init <target_chip> 或 MCP fw_init_project(target_chip=...)",
870
+ }
705
871
 
706
872
  created_files = []
873
+ detected = _detect_hardware()
707
874
 
708
875
  # 1. AGENTS.md
709
876
  agents_path = os.path.join(target_dir, "AGENTS.md")
710
- agents_content = """# AGENTS.md — Firmware Engineering & Lab Automation Guidelines
877
+ agents_content = f"""# AGENTS.md — Firmware Engineering & Lab Automation Guidelines
711
878
 
712
879
  This repository is configured with **FirmwareLoop (`fwloop`)** for AI Agent-driven firmware development, building, flashing, and hardware-in-the-loop (HIL) testing.
713
880
 
714
881
  ## Dual-Tier MCP Tools Available
715
- - **Upper-Tier (`fwloop`)**:
716
- - `fw_doctor()`: Check compiler, Python, COM ports, and probe connectivity.
717
- - `fw_configure_lab(project_name, build_backend, target_chip, uart_port)`: Interactively configure project settings in `lab/lab.yaml`.
718
- - `fw_scan_hardware(adopt=false)`: Scan connected ST-LINK / J-Link / CMSIS-DAP probes and COM ports.
719
- - `fw_build(backend="auto", configuration="Debug", clean=true)`: Build firmware across Keil5, CMake, Make, IAR, PlatformIO, etc.
720
- - `fw_flash(backend="auto", artifact_path)`: Flash compiled firmware image into target MCU via ST-LINK or J-Link.
721
- - `fw_reset(backend="auto")`: Hardware/software reset target MCU.
722
- - `fw_run_hil_test()`: Run pytest automated hardware-in-the-loop test suite.
723
- - `fw_measure(instrument_type, command)`: Safe PyVISA instrument measurement.
724
- - `fw_logic_capture(protocol="i2c|spi|uart")` / `fw_logic_decode()`: Logic analyzer protocol capture & decode.
725
- - `fw_get_evidence()`: Retrieve audit run artifacts and test evidence.
726
- - `fw_init_project()`: Scaffold multi-agent instruction files and lab configurations.
882
+ - **Upper-Tier (`fwloop`, {len(TOOLS_REGISTRY)} tools, auto-generated from MCP registry)**:
883
+ {_render_fwloop_tools_markdown(indent=' ')}
727
884
 
728
885
  - **Lower-Tier (`agentic-hil`)**:
729
886
  - `probe_target()`, `flash_firmware()`, `reset_target()`, `com_session_*()`, `debug_*()`.
@@ -755,29 +912,19 @@ This repository is configured with **FirmwareLoop (`fwloop`)** for AI Agent-driv
755
912
  fh.write(gemini_content)
756
913
  created_files.append("GEMINI.md")
757
914
 
758
- # 4. lab/lab.yaml
915
+ # 4. lab/lab.yaml (P1-5: lab.example.yaml 为唯一模板 + 硬件探测预填 + upgrade 合并)
759
916
  lab_dir = os.path.join(target_dir, "lab")
760
917
  os.makedirs(lab_dir, exist_ok=True)
761
918
  lab_yaml_path = os.path.join(lab_dir, "lab.yaml")
762
- lab_yaml_content = f"""# FirmwareLoop Project & Hardware Bench Configuration
763
- schema: "firmwareloop-lab-config/v1"
764
-
765
- project:
766
- name: "{os.path.basename(target_dir)}"
767
- target_chip: "{target_chip}"
768
- build_backend: "{build_backend}"
769
- source_dir: "."
770
-
771
- hardware:
772
- debugger_probe: "stlink"
773
- probe_serial: "auto"
774
- uart:
775
- port: "COM3"
776
- baudrate: 115200
777
- power_supply:
778
- default_voltage: 3.3
779
- """
780
- if not os.path.exists(lab_yaml_path) or overwrite:
919
+ lab_upgraded: List[str] = []
920
+ if os.path.exists(lab_yaml_path):
921
+ if upgrade:
922
+ lab_upgraded = _upgrade_lab_yaml(lab_yaml_path)
923
+ created_files.append("lab/lab.yaml (upgrade)")
924
+ # 既无 upgrade 也不 overwrite: 保留用户已有配置不动
925
+ else:
926
+ lab_yaml_content = _build_lab_yaml_from_example(
927
+ target_dir, str(target_chip), str(build_backend), detected.get("ndtdbg_dll_dir"))
781
928
  with open(lab_yaml_path, "w", encoding="utf-8") as fh:
782
929
  fh.write(lab_yaml_content)
783
930
  created_files.append("lab/lab.yaml")
@@ -805,7 +952,7 @@ hardware:
805
952
  skill_dir = os.path.join(target_dir, "skills", "firmwareloop")
806
953
  os.makedirs(skill_dir, exist_ok=True)
807
954
  skill_path = os.path.join(skill_dir, "SKILL.md")
808
- skill_content = """---
955
+ skill_content = f"""---
809
956
  name: firmwareloop
810
957
  description: FirmwareLoop workflow skill for orchestrating firmware builds (Keil/CMake/Make/etc.), probe detection (ST-LINK/J-Link), interactive lab configuration, pytest HIL testing, I2C/SPI logic analyzer captures, PyVISA instrument measurements, and end-to-end hardware acceptance.
811
958
  ---
@@ -816,19 +963,8 @@ Use this skill when developing, building, testing, or diagnosing embedded firmwa
816
963
 
817
964
  ## Capabilities & Tools
818
965
 
819
- ### Upper-Tier MCP Tools (`firmwareloop`)
820
- - `fw_doctor`: Run environmental health check.
821
- - `fw_configure_lab`: Interactively configure or update project parameters, target chip (e.g. STM32F103C8), build backend (e.g. keil), source directory, COM port, and debugger probe in `lab/lab.yaml`.
822
- - `fw_scan_hardware`: Scan and identify attached hardware debuggers (ST-LINK, J-Link, CMSIS-DAP) and COM ports.
823
- - `fw_build`: Compile firmware across 7 backends (Keil, CMake, Make, IAR, PlatformIO, Zephyr, ESP-IDF).
824
- - `fw_flash`: Flash compiled firmware image into target MCU.
825
- - `fw_reset`: Hardware or software reset of the target MCU.
826
- - `fw_run_hil_test`: Run 12-item pytest HIL automated testing.
827
- - `fw_acceptance_scenario`: Execute end-to-end acceptance scenario.
828
- - `fw_measure`: Query PyVISA/SCPI instrument readings (frequency, duty cycle, Vpp, voltage, current).
829
- - `fw_logic_capture` / `fw_logic_decode`: Digital protocol capture and verification (I2C, SPI, UART).
830
- - `fw_get_evidence`: Retrieve audit run artifacts and reports.
831
- - `fw_init_project`: Scaffold multi-agent instruction files and lab configurations.
966
+ ### Upper-Tier MCP Tools (`firmwareloop`, {len(TOOLS_REGISTRY)} tools, auto-generated from MCP registry)
967
+ {_render_fwloop_tools_markdown(indent='')}
832
968
 
833
969
  ## Mandatory Rules
834
970
  1. **Real-Hardware-First & Zero Fake Data**: Never fake or mock hardware results. When compilers, debuggers, or MCU targets are missing, fail closed immediately and output explicit error diagnostics and user setup guidance.
@@ -994,6 +1130,8 @@ custom_tools:
994
1130
  "ok": True,
995
1131
  "target_dir": target_dir,
996
1132
  "created_files": created_files,
1133
+ "detected": detected,
1134
+ "lab_upgrade_added": lab_upgraded,
997
1135
  "message": f"Successfully initialized FirmwareLoop multi-agent files in {target_dir}"
998
1136
  }
999
1137
 
@@ -1188,7 +1326,9 @@ def handle_fw_serial_send(args: Dict[str, Any]) -> Dict[str, Any]:
1188
1326
 
1189
1327
  def handle_fw_serial_read(args: Dict[str, Any]) -> Dict[str, Any]:
1190
1328
  cmd = [PYTHON_EXE, SERIAL_CLI, "read", "--format", str(args.get("format", "both"))]
1191
- if args.get("max_bytes"):
1329
+ if args.get("drain"):
1330
+ cmd.append("--drain")
1331
+ elif args.get("max_bytes"):
1192
1332
  cmd.extend(["--max-bytes", str(args["max_bytes"])])
1193
1333
  timeout_ms = int(args.get("timeout_ms", 2000))
1194
1334
  if args.get("expect"):
@@ -1619,6 +1759,11 @@ TOOLS_REGISTRY = {
1619
1759
  "description": "CLI process timeout; budget ~30s per 64KB image (page writes paced at 30ms)",
1620
1760
  "default": 300,
1621
1761
  },
1762
+ "retry": {
1763
+ "type": "integer",
1764
+ "description": "Auto-retries on TARGET_MISMATCH @0x0000 signature (target did not enter boot; verify read old vector table). BOOT_NOT_ENTERED never retries (manual power-cycle required). Result carries retry_count when a retry succeeded",
1765
+ "default": 1,
1766
+ },
1622
1767
  "dll_dir": {
1623
1768
  "type": "string",
1624
1769
  "description": "NdtDebugTool lib directory containing USBIOX.DLL and usb2io.dll; default from lab.yaml ndtdbg.dll_dir",
@@ -1871,7 +2016,7 @@ TOOLS_REGISTRY = {
1871
2016
  "handler": handle_fw_get_evidence,
1872
2017
  },
1873
2018
  "fw_init_project": {
1874
- "description": "Scaffold multi-agent instruction files (AGENTS.md, CLAUDE.md, GEMINI.md), lab.yaml bench config, and .mcp.json in the current or specified firmware project directory.",
2019
+ "description": "Scaffold multi-agent instruction files (AGENTS.md, CLAUDE.md, GEMINI.md), lab.yaml bench config, and .mcp.json in the current or specified firmware project directory. lab.yaml is generated from lab.example.yaml (single template source) with hardware detection (COM ports / NdtBox HID / NdtDebugTool location) prefilling the ndtdbg section; upgrade=true non-destructively merges missing sections/keys into an existing lab.yaml.",
1875
2020
  "inputSchema": {
1876
2021
  "type": "object",
1877
2022
  "properties": {
@@ -1882,8 +2027,7 @@ TOOLS_REGISTRY = {
1882
2027
  },
1883
2028
  "target_chip": {
1884
2029
  "type": "string",
1885
- "description": "Target MCU chip model (e.g. STM32F103C8, STM32F407ZG)",
1886
- "default": "STM32F103C8",
2030
+ "description": "Target MCU chip model (e.g. STM32F103C8, CSA37F71) - REQUIRED, no hardcoded default",
1887
2031
  },
1888
2032
  "build_backend": {
1889
2033
  "type": "string",
@@ -1896,7 +2040,13 @@ TOOLS_REGISTRY = {
1896
2040
  "description": "Whether to overwrite existing instruction files if present",
1897
2041
  "default": False,
1898
2042
  },
2043
+ "upgrade": {
2044
+ "type": "boolean",
2045
+ "description": "Non-destructively merge missing sections/keys from lab.example.yaml into an EXISTING lab/lab.yaml (existing keys are never touched; added items come from the template and need manual confirmation)",
2046
+ "default": False,
2047
+ },
1899
2048
  },
2049
+ "required": ["target_chip"],
1900
2050
  "additionalProperties": False,
1901
2051
  },
1902
2052
  "handler": handle_fw_init_project,
@@ -1981,7 +2131,12 @@ TOOLS_REGISTRY = {
1981
2131
  "inputSchema": {
1982
2132
  "type": "object",
1983
2133
  "properties": {
1984
- "max_bytes": {"type": "integer", "description": "Max bytes to drain (default 65536)", "default": 65536},
2134
+ "drain": {
2135
+ "type": "boolean",
2136
+ "description": "Drain the ENTIRE buffer in one call (>64KB payloads are written to a .hex file and only a summary + tail preview is returned, protecting the agent context from large dumps). Preferred over max_bytes when you just need everything received so far",
2137
+ "default": False,
2138
+ },
2139
+ "max_bytes": {"type": "integer", "description": "Max bytes to drain (default 65536; ignored when drain=true)", "default": 65536},
1985
2140
  "expect": {
1986
2141
  "type": "string",
1987
2142
  "description": "Wait until this pattern appears in the stream (text; use expect_hex for byte patterns)",
@@ -2081,7 +2236,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
2081
2236
  },
2082
2237
  "serverInfo": {
2083
2238
  "name": "firmwareloop",
2084
- "version": "0.6.0"
2239
+ "version": "0.6.1"
2085
2240
  }
2086
2241
  }
2087
2242
  }
@@ -2216,7 +2371,7 @@ def _settle_stashed_entrypoints(stashed) -> None:
2216
2371
  def handle_cli_update() -> int:
2217
2372
  """Handle `firmwareloop update` command."""
2218
2373
  print("============================================================")
2219
- print(" FirmwareLoop Auto-Updater (v0.6.0)")
2374
+ print(" FirmwareLoop Auto-Updater (v0.6.1)")
2220
2375
  print("============================================================")
2221
2376
 
2222
2377
  is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
@@ -2285,6 +2440,38 @@ def handle_cli_update() -> int:
2285
2440
  return res.returncode
2286
2441
 
2287
2442
 
2443
+ def _selftest_protocol_desync() -> tuple:
2444
+ """P0-1 回归检查 (selftest 1.5): echo/r 异构交替 20 次, 断言响应与请求一一对应。
2445
+
2446
+ 真机在环时执行实际检查; 无硬件/无桥环境返回 SKIP (不算 FAIL)。
2447
+ 返回 (status, detail)。"""
2448
+ try:
2449
+ import types as _types
2450
+ _tools_dir = os.path.join(REPO_ROOT, "tools")
2451
+ if _tools_dir not in sys.path:
2452
+ sys.path.insert(0, _tools_dir)
2453
+ from lib import ndtdbg as _ndbg
2454
+ import ndtdbg_cli as _cli
2455
+ _cfg = _cli.load_config(None)
2456
+ _io = _cli.resolve_io(_types.SimpleNamespace(dll_dir=None, slave_addr="0xA0",
2457
+ speed=1), _cfg)
2458
+ _io.timeout_ms = min(_io.timeout_ms, 5000)
2459
+ _sess = _ndbg.NdtBoxSession(_io) # open + echo 握手 (失败即 PROTOCOL_DESYNC)
2460
+ _mismatch = 0
2461
+ for _i in range(20):
2462
+ _tok = f"selftest{_i:02d}"
2463
+ _ok, _payload = _sess._txn(f"echo {_tok}")
2464
+ if not _ok or _payload != _tok:
2465
+ _mismatch += 1
2466
+ _sess.read_reg(0x10, 2) # 异构读 (内容任意, 只验响应行对齐)
2467
+ _sess.close()
2468
+ return (("PASS", f"echo/r 交替 20 次, 回显不匹配 {_mismatch} 次") if _mismatch == 0
2469
+ else ("FAIL", f"echo/r 交替 20 次, 回显不匹配 {_mismatch} 次 (协议错位!)"))
2470
+ except Exception as exc: # noqa: BLE001
2471
+ _cls = getattr(exc, "error_class", type(exc).__name__)
2472
+ return ("SKIP", f"需硬件在环: {_cls}: {str(exc)[:100]}")
2473
+
2474
+
2288
2475
  def handle_cli_selftest(target: str = "ndtdbg") -> int:
2289
2476
  """`fwloop selftest ndtdbg`: 按 BACKLOG 验收标准自动回归 (需硬件在环)。
2290
2477
 
@@ -2330,6 +2517,12 @@ def handle_cli_selftest(target: str = "ndtdbg") -> int:
2330
2517
  else:
2331
2518
  report("status", "FAIL", (body or {}).get("error_class", f"exit {rc}"))
2332
2519
 
2520
+ # 1.5 协议握手 + echo/r 异构交替 (P0-1 错位回归): 同构纯读流对错位免疫,
2521
+ # 必须含 echo(写型形态) 与 read(读型形态) 交替, 断言响应与请求一一对应。
2522
+ print("[1.5/4] echo/r 异构交替 20 次 (P0-1 错位回归) ...")
2523
+ _st, _detail = _selftest_protocol_desync()
2524
+ report("protocol.desync", _st, _detail)
2525
+
2333
2526
  # 2. stream 冒烟 (100ms x 10s)
2334
2527
  print("[2/4] stream 冒烟 (100ms x 10s, int16 解码) ...")
2335
2528
  rc, body = run_cli("stream", "--regs", "0xEE:8:ch", "--interval-ms", "100",
@@ -2609,7 +2802,7 @@ def set_ndtdbg_dlldir(dll_path: str) -> str:
2609
2802
  def handle_cli_setup() -> int:
2610
2803
  """Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, and CodeBuddy IDE."""
2611
2804
  print("============================================================")
2612
- print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.6.0)")
2805
+ print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.6.1)")
2613
2806
  print("============================================================")
2614
2807
  print("")
2615
2808
 
@@ -2731,31 +2924,43 @@ def handle_cli_setup() -> int:
2731
2924
  return 0
2732
2925
 
2733
2926
 
2734
- def handle_cli_init() -> int:
2735
- """Handle `firmwareloop init` command."""
2927
+ def handle_cli_init(chip: Optional[str] = None, upgrade: bool = False) -> int:
2928
+ """Handle `firmwareloop init [target_chip]` command."""
2736
2929
  print("============================================================")
2737
- print(" FirmwareLoop Multi-Agent Project Initializer (v0.6.0)")
2930
+ print(" FirmwareLoop Multi-Agent Project Initializer (v0.6.1)")
2738
2931
  print("============================================================")
2739
2932
  cwd = os.getcwd()
2740
2933
  print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
2741
- res = handle_fw_init_project({"target_dir": cwd, "overwrite": False})
2934
+ res = handle_fw_init_project({"target_dir": cwd, "overwrite": False,
2935
+ "target_chip": chip, "upgrade": upgrade})
2742
2936
  if res.get("ok"):
2743
2937
  for f in res.get("created_files", []):
2744
2938
  print(f" [+] Created: {f}")
2745
2939
  if not res.get("created_files"):
2746
2940
  print(" [*] All agent instruction files (AGENTS.md, CLAUDE.md, GEMINI.md, lab.yaml, .mcp.json) already exist.")
2941
+ det = res.get("detected") or {}
2942
+ if det.get("com_ports"):
2943
+ print(f" [i] Detected COM ports: {', '.join(p['device'] + (' (' + p['description'] + ')' if p.get('description') else '') for p in det['com_ports'])}")
2944
+ if det.get("ndtdbg_dll_dir"):
2945
+ print(f" [i] NdtDebugTool located: {det['ndtdbg_dll_dir']} (ndtdbg.dll_dir 已预填)")
2946
+ for n in det.get("notes", []):
2947
+ print(f" [!] {n}")
2948
+ if res.get("lab_upgrade_added"):
2949
+ print(f" [+] lab.yaml upgrade 补齐: {', '.join(res['lab_upgrade_added'])} (值请人工确认)")
2747
2950
  print("")
2748
2951
  print("============================================================")
2749
2952
  print("[+] Done! All AI Coding Agents (Claude Code, Qoder, Antigravity, CodeBuddy, Cursor) are now ready to operate in this repository.")
2750
2953
  print("============================================================")
2751
2954
  return 0
2752
2955
  else:
2753
- print(f"[-] Initialization failed: {res.get('message')}")
2956
+ print(f"[-] Initialization failed: {res.get('message') or res.get('error')}")
2957
+ if res.get("detail"):
2958
+ print(f" {res['detail']}")
2754
2959
  return 1
2755
2960
 
2756
2961
 
2757
2962
  def print_cli_help() -> None:
2758
- print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.6.0)
2963
+ print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.6.1)
2759
2964
 
2760
2965
  Usage:
2761
2966
  fwloop [command] (or: firmwareloop [command])
@@ -2781,7 +2986,10 @@ def main() -> None:
2781
2986
  if args:
2782
2987
  cmd = args[0].lower().strip()
2783
2988
  if cmd in ["init", "scaffold"]:
2784
- sys.exit(handle_cli_init())
2989
+ chip = args[1] if len(args) > 1 else None
2990
+ do_upgrade = any(a in ("-u", "--upgrade") for a in args[1:])
2991
+ chip = next((a for a in args[1:] if not a.startswith("-")), None)
2992
+ sys.exit(handle_cli_init(chip=chip, upgrade=do_upgrade))
2785
2993
  elif cmd in ["update", "upgrade"]:
2786
2994
  sys.exit(handle_cli_update())
2787
2995
  elif cmd in ["doctor", "check"]:
@@ -2798,7 +3006,7 @@ def main() -> None:
2798
3006
  sys.exit(2)
2799
3007
  sys.exit(handle_cli_ndtdebugtool(args[1]))
2800
3008
  elif cmd in ["version", "-v", "--version"]:
2801
- print("FirmwareLoop v0.6.0")
3009
+ print("FirmwareLoop v0.6.1")
2802
3010
  sys.exit(0)
2803
3011
  elif cmd in ["help", "-h", "--help"]:
2804
3012
  print_cli_help()
@@ -325,7 +325,7 @@ class NdtBoxIo:
325
325
  def session(self):
326
326
  """持久 serve 会话上下文: 块内的 read_reg/write_reg/raw_write_read/vbrt_reset
327
327
  复用同一 32 位桥进程与 USB 会话; 退出自动关闭。"""
328
- sess = NdtBoxSession(self)
328
+ sess = self._get_live_sess()
329
329
  self._persistent = sess
330
330
  try:
331
331
  yield sess
@@ -333,10 +333,23 @@ class NdtBoxIo:
333
333
  self._persistent = None
334
334
  sess.close()
335
335
 
336
+ def _get_live_sess(self):
337
+ """P2-2: 取活跃会话; 事务超时失效 (_dead) 的旧会话惰性重建——使 BUS_TIMEOUT
338
+ 文案 "请重试 (将自动重建会话)" 成真 (重建即新 spawn serve 桥, 含 echo 握手)。"""
339
+ if self._persistent is not None and getattr(self._persistent, "_dead", False):
340
+ try:
341
+ self._persistent.close()
342
+ except Exception:
343
+ pass
344
+ self._persistent = None
345
+ if self._persistent is None:
346
+ self._persistent = NdtBoxSession(self)
347
+ return self._persistent
348
+
336
349
  def _with_sess(self, fn):
337
- if self._persistent is not None:
338
- return fn(self._persistent)
339
- sess = NdtBoxSession(self)
350
+ sess = self._get_live_sess()
351
+ if self._persistent is sess:
352
+ return fn(sess)
340
353
  try:
341
354
  return fn(sess)
342
355
  finally:
@@ -596,26 +609,72 @@ class NdtBoxIo:
596
609
  read_gap = int(plan.get("read_gap_ms", 10))
597
610
  pages = plan.get("writes", [])
598
611
  windows = plan.get("windows", [])
612
+ t_all = time.monotonic()
599
613
  with self.session():
600
614
  self.vbrt_reset(off_ms) # 断电复位进 boot (usb2io IO10)
615
+ reset_ms = round((time.monotonic() - t_all) * 1000)
601
616
  time.sleep(0.1) # boot ready (UpdateFW Thread.Sleep(100))
617
+ # P1-1 boot-in 轮询: 读 0x0000 4B (boot ROM 应答; 运行态固件不应答, 读回
618
+ # 悬空 FF)。未进 boot 时重做一次 vbrt 再轮询; 仍失败立即报 BOOT_NOT_ENTERED
619
+ # (此前无轮询, 未进 boot 时 333 页写静默空转 ~20s, 最后报误导性的
620
+ # "校验失败 @0x0000"——实际读到的是旧固件内容; 实战 4 次烧录 3 次首试此败)
621
+ boot_t0 = time.monotonic()
622
+ boot_ok = False
623
+ for round_no in range(2):
624
+ if round_no:
625
+ self.vbrt_reset(off_ms)
626
+ time.sleep(0.1)
627
+ for _ in range(10):
628
+ try:
629
+ probe = self.raw_write_read(b"\x00\x00", 4)
630
+ except NdtBoxError as exc:
631
+ if exc.error_class == "BUS_TIMEOUT":
632
+ raise # clock-stretch 挂死: 上抛 (信封已带断电指引)
633
+ time.sleep(0.1)
634
+ continue
635
+ if probe and any(b != 0xFF for b in probe):
636
+ boot_ok = True # 有效应答 (非悬空默认电平)
637
+ break
638
+ time.sleep(0.1)
639
+ if boot_ok:
640
+ break
641
+ if not boot_ok:
642
+ raise NdtBoxError(
643
+ "BOOT_NOT_ENTERED",
644
+ "断电复位后目标未进入 boot (0x0000 读回悬空/无应答): 运行态固件不响应"
645
+ "烧录协议。请仅对目标板断电重启 (勿动 BOX), 然后重新调用 "
646
+ "fw_ndtdbg_flash; 若反复出现, 检查 usb2io IO10 供电复位接线")
647
+ boot_probe_ms = round((time.monotonic() - boot_t0) * 1000)
602
648
  # 整片擦除魔数 @0x0000 (EreaseChipCmd AA 55 A5 5A)
649
+ erase_t0 = time.monotonic()
603
650
  try:
604
651
  self.raw_write_read(bytes([0x00, 0x00, 0xAA, 0x55, 0xA5, 0x5A]), 0)
605
652
  except NdtBoxError as exc:
606
653
  if exc.error_class == "BUS_TIMEOUT":
607
- # P1-3: boot 等待显式化——把人工确认的搬运工步骤写进错误信封
608
654
  raise NdtBoxError(
609
655
  "BUS_TIMEOUT",
610
656
  "目标未进入 boot (擦除命令超时无应答)。请仅对目标板断电重启 (勿动 BOX), "
611
657
  "然后重新调用 fw_ndtdbg_flash", exc.detail) from exc
612
658
  raise
613
659
  time.sleep(erase_ms / 1000.0)
660
+ # P1-1 擦除金丝雀: 只验 "boot 还活着" (有应答即过——擦除后 0x0000 读回全 FF
661
+ # 属预期, 内容校验放校验阶段); 把失败从 20s 后的校验提前到擦除后 ~2s
662
+ try:
663
+ self.raw_write_read(b"\x00\x00", 4)
664
+ except NdtBoxError as exc:
665
+ raise NdtBoxError(
666
+ "BOOT_NOT_ENTERED",
667
+ f"擦除后 boot 无应答 (金丝雀失败): {exc.message}",
668
+ exc.detail) from exc
669
+ erase_ms_actual = round((time.monotonic() - erase_t0) * 1000)
670
+ write_t0 = time.monotonic()
614
671
  for page_hex in pages:
615
672
  self.raw_write_read(bytes.fromhex(page_hex), 0) # 页写 (2B 页地址+128B)
616
673
  if page_gap > 0:
617
674
  time.sleep(page_gap / 1000.0)
675
+ write_ms = round((time.monotonic() - write_t0) * 1000)
618
676
  time.sleep(0.2) # WriteFw 后至校验前 (Thread.Sleep(200))
677
+ verify_t0 = time.monotonic()
619
678
  for win in windows:
620
679
  off = int(win["off"])
621
680
  rb = self.raw_write_read(off.to_bytes(2, "big"), 256)
@@ -630,11 +689,16 @@ class NdtBoxIo:
630
689
  f"expect={expect[k:k + 16]} got={got[k:k + 16]}")
631
690
  if read_gap > 0:
632
691
  time.sleep(read_gap / 1000.0)
692
+ verify_ms = round((time.monotonic() - verify_t0) * 1000)
633
693
  # 完成魔数 "PASs" (UpgradeFinishCmd)
634
694
  self.raw_write_read(bytes([0x00, 0x00, 0x50, 0x41, 0x53, 0x73]), 0)
635
695
  time.sleep(0.01)
636
696
  self.vbrt_reset(off_ms) # 复位加载新固件
637
- return {"verify": "pass", "pages_written": len(pages), "read_windows": len(windows)}
697
+ # P3: 分阶段耗时 (事后分析首试失败率, 无需 job 化即可观测)
698
+ return {"verify": "pass", "pages_written": len(pages), "read_windows": len(windows),
699
+ "reset_ms": reset_ms, "boot_probe_ms": boot_probe_ms,
700
+ "erase_ms": erase_ms_actual, "write_ms": write_ms, "verify_ms": verify_ms,
701
+ "total_ms": round((time.monotonic() - t_all) * 1000)}
638
702
 
639
703
  def vbrt_reset(self, off_ms: int = 200) -> dict:
640
704
  """模组断电 off_ms 后恢复上电 (usb2io.dll IO10)——复位进入 IIC boot (IOoperation.VBRTOFF)。"""
@@ -642,6 +706,8 @@ class NdtBoxIo:
642
706
 
643
707
 
644
708
  class NdtBoxSession:
709
+ # 回显握手 token; None=每次随机 (生产)。单测注入固定值以便静态 mock 桥应答。
710
+ HANDSHAKE_TOKEN: str | None = None
645
711
  """serve 桥会话客户端: 32 位薄壳进程长驻 USB 会话, Python 经行协议驱动单事务。
646
712
 
647
713
  行协议 (全 ASCII): 请求 "open"/"r <reg> <len>"/"w <reg> <data_hex>"/
@@ -687,6 +753,23 @@ class NdtBoxSession:
687
753
  if not ok:
688
754
  self.close()
689
755
  raise self._map_error(payload)
756
+ # P0-1 回显握手: 桥多发/漏发一行响应会让整条会话永久 +1 错位 (v0.4.0~
757
+ # v0.6.0 实战暴露——同构纯读流对它免疫, 异构写读流直接腐坏)。open 后立即
758
+ # 以随机 token 验证"响应与请求一一对应", 把错位显式化为 PROTOCOL_DESYNC。
759
+ token = type(self).HANDSHAKE_TOKEN or os.urandom(4).hex()
760
+ try:
761
+ ok2, payload2 = self._txn(f"echo {token}", timeout_ms=5000)
762
+ except NdtBoxError as exc:
763
+ self.close()
764
+ raise NdtBoxError("PROTOCOL_DESYNC",
765
+ f"serve 会话握手失败: {exc.message}", exc.detail) from exc
766
+ if not ok2 or payload2.strip() != token:
767
+ self.close()
768
+ raise NdtBoxError(
769
+ "PROTOCOL_DESYNC",
770
+ f"serve 会话握手回显不匹配 (expect {token!r}, got {payload2[:64]!r})——"
771
+ "桥响应错位或 ndtdbg_bridge.ps1 版本过旧, 请升级 firmwareloop")
772
+ self._handshaken = True
690
773
 
691
774
  def _read_loop(self) -> None:
692
775
  try:
@@ -787,7 +870,16 @@ class NdtBoxSession:
787
870
  ok, payload = self._txn(f"raw {bytes(write).hex()} {read_len}")
788
871
  if not ok:
789
872
  raise self._map_error(payload, "raw 事务")
790
- return bytes.fromhex(payload.replace(" ", "")) if read_len > 0 else None
873
+ if read_len <= 0:
874
+ return None
875
+ try:
876
+ return bytes.fromhex(payload.replace(" ", ""))
877
+ except ValueError as exc:
878
+ # P1-2: 负载解析失败就地转结构化错误 (附负载原文), 不再裸抛 ValueError
879
+ # 被 CLI 顶层压成一行 UNKNOWN_ERROR (实战定位它花了 20 分钟)
880
+ raise NdtBoxError("PROTOCOL_ERROR",
881
+ f"serve 响应负载非法 hex: {exc}",
882
+ f"payload[:80]={payload[:80]!r}") from exc
791
883
 
792
884
  def vbrt_reset(self, off_ms: int = 200) -> dict:
793
885
  ok, payload = self._txn(f"vbrt {int(off_ms)}")
@@ -613,12 +613,16 @@ try {
613
613
  # 经行协议驱动。 deliberately 无格式化/无业务分支——规避 PS 语法陷阱 (0.2.1/0.2.2 坑)。
614
614
  # 行协议 (全 ASCII, 逐行 Flush):
615
615
  # -> open <- OK | ERR PROBE
616
+ # -> echo <token> <- OK <token> (回显, 握手防错位)
616
617
  # -> r <reg> <len> <- OK <hex 空格分隔> | ERR IO
617
618
  # -> w <reg> <data_hex 紧凑> <- OK | ERR IO
618
619
  # -> raw <write_hex> <read_len> <- OK <hex|-> | ERR IO
619
620
  # -> vbrt <off_ms> <- OK | ERR PROBE
620
621
  # -> close <- OK (桥退出)
621
622
  # 任何未知 op: ERR CONFIG
623
+ # P0-1: 打开成功后不再主动发 OK (该行无请求对应, 会让整条会话响应
624
+ # 永久 +1 错位——v0.4.0~v0.6.0 实战暴露); open 的应答由循环分支负责,
625
+ # 另提供 echo 回显供 Python 侧握手验证响应一一对应。
622
626
  [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
623
627
  $h = [STDataCollection.Icb]::USBIO_OpenDevice([uint32]0)
624
628
  if ($h -le 0) {
@@ -627,8 +631,6 @@ try {
627
631
  exit 1
628
632
  }
629
633
  [STDataCollection.Icb]::USBIO_SetStream([uint32]0, [uint32](128 + $Speed)) | Out-Null
630
- [Console]::WriteLine("OK")
631
- [Console]::Out.Flush()
632
634
  while ($true) {
633
635
  $line = [Console]::In.ReadLine()
634
636
  if ($null -eq $line) { break } # stdin EOF
@@ -639,6 +641,8 @@ try {
639
641
  try {
640
642
  if ($op -eq "open") {
641
643
  [Console]::WriteLine("OK")
644
+ } elseif ($op -eq "echo") {
645
+ [Console]::WriteLine("OK " + $parts[1])
642
646
  } elseif ($op -eq "r") {
643
647
  $reg = [int]$parts[1]; $rlen = [int]$parts[2]
644
648
  $tx = New-Object byte[] 2
@@ -40,12 +40,76 @@ def fail(error_class: str, message: str, detail=None) -> None:
40
40
 
41
41
  def ok(action: str, data: dict, log: str = None) -> None:
42
42
  payload = {"schema": "ndtdbg-result/v1", "ok": True, "action": action, "data": data}
43
+ try:
44
+ payload["config"] = config_provenance() # P1-4: 全部 ndtdbg 工具结果附配置溯源
45
+ except Exception: # noqa: BLE001
46
+ pass
43
47
  if log:
44
48
  payload["log"] = log
45
49
  print(json.dumps(payload, ensure_ascii=False))
46
50
  sys.exit(0)
47
51
 
48
52
 
53
+ # --------------------------------------------------------------------------- provenance (P1-4)
54
+
55
+ def _load_ndtdbg_section(path: str) -> dict | None:
56
+ """读取单份 lab.yaml 的 ndtdbg 段 (不存在/解析失败返回 None)。"""
57
+ try:
58
+ with open(path, encoding="utf-8") as fh:
59
+ data = (yaml.safe_load(fh) if yaml is not None else json.load(fh)) or {}
60
+ except Exception: # noqa: BLE001
61
+ return None
62
+ sec = data.get("ndtdbg") if isinstance(data, dict) else None
63
+ return sec if isinstance(sec, dict) else None
64
+
65
+
66
+ def config_provenance() -> dict:
67
+ """P1-4: lab.yaml 配置链溯源——三级深合并语义不变, 只加透明度。
68
+
69
+ - sources: 参与合并的 lab.yaml 路径 (高 -> 低, 复用 _CONFIG_FILES);
70
+ - ndtdbg_effective_from: ndtdbg 段实际来自的最高优先级文件;
71
+ - write_allowed_granted_by: write_allowed 授权来源 (write_allowed_source());
72
+ - overridden_keys: 项目层覆盖了低层取值的 ndtdbg 键;
73
+ - hint: 项目层缺 ndtdbg 段时的 onboarding 提示 (onboarding, 防止
74
+ "以为缺配置" 的无效排查——实战中真实问题是 serve 桥错位)。
75
+ """
76
+ chain = list(_CONFIG_FILES) # 高 -> 低
77
+ proj = os.path.join(os.getcwd(), "lab", "lab.yaml")
78
+ appdata = os.environ.get("APPDATA")
79
+ global_cfg = os.path.join(appdata, "FirmwareLoop", "lab.yaml") if appdata else None
80
+
81
+ nd_from = None
82
+ for cand in chain: # 高 -> 低, 第一份含 ndtdbg 段的即生效来源
83
+ if _load_ndtdbg_section(cand):
84
+ nd_from = cand
85
+ break
86
+
87
+ overridden: list = []
88
+ proj_sec = _load_ndtdbg_section(proj) if os.path.isfile(proj) else None
89
+ if proj_sec:
90
+ for cand in chain:
91
+ if cand == proj:
92
+ continue
93
+ lower = _load_ndtdbg_section(cand) or {}
94
+ for k, v in proj_sec.items():
95
+ if k in lower and lower[k] != v:
96
+ overridden.append(k)
97
+
98
+ out: dict = {
99
+ "sources": chain,
100
+ "ndtdbg_effective_from": nd_from,
101
+ "write_allowed_granted_by": write_allowed_source(),
102
+ "overridden_keys": sorted(set(overridden)),
103
+ }
104
+ if nd_from and nd_from != proj:
105
+ # P1-4 onboarding: ndtdbg 生效来源不是项目层 (项目层缺 ndtdbg 段或文件不存在)
106
+ example_hint = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
107
+ "lab", "lab.example.yaml")
108
+ out["hint"] = (f"ndtdbg 生效配置来自 {nd_from}; 当前项目层 lab/lab.yaml 未定义 ndtdbg 段。"
109
+ f"如需项目级覆盖, 参考 {example_hint} 的 ndtdbg 段")
110
+ return out
111
+
112
+
49
113
  # --------------------------------------------------------------------------- config
50
114
 
51
115
  _CONFIG_FILES: list[str] = [] # 实际加载的 lab.yaml (优先级从高到低), 供错误信息引用
@@ -146,8 +210,11 @@ def cmd_status(io: NdtBoxIo) -> None:
146
210
  # 0xFFFF 的 module_connect 同样是悬空默认电平, 不作为总线 OK 证据。
147
211
  chip_ok = fw_version not in (None, "v0.0.0.0", "v255.255.255.255") and not bus_hung
148
212
  bus_ok = not bus_hung and (chip_ok or (module_connect is not None and module_connect != 0xFFFF))
149
- chip_state = "OK" if chip_ok else ("UNKNOWN" if bus_ok else "NO_RESPONSE")
150
- bus_state = "HUNG" if bus_hung else ("OK" if bus_ok else "NO_RESPONSE")
213
+ # P1-3: SILENT ( NO_RESPONSE) —— 总线可自由收发但无从机应答, 常见于运行态
214
+ # 固件使用自有协议 ( boot ROM 0xA0+寄存器语义应答), 属预期而非故障;
215
+ # 旧文案 "检查接线/上拉/供电" 把 agent 引去查硬件, 浪费一轮排查。
216
+ chip_state = "OK" if chip_ok else ("UNKNOWN" if bus_ok else "SILENT")
217
+ bus_state = "HUNG" if bus_hung else ("OK" if bus_ok else "SILENT")
151
218
  data = {
152
219
  "device": "NdtBox",
153
220
  "slave_addr": io.slave_addr,
@@ -165,7 +232,9 @@ def cmd_status(io: NdtBoxIo) -> None:
165
232
  data["hint"] = ("从机疑似 clock-stretch 挂死 (常见于目标 HardFault/关中断死循环): "
166
233
  "I2C 读在事务超时内未返回, 区别于返回 FF 的无应答; 请仅对目标板断电重启 (勿动 BOX)")
167
234
  elif not bus_ok:
168
- data["hint"] = ("疑似 IIC 总线无应答: 检查接线/上拉/供电/从机地址; "
235
+ data["hint"] = ("iic_bus SILENT: 总线可自由收发但无从机应答——运行态固件通常不响应本工具读协议"
236
+ " (仅 boot ROM 应答), 属预期而非故障; 烧录流程会先断电复位进 boot 再通信。"
237
+ "若确需排查硬件, 再检查接线/上拉/供电/从机地址; "
169
238
  "0xFF/0xFFFF 读回值是总线悬空的默认电平, 不代表芯片状态")
170
239
  ok("status", data, log="Open + ReadFwVersion(0x10) + ReadModuleConnect(0x29)")
171
240
 
@@ -508,7 +577,12 @@ def main(argv=None) -> None:
508
577
  except NdtBoxError as exc:
509
578
  fail(exc.error_class, exc.message, exc.detail)
510
579
  except Exception as exc: # noqa: BLE001
511
- fail("UNKNOWN_ERROR", f"ndtdbg_cli 异常: {exc}")
580
+ # P1-2: 堆栈透出——exc_type + traceback 末 5 行随错误信封返回, agent 无需
581
+ # 再注入 monkey-patch 脚本定位 (实战定位 ndtdbg.py:790 花了 20 分钟)
582
+ import traceback
583
+ tail = "\n".join(traceback.format_exc().strip().splitlines()[-5:])
584
+ fail("UNKNOWN_ERROR", f"ndtdbg_cli 异常: {exc}",
585
+ detail=f"exc_type={type(exc).__name__}; traceback_tail:\n{tail}")
512
586
 
513
587
 
514
588
  if __name__ == "__main__":
@@ -207,9 +207,37 @@ def cmd_read(args: argparse.Namespace) -> None:
207
207
  if args.expect is not None:
208
208
  result = session.expect(args.expect, args.timeout_ms, is_hex=args.expect_hex)
209
209
  else:
210
- result = session.read(max_bytes=args.max_bytes)
210
+ result = session.read(max_bytes=RX_BUFFER_LIMIT if args.drain else args.max_bytes)
211
211
  except SerialAssistantError as exc:
212
212
  fail(exc.error_class, exc.detail, detail=exc.extra or None)
213
+
214
+ if args.drain and not args.expect:
215
+ # P2-3 (0909 提案): 单次排空整个缓冲; 大 payload 落盘而非进 MCP 响应,
216
+ # 防止大缓冲 (实战 850KB 刷屏) 撑爆 agent 上下文。返回摘要 + 文件路径。
217
+ data_hex = result.get("data_hex") or ""
218
+ n = result.get("bytes", 0)
219
+ payload: dict = {"drained_bytes": n, "buffer_bytes": result.get("buffer_bytes", 0),
220
+ "overflowed": result.get("overflowed"),
221
+ "rx_bytes_total": result.get("rx_bytes"),
222
+ "tx_bytes_total": result.get("tx_bytes")}
223
+ if n > 65536:
224
+ config = ndtdbg_cli.load_config(args.lab_config)
225
+ out = os.path.join(log_dir(config, args),
226
+ f"drain-{time.strftime('%Y%m%d-%H%M%S')}.hex")
227
+ os.makedirs(os.path.dirname(out), exist_ok=True)
228
+ with open(out, "w", encoding="utf-8") as fh:
229
+ fh.write(data_hex)
230
+ payload["file_path"] = out
231
+ tail_bytes = bytes.fromhex(data_hex.replace(" ", "")[-1024:] or "")
232
+ payload["tail_ascii"] = to_ascii(tail_bytes) # 末 512B 预览
233
+ payload["note"] = (f"{n}B 全量已落盘 (>64KB 不进 MCP 响应); "
234
+ "解析用 fw_serial_parse --source <该文件>")
235
+ else:
236
+ payload["data_hex"] = data_hex
237
+ payload["data_ascii"] = result.get("data_ascii")
238
+ ok("read", payload, log=f"SerialRead(drain, {n}B)")
239
+ return
240
+
213
241
  data = {"data_hex": result.get("data_hex"), "data_ascii": result.get("data_ascii"),
214
242
  "bytes": result.get("bytes", 0),
215
243
  "buffer_bytes": result.get("buffer_bytes"),
@@ -347,6 +375,8 @@ def build_parser() -> argparse.ArgumentParser:
347
375
 
348
376
  sp = sub.add_parser("read", parents=[common], help="读取会话接收缓冲")
349
377
  sp.add_argument("--max-bytes", type=int, default=65536)
378
+ sp.add_argument("--drain", action="store_true",
379
+ help="单次排空整个缓冲 (>64KB 落盘 .hex 返回摘要, 防大 payload 进 MCP 响应)")
350
380
  sp.add_argument("--expect", help="等待缓冲出现该文本 (boot_expect 语义)")
351
381
  sp.add_argument("--expect-hex", action="store_true", help="expect 按 HEX 解析")
352
382
  sp.add_argument("--timeout-ms", type=int, default=2000, help="expect 超时 (默认 2000)")
@@ -1,6 +1,6 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Dual-Tier MCP setup and registration helper (v0.6.0).
3
+ Dual-Tier MCP setup and registration helper (v0.6.1).
4
4
  Inspects installed AI coding agents (Qoder, Claude Code, Antigravity, Cursor)
5
5
  and configures global MCP / skills or prints exact registration commands.
6
6
 
@@ -242,7 +242,7 @@ if ($Json) {
242
242
  Write-FwJson ([pscustomobject]$report)
243
243
  } else {
244
244
  Write-Host "============================================================" -ForegroundColor Cyan
245
- Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.6.0)" -ForegroundColor Green
245
+ Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.6.1)" -ForegroundColor Green
246
246
  Write-Host "============================================================" -ForegroundColor Cyan
247
247
  Write-Host ""
248
248
  Write-Host "[1] Google Antigravity / Gemini CLI:" -ForegroundColor Yellow
File without changes
File without changes
File without changes