firmwareloop 0.1.9__tar.gz → 0.2.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.1.9 → firmwareloop-0.2.1}/PKG-INFO +3 -3
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/README.md +2 -2
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/firmwareloop.egg-info/PKG-INFO +3 -3
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/pyproject.toml +1 -1
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/__init__.py +1 -1
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/fw_mcp_server.py +159 -18
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/lib/ndtdbg.py +122 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/ndtdbg_bridge.ps1 +121 -1
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/ndtdbg_cli.py +134 -1
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/setup-agent-mcp.ps1 +2 -2
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/LICENSE +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/firmwareloop.egg-info/SOURCES.txt +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/firmwareloop.egg-info/dependency_links.txt +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/firmwareloop.egg-info/entry_points.txt +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/firmwareloop.egg-info/requires.txt +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/firmwareloop.egg-info/top_level.txt +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/setup.cfg +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/acceptance-scenario.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/build.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/can.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/check-qoder-mcp.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/common/build-backends.psm1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/common/fw.psm1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/common/uart_probe.py +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/doctor.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/flash.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/instrument_cli.py +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/lib/__init__.py +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/lib/instruments.py +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/logic_capture.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/logic_decode.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/reset.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/sigrok_csv_to_fwloop.py +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/test-backends.ps1 +0 -0
- {firmwareloop-0.1.9 → firmwareloop-0.2.1}/tools/test.ps1 +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firmwareloop
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.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
|
|
@@ -381,7 +381,7 @@ claude mcp remove --scope user agentic-hil
|
|
|
381
381
|
├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
|
|
382
382
|
├── demo-make/ 示例固件(Make;构建测试载体)
|
|
383
383
|
├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
|
|
384
|
-
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.1
|
|
384
|
+
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.2.1)
|
|
385
385
|
├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
|
|
386
386
|
├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
|
|
387
387
|
├── CLAUDE.md Claude Code CLI 指南
|
|
@@ -391,7 +391,7 @@ claude mcp remove --scope user agentic-hil
|
|
|
391
391
|
|
|
392
392
|
---
|
|
393
393
|
|
|
394
|
-
## 能力验证状态(v0.1
|
|
394
|
+
## 能力验证状态(v0.2.1)
|
|
395
395
|
|
|
396
396
|
> 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
|
|
397
397
|
> `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
|
|
@@ -353,7 +353,7 @@ claude mcp remove --scope user agentic-hil
|
|
|
353
353
|
├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
|
|
354
354
|
├── demo-make/ 示例固件(Make;构建测试载体)
|
|
355
355
|
├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
|
|
356
|
-
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.1
|
|
356
|
+
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.2.1)
|
|
357
357
|
├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
|
|
358
358
|
├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
|
|
359
359
|
├── CLAUDE.md Claude Code CLI 指南
|
|
@@ -363,7 +363,7 @@ claude mcp remove --scope user agentic-hil
|
|
|
363
363
|
|
|
364
364
|
---
|
|
365
365
|
|
|
366
|
-
## 能力验证状态(v0.1
|
|
366
|
+
## 能力验证状态(v0.2.1)
|
|
367
367
|
|
|
368
368
|
> 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
|
|
369
369
|
> `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: firmwareloop
|
|
3
|
-
Version: 0.1
|
|
3
|
+
Version: 0.2.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
|
|
@@ -381,7 +381,7 @@ claude mcp remove --scope user agentic-hil
|
|
|
381
381
|
├── demo-firmware/ 示例固件(CMake;宿主编译模拟 MCU,闭环验证载体)
|
|
382
382
|
├── demo-make/ 示例固件(Make;构建测试载体)
|
|
383
383
|
├── docs/ DEPENDENCY_MATRIX / REUSE_PLAN / V0.0.2_GAP_VERIFICATION
|
|
384
|
-
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.1
|
|
384
|
+
├── pyproject.toml 标准 Python 包配置与 CLI 入口声明 (v0.2.1)
|
|
385
385
|
├── .mcp.example.json 双层 MCP 配置模板(firmwareloop + agentic-hil)
|
|
386
386
|
├── AGENTS.md 通用智能体规范(Antigravity / Qoder / Cursor 等)
|
|
387
387
|
├── CLAUDE.md Claude Code CLI 指南
|
|
@@ -391,7 +391,7 @@ claude mcp remove --scope user agentic-hil
|
|
|
391
391
|
|
|
392
392
|
---
|
|
393
393
|
|
|
394
|
-
## 能力验证状态(v0.1
|
|
394
|
+
## 能力验证状态(v0.2.1)
|
|
395
395
|
|
|
396
396
|
> 状态定义:`Implemented`(已实现)/ `Simulator Validated`(模拟验证)/
|
|
397
397
|
> `Real Hardware Validated`(真机验证)/ `Experimental` / `Not Implemented`
|
|
@@ -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.1
|
|
3
|
+
fw_mcp_server.py - FirmwareLoop High-Level Workflow & Device MCP Server (v0.2.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
|
|
@@ -20,6 +20,7 @@ Tools provided:
|
|
|
20
20
|
- fw_ndtdbg_write_reg: NdtBox IIC register write, double-gated (ndtdbg_cli.py)
|
|
21
21
|
- fw_ndtdbg_capture_seq: NdtBox register sequence + concurrent I2C capture for waveform evidence (ndtdbg_cli.py)
|
|
22
22
|
- fw_ndtdbg_flash: NdtBox IIC firmware flash (.bin/.nfw, full-chip erase, double-gated) (ndtdbg_cli.py)
|
|
23
|
+
- fw_ndtdbg_stream: NdtBox high-rate register stream capture to CSV via one long-lived USB session (read-only) (ndtdbg_cli.py)
|
|
23
24
|
- fw_logic_capture: Logic analyzer protocol capture (logic_capture.ps1)
|
|
24
25
|
- fw_logic_decode: Logic analyzer protocol decoding & assertion (logic_decode.ps1)
|
|
25
26
|
- fw_get_evidence: Read structured audit run evidence (artifacts/runs/)
|
|
@@ -330,6 +331,39 @@ def handle_fw_ndtdbg_flash(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
330
331
|
return _ndtdbg_run(cmd, timeout_s)
|
|
331
332
|
|
|
332
333
|
|
|
334
|
+
def handle_fw_ndtdbg_stream(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
335
|
+
"""Stream-read a set of IIC registers at a fixed interval via one long-lived
|
|
336
|
+
USB session (sampling loop runs inside the 32-bit bridge process, so the
|
|
337
|
+
per-frame overhead is one StreamI2C transaction, not a full cold start).
|
|
338
|
+
Read-only. Writes CSV with timestamps; returns JSON summary."""
|
|
339
|
+
cli_script = os.path.join(REPO_ROOT, "tools", "ndtdbg_cli.py")
|
|
340
|
+
regs = args.get("regs") or ["0xEE:8"]
|
|
341
|
+
duration_s = min(max(float(args.get("duration_s", 10)), 0.0), 600.0)
|
|
342
|
+
max_frames = int(args.get("max_frames", 0))
|
|
343
|
+
if duration_s <= 0 and max_frames <= 0:
|
|
344
|
+
return {"ok": False, "error_class": "CONFIG_ERROR",
|
|
345
|
+
"error": "stream requires duration_s or max_frames > 0"}
|
|
346
|
+
cmd = [PYTHON_EXE, cli_script, "stream", "--regs", *[str(r) for r in regs]]
|
|
347
|
+
if args.get("interval_ms") is not None:
|
|
348
|
+
cmd.extend(["--interval-ms", str(int(args["interval_ms"]))])
|
|
349
|
+
cmd.extend(["--duration-s", str(duration_s)])
|
|
350
|
+
if max_frames > 0:
|
|
351
|
+
cmd.extend(["--max-frames", str(max_frames)])
|
|
352
|
+
if args.get("out_path"):
|
|
353
|
+
cmd.extend(["--out", str(args["out_path"])])
|
|
354
|
+
decode = args.get("decode", "hex")
|
|
355
|
+
if decode != "hex":
|
|
356
|
+
cmd.extend(["--decode", decode])
|
|
357
|
+
if args.get("retry") is not None:
|
|
358
|
+
cmd.extend(["--retry", str(int(args["retry"]))])
|
|
359
|
+
if args.get("fail_threshold") is not None:
|
|
360
|
+
cmd.extend(["--fail-threshold", str(int(args["fail_threshold"]))])
|
|
361
|
+
if args.get("require_target"):
|
|
362
|
+
cmd.append("--require-target")
|
|
363
|
+
_ndtdbg_reg_args(args, cmd)
|
|
364
|
+
return _ndtdbg_run(cmd, timeout=int(duration_s) + 120)
|
|
365
|
+
|
|
366
|
+
|
|
333
367
|
def handle_fw_ndtdbg_capture_seq(args: Dict[str, Any]) -> Dict[str, Any]:
|
|
334
368
|
"""单次调用内并发: 逻辑分析仪抓取与 NdtBox 寄存器序列同时进行。
|
|
335
369
|
|
|
@@ -1352,6 +1386,65 @@ TOOLS_REGISTRY = {
|
|
|
1352
1386
|
},
|
|
1353
1387
|
"handler": handle_fw_ndtdbg_flash,
|
|
1354
1388
|
},
|
|
1389
|
+
"fw_ndtdbg_stream": {
|
|
1390
|
+
"description": "Stream-read a set of IIC registers from the NdtBox target chip (ChipSea Fxx) at a fixed interval via one long-lived USB session. Read-only. Writes CSV with timestamps; returns JSON summary (frames, FF_GAP/IO_FAIL counts, interval stats). For capturing sensor/debug data streams (e.g. PT488 0xEE 4-channel data). Typical combo: write_reg(0xBE=01, confirm) to start debug_mode, then stream(0xEE).",
|
|
1391
|
+
"inputSchema": {
|
|
1392
|
+
"type": "object",
|
|
1393
|
+
"properties": {
|
|
1394
|
+
"regs": {
|
|
1395
|
+
"type": "array",
|
|
1396
|
+
"items": {"type": "string"},
|
|
1397
|
+
"description": "Register specs 'addr:len[:name]', e.g. ['0xEE:8:ch', '0x20:138:event']. Default ['0xEE:8'].",
|
|
1398
|
+
"default": ["0xEE:8"],
|
|
1399
|
+
},
|
|
1400
|
+
"interval_ms": {
|
|
1401
|
+
"type": "integer",
|
|
1402
|
+
"description": "Target sampling interval in ms. Actual interval = max(interval_ms, io_time); jitter recorded in summary.",
|
|
1403
|
+
"default": 100,
|
|
1404
|
+
},
|
|
1405
|
+
"duration_s": {
|
|
1406
|
+
"type": "number",
|
|
1407
|
+
"description": "Total capture duration in seconds (capped at 600). Either duration_s or max_frames must be > 0.",
|
|
1408
|
+
"default": 10,
|
|
1409
|
+
},
|
|
1410
|
+
"max_frames": {
|
|
1411
|
+
"type": "integer",
|
|
1412
|
+
"description": "Stop after N frames (alternative to duration_s).",
|
|
1413
|
+
"default": 0,
|
|
1414
|
+
},
|
|
1415
|
+
"out_path": {
|
|
1416
|
+
"type": "string",
|
|
1417
|
+
"description": "CSV output path. Default: <cwd>/lab/streams/ndtdbg_stream_<ts>.csv",
|
|
1418
|
+
},
|
|
1419
|
+
"decode": {
|
|
1420
|
+
"type": "string",
|
|
1421
|
+
"enum": ["hex", "int16le", "uint16le", "int8", "uint8"],
|
|
1422
|
+
"description": "Optional decoded columns besides raw hex (int16le for PT488 0xEE channels).",
|
|
1423
|
+
"default": "hex",
|
|
1424
|
+
},
|
|
1425
|
+
"retry": {
|
|
1426
|
+
"type": "integer",
|
|
1427
|
+
"description": "Retries per frame per register on all-0xFF / IO error before marking the frame FAILED.",
|
|
1428
|
+
"default": 2,
|
|
1429
|
+
},
|
|
1430
|
+
"fail_threshold": {
|
|
1431
|
+
"type": "integer",
|
|
1432
|
+
"description": "Abort capture after N consecutive IO_FAIL frames (0 = never abort); already-captured data is kept and returned.",
|
|
1433
|
+
"default": 50,
|
|
1434
|
+
},
|
|
1435
|
+
"require_target": {
|
|
1436
|
+
"type": "boolean",
|
|
1437
|
+
"description": "Probe reg 0x10 before capture; abort with TARGET_MISMATCH when the chip does not answer.",
|
|
1438
|
+
"default": False,
|
|
1439
|
+
},
|
|
1440
|
+
"dll_dir": {"type": "string", "description": "NdtDebugTool lib dir; default from lab.yaml ndtdbg.dll_dir"},
|
|
1441
|
+
"slave_addr": {"type": "string", "description": "IIC slave address, e.g. 0xA0; default from lab.yaml"},
|
|
1442
|
+
},
|
|
1443
|
+
"required": ["regs"],
|
|
1444
|
+
"additionalProperties": False,
|
|
1445
|
+
},
|
|
1446
|
+
"handler": handle_fw_ndtdbg_stream,
|
|
1447
|
+
},
|
|
1355
1448
|
"fw_ndtdbg_capture_seq": {
|
|
1356
1449
|
"description": "One-call protocol evidence: concurrently runs a NdtBox register write sequence and a sigrok I2C capture, returning the capture path for fw_logic_decode. NOTE: the default sequence (0xBE=01, 0x01=BE 42, poll 0x01, readback 0xBE) is an EXAMPLE only - always pass your target firmware's real register protocol via writes/poll_reg/poll_expect. Writes are double-gated (confirm: true + lab.yaml ndtdbg.write_allowed); degrades to sequence-only execution (capture_skipped) when sigrok-cli is missing.",
|
|
1357
1450
|
"inputSchema": {
|
|
@@ -1526,7 +1619,7 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
1526
1619
|
},
|
|
1527
1620
|
"serverInfo": {
|
|
1528
1621
|
"name": "firmwareloop",
|
|
1529
|
-
"version": "0.1
|
|
1622
|
+
"version": "0.2.1"
|
|
1530
1623
|
}
|
|
1531
1624
|
}
|
|
1532
1625
|
}
|
|
@@ -1615,10 +1708,53 @@ def process_request(req: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
1615
1708
|
return None
|
|
1616
1709
|
|
|
1617
1710
|
|
|
1711
|
+
def _local_bin_entrypoints() -> list:
|
|
1712
|
+
"""uv tool 入口点 exe (~/.local/bin)——本命令自身与常驻 MCP server 都从这里运行。"""
|
|
1713
|
+
home = os.path.expanduser("~")
|
|
1714
|
+
return [os.path.join(home, ".local", "bin", name)
|
|
1715
|
+
for name in ("fwloop.exe", "fw-mcp.exe", "agentic-hil.exe")]
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
def _stash_local_bin_entrypoints() -> list:
|
|
1719
|
+
"""升级前把入口点 exe 重命名让位 (Windows 允许 rename 运行中的 exe, 不允许覆盖写)。
|
|
1720
|
+
|
|
1721
|
+
否则 `fwloop update` 第一次必然失败: uv 复制新 fwloop.exe 的目标文件正是当前
|
|
1722
|
+
命令自己的启动映像 (被锁定, os error 32), 第二次则因 "Nothing to upgrade"
|
|
1723
|
+
跳过入口点同步。返回 [(原路径, 备份路径)]。
|
|
1724
|
+
"""
|
|
1725
|
+
stashed = []
|
|
1726
|
+
for path in _local_bin_entrypoints():
|
|
1727
|
+
if not os.path.isfile(path):
|
|
1728
|
+
continue
|
|
1729
|
+
stash = path + ".upgrading"
|
|
1730
|
+
try:
|
|
1731
|
+
if os.path.exists(stash): # 上次升级残留 (旧映像仍被运行中进程持有)
|
|
1732
|
+
os.remove(stash)
|
|
1733
|
+
os.rename(path, stash)
|
|
1734
|
+
stashed.append((path, stash))
|
|
1735
|
+
print(f"[*] Entrypoint stashed for upgrade: {os.path.basename(path)} -> .upgrading")
|
|
1736
|
+
except OSError:
|
|
1737
|
+
pass # 让位失败不阻塞升级: uv 直接覆盖, 若 os error 32 走针对性诊断
|
|
1738
|
+
return stashed
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
def _settle_stashed_entrypoints(stashed) -> None:
|
|
1742
|
+
"""升级收尾: uv 已写入新入口点 -> 删备份 (旧映像仍被运行中进程持有时留待下次);
|
|
1743
|
+
uv 未写入 (升级失败/中止) -> 还原旧入口点, 保证 fwloop 命令不消失。"""
|
|
1744
|
+
for path, stash in stashed:
|
|
1745
|
+
try:
|
|
1746
|
+
if os.path.isfile(path):
|
|
1747
|
+
os.remove(stash) # 新 exe 已就位, 备份可弃
|
|
1748
|
+
else:
|
|
1749
|
+
os.rename(stash, path) # 升级未完成: 还原
|
|
1750
|
+
except OSError:
|
|
1751
|
+
pass # 旧映像仍被本命令/常驻 MCP 持有, 留待下次清理
|
|
1752
|
+
|
|
1753
|
+
|
|
1618
1754
|
def handle_cli_update() -> int:
|
|
1619
1755
|
"""Handle `firmwareloop update` command."""
|
|
1620
1756
|
print("============================================================")
|
|
1621
|
-
print(" FirmwareLoop Auto-Updater (v0.1
|
|
1757
|
+
print(" FirmwareLoop Auto-Updater (v0.2.1)")
|
|
1622
1758
|
print("============================================================")
|
|
1623
1759
|
|
|
1624
1760
|
is_git_repo = os.path.exists(os.path.join(REPO_ROOT, ".git"))
|
|
@@ -1661,19 +1797,24 @@ def handle_cli_update() -> int:
|
|
|
1661
1797
|
uv_path = shutil.which("uv")
|
|
1662
1798
|
if uv_path:
|
|
1663
1799
|
print("[*] Upgrading firmwareloop via `uv tool upgrade firmwareloop`...")
|
|
1800
|
+
# 关键: 入口点 exe (.local\bin\fwloop.exe) 正被本命令自身运行——Windows 禁止
|
|
1801
|
+
# 覆盖运行中的 exe (os error 32), 不让位则第一次 upgrade 必然失败。
|
|
1802
|
+
# rename 让位 (运行中的 exe 允许重命名), uv 复制新入口点后在收尾阶段清理/还原。
|
|
1803
|
+
stashed = _stash_local_bin_entrypoints()
|
|
1664
1804
|
try:
|
|
1665
1805
|
res = subprocess.run([uv_path, "tool", "upgrade", "firmwareloop"])
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1806
|
+
finally:
|
|
1807
|
+
_settle_stashed_entrypoints(stashed)
|
|
1808
|
+
if res.returncode == 0:
|
|
1809
|
+
print("\n============================================================")
|
|
1810
|
+
print("[+] FirmwareLoop has been upgraded to the latest version!")
|
|
1811
|
+
print("============================================================")
|
|
1812
|
+
return 0
|
|
1813
|
+
print(f"[-] uv tool upgrade returned code {res.returncode}")
|
|
1814
|
+
print(" 工具环境可能已升级成功, 仅入口点 exe 未更新 (os error 32 = 文件被占用)。")
|
|
1815
|
+
print(" 占用者通常是本命令自身或常驻的 MCP server 进程; 关闭 IDE 后重跑一次")
|
|
1816
|
+
print(" `fwloop update` 即可完成收尾。")
|
|
1817
|
+
return res.returncode
|
|
1677
1818
|
else:
|
|
1678
1819
|
print("[*] Upgrading via pip (`pip install --upgrade firmwareloop`)...")
|
|
1679
1820
|
res = subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "firmwareloop"])
|
|
@@ -1888,7 +2029,7 @@ def set_ndtdbg_dlldir(dll_path: str) -> str:
|
|
|
1888
2029
|
def handle_cli_setup() -> int:
|
|
1889
2030
|
"""Handle `firmwareloop setup` command: simultaneously configure Antigravity CLI, Claude Code CLI, Qoder IDE, and CodeBuddy IDE."""
|
|
1890
2031
|
print("============================================================")
|
|
1891
|
-
print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.1
|
|
2032
|
+
print(" FirmwareLoop Multi-Agent Global Setup Helper (v0.2.1)")
|
|
1892
2033
|
print("============================================================")
|
|
1893
2034
|
print("")
|
|
1894
2035
|
|
|
@@ -2013,7 +2154,7 @@ def handle_cli_setup() -> int:
|
|
|
2013
2154
|
def handle_cli_init() -> int:
|
|
2014
2155
|
"""Handle `firmwareloop init` command."""
|
|
2015
2156
|
print("============================================================")
|
|
2016
|
-
print(" FirmwareLoop Multi-Agent Project Initializer (v0.1
|
|
2157
|
+
print(" FirmwareLoop Multi-Agent Project Initializer (v0.2.1)")
|
|
2017
2158
|
print("============================================================")
|
|
2018
2159
|
cwd = os.getcwd()
|
|
2019
2160
|
print(f"[*] Initializing multi-agent guidelines & bench config in:\n {cwd}\n")
|
|
@@ -2034,7 +2175,7 @@ def handle_cli_init() -> int:
|
|
|
2034
2175
|
|
|
2035
2176
|
|
|
2036
2177
|
def print_cli_help() -> None:
|
|
2037
|
-
print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.1
|
|
2178
|
+
print("""FirmwareLoop (fwloop) — AI Agent Firmware Engineering & Lab Automation Platform (v0.2.1)
|
|
2038
2179
|
|
|
2039
2180
|
Usage:
|
|
2040
2181
|
fwloop [command] (or: firmwareloop [command])
|
|
@@ -2074,7 +2215,7 @@ def main() -> None:
|
|
|
2074
2215
|
sys.exit(2)
|
|
2075
2216
|
sys.exit(handle_cli_ndtdebugtool(args[1]))
|
|
2076
2217
|
elif cmd in ["version", "-v", "--version"]:
|
|
2077
|
-
print("FirmwareLoop v0.1
|
|
2218
|
+
print("FirmwareLoop v0.2.1")
|
|
2078
2219
|
sys.exit(0)
|
|
2079
2220
|
elif cmd in ["help", "-h", "--help"]:
|
|
2080
2221
|
print_cli_help()
|
|
@@ -79,6 +79,72 @@ def parse_module_connect(data: bytes) -> int:
|
|
|
79
79
|
return int.from_bytes(data[:2], "little")
|
|
80
80
|
|
|
81
81
|
|
|
82
|
+
# ---------------------------------------------------------------------------
|
|
83
|
+
# stream 连续采集 (纯函数部分, 平台无关可单测)
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def parse_reg_spec(spec: str) -> dict:
|
|
87
|
+
"""解析寄存器 spec 'addr:len[:name]' -> {'addr', 'len', 'name'}。
|
|
88
|
+
|
|
89
|
+
addr 支持 0x 前缀; name 省略时自动生成 reg_<hex>。Fail-Closed: 格式/越界抛 CONFIG_ERROR。
|
|
90
|
+
"""
|
|
91
|
+
parts = str(spec).split(":")
|
|
92
|
+
if len(parts) < 2 or len(parts) > 3 or not parts[0] or not parts[1]:
|
|
93
|
+
raise NdtBoxError("CONFIG_ERROR", "reg spec 格式非法 (addr:len[:name])", str(spec))
|
|
94
|
+
try:
|
|
95
|
+
addr = int(parts[0], 0)
|
|
96
|
+
length = int(parts[1], 0)
|
|
97
|
+
except ValueError as exc:
|
|
98
|
+
raise NdtBoxError("CONFIG_ERROR", "reg spec addr/len 不是合法整数", str(spec)) from exc
|
|
99
|
+
if not 0 <= addr <= 0xFF:
|
|
100
|
+
raise NdtBoxError("CONFIG_ERROR", "reg spec addr 越界 (0..0xFF)", hex(addr))
|
|
101
|
+
if not 1 <= length <= 256:
|
|
102
|
+
raise NdtBoxError("CONFIG_ERROR", "reg spec len 越界 (1..256)", str(length))
|
|
103
|
+
name = parts[2].strip() if len(parts) == 3 else ""
|
|
104
|
+
if name and not all(c.isalnum() or c == "_" for c in name):
|
|
105
|
+
raise NdtBoxError("CONFIG_ERROR", "reg spec name 只允许字母/数字/下划线", name)
|
|
106
|
+
return {"addr": addr, "len": length, "name": name or f"reg_{addr:02X}"}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def decode_payload(hex_str: str | None, mode: str) -> str | None:
|
|
110
|
+
"""hex 载荷按模式解码为 '|' 分隔数值串; mode=hex 或空载荷返回 None。
|
|
111
|
+
|
|
112
|
+
'BD 00 AB 00' + int16le -> '189|171' (小端); int16le 对 >=0x8000 做 C# (short) 有符号语义。
|
|
113
|
+
"""
|
|
114
|
+
if mode == "hex" or not hex_str or hex_str == "-":
|
|
115
|
+
return None
|
|
116
|
+
try:
|
|
117
|
+
raw = bytes.fromhex(hex_str.replace(" ", ""))
|
|
118
|
+
except ValueError as exc:
|
|
119
|
+
raise NdtBoxError("CONFIG_ERROR", "hex 载荷非法", hex_str) from exc
|
|
120
|
+
vals: list[int] = []
|
|
121
|
+
if mode in ("int16le", "uint16le"):
|
|
122
|
+
for i in range(0, len(raw) - len(raw) % 2, 2):
|
|
123
|
+
v = int.from_bytes(raw[i:i + 2], "little")
|
|
124
|
+
if mode == "int16le" and v >= 0x8000:
|
|
125
|
+
v -= 0x10000
|
|
126
|
+
vals.append(v)
|
|
127
|
+
elif mode in ("int8", "uint8"):
|
|
128
|
+
for b in raw:
|
|
129
|
+
vals.append(b - 256 if (mode == "int8" and b >= 0x80) else b)
|
|
130
|
+
else:
|
|
131
|
+
raise NdtBoxError("CONFIG_ERROR", "decode 模式非法 (hex/int16le/uint16le/int8/uint8)", mode)
|
|
132
|
+
return "|".join(str(v) for v in vals) if vals else None
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def interval_stats(frame_times_ms: list[float]) -> dict:
|
|
136
|
+
"""帧完成时间序列 -> 实际间隔统计 {avg_ms, p95_ms, gaps_n}; 帧数 < 2 时返回空 dict。"""
|
|
137
|
+
gaps = [round(b - a, 1) for a, b in zip(frame_times_ms, frame_times_ms[1:])]
|
|
138
|
+
if not gaps:
|
|
139
|
+
return {}
|
|
140
|
+
ordered = sorted(gaps)
|
|
141
|
+
return {
|
|
142
|
+
"gaps_n": len(gaps),
|
|
143
|
+
"interval_actual_avg_ms": round(sum(gaps) / len(gaps), 1),
|
|
144
|
+
"interval_actual_p95_ms": ordered[min(len(ordered) - 1, int(len(ordered) * 0.95))],
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
|
|
82
148
|
def strip_nfw(file_bytes: bytes, header_len: int = 256) -> bytes:
|
|
83
149
|
""".nfw 固件: 去 256 字节文件头 (ChipSea.cs LoadFile)。"""
|
|
84
150
|
if len(file_bytes) <= header_len:
|
|
@@ -263,6 +329,62 @@ class NdtBoxIo:
|
|
|
263
329
|
raise NdtBoxError("CONFIG_ERROR", "run_ops 空序列", None)
|
|
264
330
|
return self._bridge_pipe("run", json.dumps(ops, separators=(",", ":")), timeout_ms)
|
|
265
331
|
|
|
332
|
+
def stream_run(self, spec: dict, on_row=None) -> dict:
|
|
333
|
+
"""stream action: 桥进程内长驻 USB 会话连续采集 (读循环在 32 位桥内, 消除逐帧冷启动)。
|
|
334
|
+
|
|
335
|
+
spec: {"regs":[{"addr","len","name"}...], "interval_ms","duration_s","max_frames",
|
|
336
|
+
"retry","fail_threshold","require_target"}
|
|
337
|
+
on_row(parts): 每寄存器读完成回调, parts = ['#F', frame, t_rel_ms, reg, hex|'-', tag]
|
|
338
|
+
(tag: OK / FF_GAP / IO_FAIL), tab 分隔。
|
|
339
|
+
末行 JSON 为桥 summary (frames_* / aborted / abort_reason / duration_actual_ms);
|
|
340
|
+
桥失败抛 NdtBoxError; Ctrl+C 传播 KeyboardInterrupt, finally 杀桥进程
|
|
341
|
+
(已回调数据由调用方持有, CSV 保留)。
|
|
342
|
+
"""
|
|
343
|
+
if not os.path.exists(PS32):
|
|
344
|
+
raise NdtBoxError("CONFIG_ERROR", f"32 位 PowerShell 不存在: {PS32}")
|
|
345
|
+
if not os.path.exists(self.bridge_ps1):
|
|
346
|
+
raise NdtBoxError("CONFIG_ERROR", f"桥脚本不存在: {self.bridge_ps1}")
|
|
347
|
+
args = [PS32, "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass",
|
|
348
|
+
"-File", self.bridge_ps1, "-Action", "stream",
|
|
349
|
+
"-DllDir", self.dll_dir, "-SlaveAddr", str(self.slave_addr),
|
|
350
|
+
"-Speed", str(self.speed)]
|
|
351
|
+
proc = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
352
|
+
stderr=subprocess.PIPE, text=True, encoding="utf-8",
|
|
353
|
+
errors="replace")
|
|
354
|
+
try:
|
|
355
|
+
proc.stdin.write(json.dumps(spec, separators=(",", ":")))
|
|
356
|
+
proc.stdin.close()
|
|
357
|
+
except Exception:
|
|
358
|
+
pass
|
|
359
|
+
summary: dict | None = None
|
|
360
|
+
try:
|
|
361
|
+
for line in proc.stdout:
|
|
362
|
+
line = line.rstrip("\r\n")
|
|
363
|
+
if not line:
|
|
364
|
+
continue
|
|
365
|
+
if line.startswith("#F\t"):
|
|
366
|
+
if on_row:
|
|
367
|
+
on_row(line.split("\t"))
|
|
368
|
+
elif line.startswith("{"):
|
|
369
|
+
summary = json.loads(line)
|
|
370
|
+
# 其他行 (未来 #progress 等) 忽略
|
|
371
|
+
finally:
|
|
372
|
+
try:
|
|
373
|
+
if proc.poll() is None:
|
|
374
|
+
proc.kill()
|
|
375
|
+
stderr_tail = (proc.stderr.read() or "").strip()[-400:]
|
|
376
|
+
proc.wait(timeout=10)
|
|
377
|
+
except Exception:
|
|
378
|
+
stderr_tail = ""
|
|
379
|
+
if summary is None:
|
|
380
|
+
raise NdtBoxError("UNKNOWN_ERROR", "桥 stream 无 summary 输出 (进程提前退出/挂死被终止)",
|
|
381
|
+
stderr_tail or None)
|
|
382
|
+
if not summary.get("ok"):
|
|
383
|
+
raise NdtBoxError(summary.get("error_class", "UNKNOWN_ERROR"),
|
|
384
|
+
summary.get("error", "桥 stream 返回失败"),
|
|
385
|
+
summary.get("detail"))
|
|
386
|
+
return summary.get("data", {})
|
|
387
|
+
|
|
266
388
|
def flash_exec(self, plan: dict, timeout_ms: int | None = None) -> dict:
|
|
267
389
|
"""单进程完整固件烧录 (bridge "flash"), 复刻原厂 GUI 进程模型:
|
|
268
390
|
usb2io(IO10) 断电复位进 boot + USBIOX IIC 全序列在**同一进程同一会话**内连续执行,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# 输出: stdout 单行 JSON {ok, action, data} 或 {ok:false, error_class, error, detail}。
|
|
5
5
|
param(
|
|
6
6
|
[Parameter(Mandatory = $true)]
|
|
7
|
-
[ValidateSet("open", "read", "write", "version", "connect", "poll", "close", "raw", "vbrt", "run", "flash")]
|
|
7
|
+
[ValidateSet("open", "read", "write", "version", "connect", "poll", "close", "raw", "vbrt", "run", "flash", "stream")]
|
|
8
8
|
[string]$Action,
|
|
9
9
|
|
|
10
10
|
[string]$DllDir,
|
|
@@ -440,6 +440,126 @@ try {
|
|
|
440
440
|
Write-Output (@{ ok = $true; action = "flash"; data = @{ verify = "pass"; pages_written = $wTotal; read_windows = $rTotal } } | ConvertTo-Json -Compress -Depth 4)
|
|
441
441
|
exit 0
|
|
442
442
|
}
|
|
443
|
+
"stream" {
|
|
444
|
+
# 高速连续采集 (只读): 单桥进程长驻 USB 会话, 循环内只做 I2C 事务 + sleep——
|
|
445
|
+
# 消除逐帧 spawn 的冷启动开销 (进程+DLL+USB 会话 ≈2s), 单帧开销 = 一次 StreamI2C 事务。
|
|
446
|
+
# stdin JSON: {"regs":[{"addr":238,"len":8,"name":"ch"},...],"interval_ms":100,
|
|
447
|
+
# "duration_s":10,"max_frames":0,"retry":2,"fail_threshold":50,
|
|
448
|
+
# "require_target":false}
|
|
449
|
+
# 输出行协议 (tab 分隔, 全 ASCII, Python 端逐行消费写 CSV):
|
|
450
|
+
# #F\t<frame>\t<t_rel_ms>\t<reg>\t<hex|- >\t<tag> 每寄存器一行 (tag: OK/FF_GAP/IO_FAIL)
|
|
451
|
+
# 末行 JSON: {ok, action:"stream", data:{frames_ok, frames_ff_gap, frames_io_fail,
|
|
452
|
+
# frames_total, aborted, abort_reason, duration_actual_ms}}
|
|
453
|
+
# 帧语义 (提案 §4.2): OK=读成功且非全FF; FF_GAP=读成功但全FF (总线高阻特征, 只统计不熔断);
|
|
454
|
+
# IO_FAIL=重试后仍 StreamI2C=false, 连续计数, 达 fail_threshold 熔断 (优雅收尾保留已采数据)。
|
|
455
|
+
$cfgJson = [Console]::In.ReadToEnd()
|
|
456
|
+
$cfg = $cfgJson | ConvertFrom-Json
|
|
457
|
+
if (-not $cfg) { Fail "CONFIG_ERROR" "stream 需要 stdin 传入配置 JSON" $null }
|
|
458
|
+
$sRegs = @($cfg.regs)
|
|
459
|
+
if ($sRegs.Count -eq 0) { Fail "CONFIG_ERROR" "stream regs 为空" $null }
|
|
460
|
+
$sInterval = if ($null -ne $cfg.interval_ms) { [int]$cfg.interval_ms } else { 100 }
|
|
461
|
+
$sDurationS = [double]$cfg.duration_s
|
|
462
|
+
$sMaxFrames = if ($null -ne $cfg.max_frames) { [int]$cfg.max_frames } else { 0 }
|
|
463
|
+
$sRetry = if ($null -ne $cfg.retry) { [int]$cfg.retry } else { 2 }
|
|
464
|
+
$sFailThr = if ($null -ne $cfg.fail_threshold) { [int]$cfg.fail_threshold } else { 50 }
|
|
465
|
+
$sRequireTarget = [bool]$cfg.require_target
|
|
466
|
+
if ($sDurationS -le 0 -and $sMaxFrames -le 0) { Fail "CONFIG_ERROR" "stream 需要 duration_s 或 max_frames 之一 > 0" $null }
|
|
467
|
+
if ($sInterval -lt 1) { Fail "CONFIG_ERROR" "stream interval_ms 必须 >= 1" $sInterval }
|
|
468
|
+
foreach ($r in $sRegs) {
|
|
469
|
+
if (([int]$r.len) -lt 1 -or ([int]$r.len) -gt 256) { Fail "CONFIG_ERROR" "stream 读长度越界 (1..256)" $r.len }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
473
|
+
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
|
474
|
+
$h = Open-Device 0 # 打不开 -> Fail PROBE_NOT_FOUND (发生在任何 #F 行之前)
|
|
475
|
+
|
|
476
|
+
if ($sRequireTarget) {
|
|
477
|
+
$tb = New-Object byte[] 2
|
|
478
|
+
$tb[0] = [byte]$SlaveAddr; $tb[1] = 0x10
|
|
479
|
+
$vb = New-Object byte[] 4
|
|
480
|
+
$verOk = $false
|
|
481
|
+
for ($vt = 0; $vt -le $sRetry; $vt++) {
|
|
482
|
+
if ((Invoke-WriteRead $h $tb $vb)) {
|
|
483
|
+
$meaningful = $false
|
|
484
|
+
foreach ($b in $vb) { if ($b -ne 0 -and $b -ne 0xFF) { $meaningful = $true; break } }
|
|
485
|
+
if ($meaningful) { $verOk = $true; break }
|
|
486
|
+
}
|
|
487
|
+
Start-Sleep -Milliseconds 100
|
|
488
|
+
}
|
|
489
|
+
if (-not $verOk) {
|
|
490
|
+
[STDataCollection.Icb]::USBIO_CloseDevice([uint32]0) | Out-Null
|
|
491
|
+
Fail "TARGET_MISMATCH" "stream 起始探测失败 (0x10 无有效应答); 检查接线/上拉/供电/地址, 或去掉 require_target" $null
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
$framesOk = 0; $framesGap = 0; $framesIoFail = 0
|
|
496
|
+
$frameNo = 0
|
|
497
|
+
$consecIoFail = 0
|
|
498
|
+
$aborted = $false; $abortReason = ""
|
|
499
|
+
$durationMsTotal = [int]($sDurationS * 1000)
|
|
500
|
+
while ($true) {
|
|
501
|
+
$frameNo++
|
|
502
|
+
$frameStartMs = $sw.Elapsed.TotalMilliseconds
|
|
503
|
+
$hasIoFail = $false; $hasGap = $false
|
|
504
|
+
foreach ($r in $sRegs) {
|
|
505
|
+
$reg = [int]$r.addr; $rlen = [int]$r.len
|
|
506
|
+
$hexOut = "-"; $tag = "OK"
|
|
507
|
+
try {
|
|
508
|
+
for ($attempt = 0; $attempt -le $sRetry; $attempt++) {
|
|
509
|
+
$tx = New-Object byte[] 2
|
|
510
|
+
$tx[0] = [byte]$SlaveAddr; $tx[1] = [byte]$reg
|
|
511
|
+
$rb = New-Object byte[] $rlen
|
|
512
|
+
if (Invoke-WriteRead $h $tx $rb) {
|
|
513
|
+
# ForEach-Object 将管道元素绑定到 $_——scriptblock 用 param()
|
|
514
|
+
# 形式收参恒为 null (Null 方法调用), 必须内联 $_
|
|
515
|
+
$hexStr = (($rb | ForEach-Object { $_.ToString("X2") }) -join " ")
|
|
516
|
+
$allFF = $true
|
|
517
|
+
foreach ($b in $rb) { if ($b -ne 0xFF) { $allFF = $false; break } }
|
|
518
|
+
if ($allFF) {
|
|
519
|
+
$tag = "FF_GAP"; $hexOut = $hexStr # 总线高阻特征: 记录原值, 重试
|
|
520
|
+
if ($attempt -lt $sRetry) { Start-Sleep -Milliseconds 5 }
|
|
521
|
+
} else {
|
|
522
|
+
$tag = "OK"; $hexOut = $hexStr
|
|
523
|
+
break
|
|
524
|
+
}
|
|
525
|
+
} else {
|
|
526
|
+
$tag = "IO_FAIL"; $hexOut = "-"
|
|
527
|
+
if ($attempt -lt $sRetry) { Start-Sleep -Milliseconds 5 }
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
} catch {
|
|
531
|
+
# 单寄存器异常降级为 IO_FAIL 行继续采集, 不作废整个 stream (fail-closed 韧性)
|
|
532
|
+
$tag = "IO_FAIL"; $hexOut = "-"
|
|
533
|
+
}
|
|
534
|
+
if ($tag -eq "IO_FAIL") { $hasIoFail = $true }
|
|
535
|
+
elseif ($tag -eq "FF_GAP") { $hasGap = $true }
|
|
536
|
+
$tRel = [math]::Round($sw.Elapsed.TotalMilliseconds, 1)
|
|
537
|
+
[Console]::WriteLine("#F`t{0}`t{1}`t0x{2:X2}`t{3}`t{4}" -f $frameNo, $tRel, $reg, $hexOut, $tag)
|
|
538
|
+
[Console]::Out.Flush()
|
|
539
|
+
}
|
|
540
|
+
if ($hasIoFail) { $framesIoFail++; $consecIoFail++ }
|
|
541
|
+
else {
|
|
542
|
+
if ($hasGap) { $framesGap++ } else { $framesOk++ }
|
|
543
|
+
$consecIoFail = 0 # 读成功帧 (OK/FF_GAP) 重置连续失败计数
|
|
544
|
+
}
|
|
545
|
+
if ($sFailThr -gt 0 -and $consecIoFail -ge $sFailThr) { $aborted = $true; $abortReason = "fail_threshold"; break }
|
|
546
|
+
if ($sMaxFrames -gt 0 -and $frameNo -ge $sMaxFrames) { break }
|
|
547
|
+
$elapsedMs = $sw.Elapsed.TotalMilliseconds
|
|
548
|
+
if ($sDurationS -gt 0 -and $elapsedMs -ge $durationMsTotal) { break }
|
|
549
|
+
if ($sDurationS -gt 0) {
|
|
550
|
+
$wait = $sInterval - ($elapsedMs - $frameStartMs)
|
|
551
|
+
if ($wait -gt 0) { Start-Sleep -Milliseconds ([int]$wait) }
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
[STDataCollection.Icb]::USBIO_CloseDevice([uint32]0) | Out-Null
|
|
555
|
+
$sum = @{ ok = $true; action = "stream"; data = @{
|
|
556
|
+
frames_ok = $framesOk; frames_ff_gap = $framesGap; frames_io_fail = $framesIoFail;
|
|
557
|
+
frames_total = $frameNo; aborted = $aborted; abort_reason = $abortReason;
|
|
558
|
+
duration_actual_ms = [int]$sw.Elapsed.TotalMilliseconds } }
|
|
559
|
+
[Console]::WriteLine(($sum | ConvertTo-Json -Compress -Depth 5))
|
|
560
|
+
[Console]::Out.Flush()
|
|
561
|
+
exit 0
|
|
562
|
+
}
|
|
443
563
|
}
|
|
444
564
|
} catch {
|
|
445
565
|
Fail "UNKNOWN_ERROR" "bridge 异常: $($_.Exception.Message)" $_.ScriptStackTrace
|
|
@@ -9,9 +9,11 @@ import argparse
|
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
11
|
import sys
|
|
12
|
+
from datetime import datetime
|
|
12
13
|
|
|
13
14
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
14
|
-
from lib.ndtdbg import ChipSeaFxx, NdtBoxError, NdtBoxIo # noqa: E402
|
|
15
|
+
from lib.ndtdbg import (ChipSeaFxx, NdtBoxError, NdtBoxIo, decode_payload, # noqa: E402
|
|
16
|
+
interval_stats, parse_reg_spec)
|
|
15
17
|
|
|
16
18
|
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
17
19
|
|
|
@@ -195,6 +197,112 @@ def cmd_poll(io: NdtBoxIo, duration_ms: int, interval_ms: int, max_frames: int,
|
|
|
195
197
|
ok("poll", result, log=f"poll({duration_ms}ms, {interval_ms}ms): {len(frames)} frames")
|
|
196
198
|
|
|
197
199
|
|
|
200
|
+
def cmd_stream(io: NdtBoxIo, config: dict, specs: list, interval_ms, duration_s: float,
|
|
201
|
+
max_frames: int, out_path, decode: str, retry: int, fail_threshold,
|
|
202
|
+
require_target: bool) -> None:
|
|
203
|
+
"""高速连续采集 (只读): 桥进程内长驻 USB 会话, 单帧开销 = 一次 I2C 事务。
|
|
204
|
+
|
|
205
|
+
帧语义: OK / FF_GAP (读成功但全 FF, 总线高阻特征, 只统计不熔断) /
|
|
206
|
+
IO_FAIL (重试后仍无应答, 连续计数达 fail_threshold 熔断)。Ctrl+C 优雅收尾,
|
|
207
|
+
CSV 已采数据保留, summary 标记 aborted。
|
|
208
|
+
"""
|
|
209
|
+
# 默认值: lab.yaml ndtdbg.stream 段可覆盖内置默认 (均有默认值, 非必填)
|
|
210
|
+
stream_cfg = (config.get("ndtdbg") or {}).get("stream") or {}
|
|
211
|
+
if interval_ms is None:
|
|
212
|
+
interval_ms = int(stream_cfg.get("default_interval_ms", 100))
|
|
213
|
+
if fail_threshold is None:
|
|
214
|
+
fail_threshold = int(stream_cfg.get("fail_threshold", 50))
|
|
215
|
+
if duration_s <= 0 and max_frames <= 0:
|
|
216
|
+
fail("CONFIG_ERROR", "stream 需要 --duration-s 或 --max-frames 之一 > 0",
|
|
217
|
+
detail=f"duration_s={duration_s}, max_frames={max_frames}")
|
|
218
|
+
|
|
219
|
+
if not out_path:
|
|
220
|
+
out_path = os.path.join("lab", "streams",
|
|
221
|
+
datetime.now().strftime("ndtdbg_stream_%Y%m%d_%H%M%S.csv"))
|
|
222
|
+
abs_out = os.path.abspath(out_path)
|
|
223
|
+
windir = os.environ.get("WINDIR", r"C:\Windows")
|
|
224
|
+
if abs_out.lower().startswith(windir.lower() + os.sep):
|
|
225
|
+
fail("CONFIG_ERROR", "输出路径拒绝写入系统目录", abs_out)
|
|
226
|
+
out_dir = os.path.dirname(abs_out)
|
|
227
|
+
if out_dir:
|
|
228
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
229
|
+
|
|
230
|
+
spec = {
|
|
231
|
+
"regs": [{"addr": s["addr"], "len": s["len"], "name": s["name"]} for s in specs],
|
|
232
|
+
"interval_ms": interval_ms, "duration_s": float(duration_s), "max_frames": max_frames,
|
|
233
|
+
"retry": retry, "fail_threshold": fail_threshold, "require_target": require_target,
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
rows = {"ok": 0, "FF_GAP": 0, "IO_FAIL": 0} # 行级计数 (Ctrl+C 时桥内帧级统计丢失, 以此近似)
|
|
237
|
+
frame_times: list = [] # 帧完成 t_rel_ms 序列 (间隔统计)
|
|
238
|
+
state = {"frame": None, "t": None}
|
|
239
|
+
|
|
240
|
+
fh = open(abs_out, "w", encoding="utf-8", newline="")
|
|
241
|
+
try:
|
|
242
|
+
header = "frame,timestamp_iso,t_rel_ms,reg,raw_hex"
|
|
243
|
+
if decode != "hex":
|
|
244
|
+
header += "," + ",".join(f"{s['name']}_decoded" for s in specs)
|
|
245
|
+
fh.write(header + "\n")
|
|
246
|
+
|
|
247
|
+
def on_row(parts: list) -> None:
|
|
248
|
+
# 桥行协议: '#F', frame, t_rel_ms, reg, hex|'-', tag (hex 已空格分隔, 直接透传)
|
|
249
|
+
frame = int(parts[1]); t_rel = float(parts[2])
|
|
250
|
+
reg = parts[3]; hexs = parts[4]; tag = parts[5] if len(parts) > 5 else "OK"
|
|
251
|
+
line = (f"{frame},{datetime.now().isoformat(timespec='milliseconds')},"
|
|
252
|
+
f"{t_rel:g},{reg},{hexs}")
|
|
253
|
+
if decode != "hex":
|
|
254
|
+
line += "," + (decode_payload(hexs, decode) or "")
|
|
255
|
+
if tag != "OK":
|
|
256
|
+
line += f";{tag}"
|
|
257
|
+
fh.write(line + "\n")
|
|
258
|
+
if frame != state["frame"]:
|
|
259
|
+
if state["t"] is not None:
|
|
260
|
+
frame_times.append(state["t"])
|
|
261
|
+
state["frame"] = frame
|
|
262
|
+
state["t"] = t_rel
|
|
263
|
+
rows[tag] = rows.get(tag, 0) + 1
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
data = io.stream_run(spec, on_row=on_row)
|
|
267
|
+
except KeyboardInterrupt:
|
|
268
|
+
data = None # 桥被终止; 行级计数近似, CSV 已写数据保留
|
|
269
|
+
finally:
|
|
270
|
+
fh.close()
|
|
271
|
+
|
|
272
|
+
if state["t"] is not None:
|
|
273
|
+
frame_times.append(state["t"])
|
|
274
|
+
total = data.get("frames_total") if data else sum(rows.values())
|
|
275
|
+
summary = {
|
|
276
|
+
"csv_path": abs_out,
|
|
277
|
+
"frames_ok": data.get("frames_ok", rows["ok"]) if data else rows["ok"],
|
|
278
|
+
"frames_ff_gap": data.get("frames_ff_gap", rows["FF_GAP"]) if data else rows["FF_GAP"],
|
|
279
|
+
"frames_io_fail": data.get("frames_io_fail", rows["IO_FAIL"]) if data else rows["IO_FAIL"],
|
|
280
|
+
"frames_total": total if total else sum(rows.values()),
|
|
281
|
+
"interval_target_ms": interval_ms,
|
|
282
|
+
"duration_actual_s": round((data.get("duration_actual_ms") or 0) / 1000.0, 2) if data
|
|
283
|
+
else round((state["t"] or 0) / 1000.0, 2),
|
|
284
|
+
"aborted": bool(data.get("aborted")) if data else True,
|
|
285
|
+
"abort_reason": data.get("abort_reason") or ("interrupted" if data is None else ""),
|
|
286
|
+
}
|
|
287
|
+
summary.update(interval_stats(frame_times))
|
|
288
|
+
if decode != "hex":
|
|
289
|
+
summary["decode"] = decode
|
|
290
|
+
|
|
291
|
+
if summary["aborted"] and summary["abort_reason"] == "fail_threshold":
|
|
292
|
+
# 熔断: Fail-Closed 错误信封, 但 data 携带已采数据 summary
|
|
293
|
+
print(json.dumps({
|
|
294
|
+
"schema": "ndtdbg-result/v1", "ok": False, "error_class": "STREAM_ABORTED",
|
|
295
|
+
"error": f"连续 IO_FAIL 达到熔断阈值 {fail_threshold}, 已采数据保留于 CSV",
|
|
296
|
+
"data": summary,
|
|
297
|
+
}, ensure_ascii=False))
|
|
298
|
+
sys.exit(1)
|
|
299
|
+
payload = {"schema": "ndtdbg-result/v1", "ok": True, "action": "stream", "data": summary}
|
|
300
|
+
if data is None:
|
|
301
|
+
payload["log"] = "用户中断, 已采数据保留"
|
|
302
|
+
print(json.dumps(payload, ensure_ascii=False))
|
|
303
|
+
sys.exit(0)
|
|
304
|
+
|
|
305
|
+
|
|
198
306
|
def cmd_flash_bin(io: NdtBoxIo, config: dict, file_path: str, erase_ms: int, confirm: bool) -> None:
|
|
199
307
|
# 烧录 = 整片擦除 + 页写, 破坏性高于 write-reg, 同款双闸门
|
|
200
308
|
allowed = (config.get("ndtdbg") or {}).get("write_allowed", False)
|
|
@@ -249,6 +357,26 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
249
357
|
f.add_argument("--file", required=True,
|
|
250
358
|
help="固件文件 (.bin 自动 128 对齐补 0xFF; .nfw 去 256 头)")
|
|
251
359
|
f.add_argument("--erase-ms", type=int, default=2000, help="擦除等待 ms (对齐原厂 2000)")
|
|
360
|
+
|
|
361
|
+
st = sub.add_parser("stream", parents=[common],
|
|
362
|
+
help="高速连续采集寄存器组到 CSV (单会话长驻, 只读)")
|
|
363
|
+
st.add_argument("--regs", nargs="+", default=["0xEE:8"],
|
|
364
|
+
help="寄存器 spec addr:len[:name], 如 0xEE:8:ch 0x20:138:event")
|
|
365
|
+
st.add_argument("--interval-ms", type=int, default=None,
|
|
366
|
+
help="目标采样间隔 ms (默认 lab.yaml ndtdbg.stream.default_interval_ms 或 100)")
|
|
367
|
+
st.add_argument("--duration-s", type=float, default=0,
|
|
368
|
+
help="采集总时长秒 (与 --max-frames 至少一个 > 0)")
|
|
369
|
+
st.add_argument("--max-frames", type=int, default=0,
|
|
370
|
+
help="达到 N 帧后停止 (与 --duration-s 二选一)")
|
|
371
|
+
st.add_argument("--out", help="CSV 输出路径 (默认 lab/streams/ndtdbg_stream_<ts>.csv)")
|
|
372
|
+
st.add_argument("--decode", choices=["hex", "int16le", "uint16le", "int8", "uint8"],
|
|
373
|
+
default="hex", help="decoded 附加列的解码模式 (默认仅 raw hex)")
|
|
374
|
+
st.add_argument("--retry", type=int, default=2,
|
|
375
|
+
help="每帧每寄存器重试次数 (全 FF / IO 错误, 默认 2)")
|
|
376
|
+
st.add_argument("--fail-threshold", type=int, default=None,
|
|
377
|
+
help="连续 IO_FAIL 熔断阈值 (默认 lab.yaml ndtdbg.stream.fail_threshold 或 50; 0=不熔断)")
|
|
378
|
+
st.add_argument("--require-target", action="store_true",
|
|
379
|
+
help="采集前探测 0x10, 无有效芯片应答则 TARGET_MISMATCH")
|
|
252
380
|
return p
|
|
253
381
|
|
|
254
382
|
|
|
@@ -273,6 +401,11 @@ def main(argv=None) -> None:
|
|
|
273
401
|
cmd_poll(io, args.duration_ms, args.interval_ms, args.max_frames, args.full)
|
|
274
402
|
elif args.cmd == "flash-bin":
|
|
275
403
|
cmd_flash_bin(io, config, args.file, args.erase_ms, args.confirm)
|
|
404
|
+
elif args.cmd == "stream":
|
|
405
|
+
specs = [parse_reg_spec(s) for s in args.regs]
|
|
406
|
+
cmd_stream(io, config, specs, args.interval_ms, args.duration_s,
|
|
407
|
+
args.max_frames, args.out, args.decode, args.retry,
|
|
408
|
+
args.fail_threshold, args.require_target)
|
|
276
409
|
else: # pragma: no cover
|
|
277
410
|
fail("CONFIG_ERROR", f"unknown cmd: {args.cmd}")
|
|
278
411
|
except NdtBoxError as exc:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
|
-
Dual-Tier MCP setup and registration helper (v0.1
|
|
3
|
+
Dual-Tier MCP setup and registration helper (v0.2.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.1
|
|
245
|
+
Write-Host " FirmwareLoop Dual-Tier MCP & Agent Setup Helper (v0.2.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
|