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.
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/PKG-INFO +3 -3
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/README.md +2 -2
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/PKG-INFO +3 -3
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/pyproject.toml +1 -1
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/__init__.py +1 -1
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/doctor.ps1 +87 -5
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/fw_mcp_server.py +275 -67
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/lib/ndtdbg.py +99 -7
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/ndtdbg_bridge.ps1 +6 -2
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/ndtdbg_cli.py +78 -4
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/serial_cli.py +31 -1
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/setup-agent-mcp.ps1 +2 -2
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/LICENSE +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/SOURCES.txt +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/dependency_links.txt +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/entry_points.txt +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/requires.txt +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/firmwareloop.egg-info/top_level.txt +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/setup.cfg +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/acceptance-scenario.ps1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/build.ps1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/can.ps1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/check-qoder-mcp.ps1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/common/build-backends.psm1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/common/fw.psm1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/common/uart_probe.py +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/flash.ps1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/instrument_cli.py +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/lib/__init__.py +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/lib/instruments.py +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/lib/serial_assistant.py +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/logic_capture.ps1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/logic_decode.ps1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/reset.ps1 +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/sigrok_csv_to_fwloop.py +0 -0
- {firmwareloop-0.6.0 → firmwareloop-0.6.1}/tools/test-backends.ps1 +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
395
|
+
## 能力验证状态(v0.6.1)
|
|
396
396
|
|
|
397
397
|
> 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
|
|
398
398
|
> `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
|
|
@@ -129,28 +129,110 @@ Add-FwCheck 'pyvisa' {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
# COM ports present (UART candidates) - informational, never hardcoded.
|
|
132
|
-
#
|
|
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
|
-
$
|
|
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 ($
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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
|
-
|
|
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("
|
|
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,
|
|
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
|
-
"
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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"] = ("
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|