串口助手补残行封顶与会话边界复位、截头按行对齐、open 保持 dtr/rts;判定加命中行/顺序/耗时/JSON 与产物清理

This commit is contained in:
2026-10-10 03:17:16 +08:00
parent fccf99bc90
commit 61a6061950
8 changed files with 410 additions and 46 deletions
+78 -8
View File
@@ -8,12 +8,15 @@
标记——芯片里跑的必须就是这份构建(--no-fw-check 关闭)
3. wait-idle:RX 静默 >= settle-ms 收尾;首字节超过 --first-wait-ms 未到 → no_data
(快速失败);整体 max-wait-ms 超时
4. 从日志提取 [RX] 载荷行(剥掉 "[RX] <ISO时间戳> " 前缀)
4. 从日志提取 [RX] 载荷行(剥掉 "[RX] <ISO时间戳> " 前缀),并给每行加 `[+Nms]` 相对
偏移(daemon 毫秒时基,本次第一行 = +0ms;--raw-log 出纯载荷)。MCU 侧时间戳只有
秒级、开机头几行还没时钟,量不了耗时——这个前缀就是耗时口径
4.5 烧录回读校验:MDK-ARM\\reset.bat cmp(= ST-LINK_CLI -CmpFile 全片逐字节比对,
`No difference found` 才算真写进去;--no-verify-flash 关闭)。
**必须排在采集之后**:实测 -CmpFile 连接时会复位目标(板子当场重打一遍开机日志)
5. verify_log.py 标记校验(含启动锚点计数:`reset cause:` 出现 0 次或 >N 次 = FAIL,
N 由 --max-boot-count 给,默认 1;--expect-reset-cause 可对第一个锚点做原因白名单)
N 由 --max-boot-count 给,默认 1;--expect-reset-cause 可对第一个锚点做原因白名单;
--require-order 查标记先后顺序;--done-marker + --max-boot-ms 量开机耗时)
6. (仅当 --require-selftest-pass)自检记录联合判定(S7):
--selftest-log 提供本地副本则直接用它,否则从设备 FTP 抓取 /log/selftest.log
(lftpd :21 匿名);判定:文件缺失 / 0 字节 = 无故障 → PASS,有 64B 记录 = FAIL
@@ -41,6 +44,9 @@
[--fw-hex <构建产物 hex>] [--no-fw-check]
[--no-verify-flash] [--readback-timeout 120]
[--max-boot-count 1] [--expect-reset-cause 'software;power-on']
[--require-order 'App init OK>Network Init>net init: OK']
[--done-marker 'net init: OK'] [--max-boot-ms 0]
[--raw-log] [--keep-out 20]
--port 缺省取 .opencode/serial_config.json 的 portName(单一配置源)
@@ -166,6 +172,26 @@ def _ftp_quit(ftp):
pass
def _prune_out_dir(out_dir, keep):
"""只保留最近 N 份产物(`boot_log_*.txt` / `selftest_log_*.log`)。
这些文件按 `%Y%m%d_%H%M%S` 命名、每次 @test 各留一份,旧实现从不清理——
本机实测 out 目录已堆到 31 份 boot_log + 7 份 selftest_log(2026-10-10)。
文件名含时间戳 → 按名字排序即按时间排序。
"""
if keep <= 0:
return 0
removed = 0
for pat in ("boot_log_*.txt", "selftest_log_*.log"):
for p in sorted(out_dir.glob(pat), key=lambda q: q.name)[:-keep]:
try:
p.unlink()
removed += 1
except OSError:
pass
return removed
def _fetch_selftest_log(device_ip, out_dir, stamp):
"""从设备 FTP 抓取 /log/selftest.log(lftpd :21 匿名,根目录 = 卷根)。
@@ -258,6 +284,20 @@ def main(argv=None):
ap.add_argument("--expect-reset-cause", default="",
help="';'-separated substrings the FIRST reset cause must match "
"(case-insensitive); empty = report only, no verdict")
ap.add_argument("--require-order", default="",
help="'>'-separated markers that must appear in that line order "
"(catches out-of-order boot steps); empty = off")
ap.add_argument("--done-marker", default="net init: OK",
help="marker whose line marks boot completion (for --max-boot-ms); "
"empty = don't measure")
ap.add_argument("--max-boot-ms", type=int, default=0,
help="fail if time from first line to --done-marker exceeds N ms "
"(0 = measure and report only)")
ap.add_argument("--raw-log", action="store_true",
help="write payload-only boot log (no '[+Nms]' relative-time prefix)")
ap.add_argument("--keep-out", type=int, default=20,
help="keep the newest N boot_log_*/selftest_log_* files in --out-dir "
"(0 = keep everything)")
args = ap.parse_args(argv)
# 串口助手 CLI 用当前解释器(AGENTS.md 统一入口 = 仓库 .venv python)
@@ -289,6 +329,10 @@ def main(argv=None):
out_dir = Path(args.out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
pruned = _prune_out_dir(out_dir, args.keep_out)
if pruned:
print("OUT: pruned {} old artifact file(s) in {} (keep={})".format(
pruned, out_dir, args.keep_out))
stamp = datetime.now().strftime("%Y%m%d_%H%M%S")
boot_txt = out_dir / "boot_log_{}.txt".format(stamp)
@@ -371,21 +415,44 @@ def main(argv=None):
return 2
# 5. 提取 [RX] 载荷行(剥掉 "[RX] <ISO时间戳> " 前缀)
# 默认给每行加 `[+Nms]` 相对偏移(daemon 毫秒时基,本次采集第一行 = +0ms)。
# 为什么加:MCU 侧时间戳只有**秒级**,且开机头几行时钟还没起(`----/--/--`),
# 根本量不了耗时;有了它 verify_log 才能查"能开机但慢了几秒"(--max-boot-ms)。
# --raw-log 恢复旧格式(纯载荷)。
data, out = tool("log", "--tail", "0", "--timeout", "15")
if data is None:
print("RUN_TEST_ERROR: log read failed: {}".format(out))
return 2
rx = []
t0 = None
for l in data.get("lines", []):
if isinstance(l, str) and l.startswith("[RX] "):
rest = l[5:]
sp = rest.find(" ")
rx.append(rest[sp + 1:] if sp >= 0 else rest)
if not (isinstance(l, str) and l.startswith("[RX] ")):
continue
rest = l[5:]
sp = rest.find(" ")
ts_txt = rest[:sp] if sp >= 0 else ""
payload = rest[sp + 1:] if sp >= 0 else rest
if args.raw_log:
rx.append(payload)
continue
ts = None
if ts_txt:
try:
ts = datetime.fromisoformat(ts_txt)
except ValueError:
ts = None
if ts is None:
rx.append(payload) # 时间戳读不出来就不加前缀(不猜)
continue
if t0 is None:
t0 = ts
rx.append("[+{}ms] {}".format(int((ts - t0).total_seconds() * 1000), payload))
if not rx:
print("RUN_TEST_ERROR: no [RX] lines in log (device silent?)")
return 2
boot_txt.write_bytes(("\r\n".join(rx) + "\r\n").encode("utf-8"))
print("Boot log: {} (lines={} gen={})".format(boot_txt, len(rx), data.get("gen")))
print("Boot log: {} (lines={} gen={} ts={})".format(
boot_txt, len(rx), data.get("gen"), "raw" if args.raw_log else "+ms"))
# 5.6 烧录回读校验(AGENTS.md 铁律:`No difference found` 才算真写进去)。
# 必须排在**采集之后**:实测 -CmpFile 连接时会复位目标(板子当场重打一遍开机
@@ -422,7 +489,10 @@ def main(argv=None):
selftest_pass=args.require_selftest_pass,
selftest_log=selftest_local,
max_boot_count=args.max_boot_count,
expect_reset_cause=args.expect_reset_cause)
expect_reset_cause=args.expect_reset_cause,
require_order=args.require_order,
done_marker=args.done_marker,
max_boot_ms=args.max_boot_ms)
print("RUN_TEST_RC={}".format(rc))
return rc