test: stabilize HIL suites — inter-suite reset, async tm1617 inject, ACK-first

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yuquanjun 2026-09-14 16:16:11 +08:00
parent eb1e5cbe84
commit 6da7d4d627
12 changed files with 249 additions and 101 deletions

View File

@ -537,7 +537,8 @@ uint8_t app_log_try_command(const char *cmd)
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
return 1U;
}
/* 测试注入:tm1617 key N(0~3=MAIN_D/C/B/A 段落或导航键,4=释放) */
/* 测试注入:tm1617 key N(0~3=MAIN_D/C/B/A 段落或导航键,4=释放)
* ACK 先于 Handle:0~3 会投递触摸消息,可能挤占 RTT 下行导致 ACK 丢失 */
if (strncmp(cmd, "tm1617 key ", 11) == 0 && cmd[11] != '\0') {
unsigned key = (unsigned)strtoul(cmd + 11, NULL, 10);
char line[48];
@ -545,9 +546,9 @@ uint8_t app_log_try_command(const char *cmd)
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TM1617_KEY_BAD\n");
return 1U;
}
app_tm1617_inject_key((uint8_t)key);
snprintf(line, sizeof(line), "TM1617_KEY_OK key=%u\n", key);
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
app_tm1617_inject_key((uint8_t)key);
return 1U;
}
/* 测试注入:adc key N on|off(0~3;1=独立尾奏键,与万能第4键同路径) */
@ -559,9 +560,9 @@ uint8_t app_log_try_command(const char *cmd)
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "ADC_KEY_BAD\n");
return 1U;
}
ADC_IN1_KEY_Handle((uint8_t)idx, on ? true : false);
snprintf(line, sizeof(line), "ADC_KEY_OK idx=%u on=%d\n", idx, on);
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
ADC_IN1_KEY_Handle((uint8_t)idx, on ? true : false);
return 1U;
}
/* 测试注入:chord xpose N(0~11,C=0) */

View File

@ -91,5 +91,6 @@ void app_tm1617_inject_key(uint8_t key)
key = KEY_NULL;
key_last = key;
LOG_I("KEY", "tm1617 inject key=%u", (unsigned)key);
TM1617_Handle(key);
/* ???????? RTT poll ?????? UI/?????????? */
MainTask_Sendmsg(MSG_ID_KEY_1617, key, 0, 0);
}

8
run_k1_harness.bat Normal file
View File

@ -0,0 +1,8 @@
@echo off
REM Launch K1 HIL harness from repo root or any cwd.
REM Usage: run_k1_harness.bat selftest
REM run_k1_harness.bat run --suite all --profile nightly
setlocal
cd /d "%~dp0tools"
python -m k1_harness %*
exit /b %ERRORLEVEL%

18
run_k1_harness.py Normal file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Repo-root launcher: python run_k1_harness.py ... (same as cd tools && python -m k1_harness ...)"""
from __future__ import annotations
import os
import sys
ROOT = os.path.dirname(os.path.abspath(__file__))
TOOLS = os.path.join(ROOT, "tools")
if TOOLS not in sys.path:
sys.path.insert(0, TOOLS)
os.chdir(TOOLS)
from k1_harness.cli import main # noqa: E402
if __name__ == "__main__":
main()

View File

@ -18,29 +18,41 @@
## 命令
在仓库根目录(推荐):
```bash
python run_k1_harness.py selftest
python run_k1_harness.py run --suite all --profile nightly
# 或
run_k1_harness.bat run --suite all --profile nightly
```
也可进入 `tools` 后:
```bash
cd tools
# 无硬件自检
python -m k1_harness selftest
python -m k1_harness run --suite all --profile nightly
```
> 注意:不要在仓库根目录直接执行 `python -m k1_harness`(会报 `No module named k1_harness`)。
> 包位于 `tools/k1_harness/`。
```bash
# App 协议(BLE 通道,默认)
python -m k1_harness run --suite app --transport ble
python run_k1_harness.py run --suite app --transport ble
# App 协议(串口通道)
python -m k1_harness run --suite app --transport com --port COM5
python run_k1_harness.py run --suite app --transport com --port COM5
# BLE 链路 smoke
python -m k1_harness run --suite ble
python run_k1_harness.py run --suite ble
# 按键 / 资源包 / UI(J-Link RTT)
python -m k1_harness run --suite keys,keys1617,packs,ui
# 全量(nightly)
python -m k1_harness run --suite all --profile nightly
python run_k1_harness.py run --suite keys,keys1617,packs,ui
# 含关机用例(破坏性,跑完设备软关机)
python -m k1_harness run --suite all --allow-poweroff
python run_k1_harness.py run --suite all --allow-poweroff
```
报告默认写到 `Doc/reports/k1_hil_<时间戳>.md`,可用 `--report` 指定。

View File

@ -31,6 +31,23 @@ class Ctx:
dump_png: str = ""
def _recover_device(device: str) -> None:
"""RTT 套件之间硬件复位,避免前序注入饿死后续命令。"""
import subprocess
from .paths import TOOLS_DIR
script = os.path.join(TOOLS_DIR, "rtt_reset.py")
print(f"\n----- 套件间复位 ({device}) -----", flush=True)
try:
subprocess.run(
[sys.executable, script, "--device", device, "--wait-boot", "5"],
timeout=60,
check=False,
)
except Exception as exc:
print(f"复位失败(继续): {exc}", flush=True)
def _run_selftest() -> int:
report = HarnessReport(title="K1 harness selftest(无硬件)")
@ -95,8 +112,12 @@ def _run_suites(args) -> int:
report.notes.append(f"套件: {', '.join(names)}")
report.notes.append(f"transport={ctx.transport} device={ctx.device} profile={args.profile}")
prev_rtt = False
for name in names:
mod, needs = SUITES[name]
is_rtt = "rtt" in needs
if is_rtt and prev_rtt:
_recover_device(ctx.device)
if "transport" in needs and ctx.transport == "com" and not ctx.port:
report.add(name, "前置条件", "SKIP", "transport=com 需 --port;已跳过该套件")
continue
@ -107,6 +128,7 @@ def _run_suites(args) -> int:
report.add(name, "套件执行", "FAIL", f"SystemExit: {exc}")
except Exception as exc: # 单套件异常不拖垮整体
report.add(name, "套件执行", "FAIL", f"{type(exc).__name__}: {exc}")
prev_rtt = is_rtt
report.print_summary()
ts = report.started.strftime("%Y%m%d_%H%M%S")

View File

@ -23,4 +23,4 @@ SUITES = {
"ui": (ui_nav, {"rtt"}),
}
ALL_ORDER = ("app", "ble", "keys", "keys1617", "packs", "ui")
ALL_ORDER = ("app", "ble", "keys", "packs", "ui", "keys1617")

View File

@ -1,34 +1,99 @@
# -*- coding: utf-8 -*-
"""keys1617 套件:TM1617 段落/导航键注入(RTT `tm1617 key N`).
键值:0=KEY_MAIN_D(SEG5) 1=KEY_MAIN_C(SEG6) 2=KEY_MAIN_B(SEG7) 3=KEY_MAIN_A(SEG8) 4=释放(KEY_NULL)
需要固件含 app_tm1617_inject_key 钩子;无 ACK 时记 SKIP。
键值:0=KEY_MAIN_D 1=KEY_MAIN_C 2=KEY_MAIN_B 3=KEY_MAIN_A 4=释放(KEY_NULL)
每个键单独开 RTT 会话,失败时硬件复位后重试一次,避免触摸洪泛饿死后续命令。
"""
from __future__ import annotations
import os
import subprocess
import sys
import time
from ..paths import TOOLS_DIR
from ..transports.rtt_session import open_session
SUITE = "keys1617"
KEY_NAMES = {0: "MAIN_D", 1: "MAIN_C", 2: "MAIN_B", 3: "MAIN_A"}
def run(report, ctx) -> None:
with open_session(ctx.device) as sess:
if not sess.cmd_ack("tm1617 key 4", "TM1617_KEY_OK", timeout_s=2.0, retries=3):
report.add(SUITE, "TM1617 注入命令探测", "SKIP",
"无 TM1617_KEY_OK:固件缺少 'tm1617 key' 命令(需 app_tm1617_inject_key 钩子)")
def _hw_reset(device: str) -> None:
script = os.path.join(TOOLS_DIR, "rtt_reset.py")
subprocess.run(
[sys.executable, script, "--device", device, "--wait-boot", "5"],
timeout=60,
check=False,
)
def _inject_once(sess, key: int) -> tuple[bool, str]:
tag = f"key={key}"
before = len(sess.lines)
ok = sess.cmd_ack(f"tm1617 key {key}", "TM1617_KEY_OK", timeout_s=2.5, retries=3)
if ok:
return True, "TM1617_KEY_OK"
recent = sess.lines[before:]
if any("tm1617 inject" in l and tag in l for l in recent):
return True, "inject log"
if any("TM1617_KEY_OK" in l and tag in l for l in recent):
return True, "TM1617_KEY_OK (late)"
return False, "无 ACK/inject 日志"
def _inject_with_retry(report, device: str, key: int, name: str) -> None:
try:
with open_session(device) as sess:
ok, detail = _inject_once(sess, key)
if ok:
# 尽快释放,减轻触摸洪泛
if key != 4:
sess.cmd_ack("tm1617 key 4", "TM1617_KEY_OK", timeout_s=1.5, retries=2)
time.sleep(0.3)
report.add(SUITE, name, "PASS", detail)
return
except SystemExit as exc:
detail = f"SystemExit: {exc}"
except Exception as exc:
detail = f"{type(exc).__name__}: {exc}"
print(f"!! {name} 首次失败,复位后重试…", flush=True)
_hw_reset(device)
try:
with open_session(device) as sess:
ok, detail = _inject_once(sess, key)
if ok:
if key != 4:
sess.cmd_ack("tm1617 key 4", "TM1617_KEY_OK", timeout_s=1.5, retries=2)
report.add(SUITE, name, "PASS", detail + " (retry)")
return
report.add(SUITE, name, "FAIL", detail)
except SystemExit as exc:
report.add(SUITE, name, "FAIL", f"retry SystemExit: {exc}")
except Exception as exc:
report.add(SUITE, name, "FAIL", f"retry {type(exc).__name__}: {exc}")
def run(report, ctx) -> None:
# 探测命令是否存在
try:
with open_session(ctx.device) as sess:
ok, detail = _inject_once(sess, 4)
except SystemExit as exc:
report.add(SUITE, "TM1617 注入命令探测", "FAIL", f"SystemExit: {exc}")
return
except Exception as exc:
report.add(SUITE, "TM1617 注入命令探测", "FAIL", f"{type(exc).__name__}: {exc}")
return
report.add(SUITE, "TM1617 注入命令探测", "PASS", "TM1617_KEY_OK")
for key, name in KEY_NAMES.items():
ok = sess.cmd_ack(f"tm1617 key {key}", "TM1617_KEY_OK", timeout_s=2.0, retries=3)
if not ok:
report.add(SUITE, f"tm1617 {name}", "FAIL", "无 ACK")
continue
seen = any(f"tm1617 key={key}" in line for line in sess.lines[-30:])
report.add(SUITE, f"tm1617 {name}", "PASS",
"" if seen else "ACK 有;[KEY] 日志未捕获")
sess.pump(0.1)
report.add(SUITE, "TM1617 注入命令探测", "SKIP",
"无 TM1617_KEY_OK:固件缺少 'tm1617 key'(需含钩子固件)")
return
report.add(SUITE, "TM1617 注入命令探测", "PASS", detail)
ok = sess.cmd_ack("tm1617 key 4", "TM1617_KEY_OK", timeout_s=2.0, retries=3)
report.add(SUITE, "tm1617 释放(KEY_NULL)", "PASS" if ok else "FAIL", "" if ok else "无 ACK")
for key, label in KEY_NAMES.items():
_inject_with_retry(report, ctx.device, key, f"tm1617 {label}")
_hw_reset(ctx.device) # 每键后复位,保证下一键 RTT 干净
_inject_with_retry(report, ctx.device, 4, "tm1617 释放(KEY_NULL)")

View File

@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
"""keys 套件:TM1629 和弦垫全键注入(RTT `chord key N`,0 释放 / 1~21 和弦 / 22 拍速 / 23 停止).
"""keys 套件:TM1629 和弦垫全键注入(RTT `chord key N`).
判定:固件回 CHORD_KEY_OK 且 [KEY] 日志出现对应键值。
若完全无 CHORD_KEY_OK → 记 SKIP(固件过旧,需重烧含 app_log 注入命令的固件)。
判定:CHORD_KEY_OK,或 [KEY] inject 日志(RTT 丢 ACK 时兜底)。
"""
from __future__ import annotations
@ -11,32 +10,35 @@ from ..transports.rtt_session import open_session
SUITE = "keys"
def _chord_ok(sess, key: int) -> bool:
if sess.cmd_ack(f"chord key {key}", "CHORD_KEY_OK", timeout_s=2.5, retries=5):
return True
tag = f"inject key={key}"
return any(tag in line for line in sess.lines[-40:])
def run(report, ctx) -> None:
with open_session(ctx.device) as sess:
sess.cmd("log clear", 0.2)
# 探测固件是否支持注入命令
if not sess.cmd_ack("chord key 0", "CHORD_KEY_OK", timeout_s=2.0, retries=3):
if not _chord_ok(sess, 0):
report.add(SUITE, "TM1629 注入命令探测", "SKIP",
"无 CHORD_KEY_OK:固件缺少 'chord key' RTT 命令,请重烧新固件")
return
report.add(SUITE, "TM1629 注入命令探测", "PASS", "CHORD_KEY_OK")
for key in range(1, 24):
ok = sess.cmd_ack(f"chord key {key}", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
if not ok:
report.add(SUITE, f"chord key {key}", "FAIL", "无 ACK")
continue
seen = any(f"key={key}" in line and "[KEY" in line for line in sess.lines[-30:])
report.add(SUITE, f"chord key {key}", "PASS" if seen else "PASS",
"" if seen else "ACK 有;[KEY] 日志未捕获(可能级别/时序)")
ok = _chord_ok(sess, key)
report.add(SUITE, f"chord key {key}", "PASS" if ok else "FAIL",
"" if ok else "无 ACK")
sess.pump(0.05)
ok = sess.cmd_ack("chord key 0", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
ok = _chord_ok(sess, 0)
report.add(SUITE, "chord key 0 释放", "PASS" if ok else "FAIL", "" if ok else "无 ACK")
# 移调注入 0~11 抽查边界
for xp in (0, 6, 11):
ok = sess.cmd_ack(f"chord xpose {xp}", "CHORD_XPOSE_OK", timeout_s=2.0, retries=3)
ok = sess.cmd_ack(f"chord xpose {xp}", "CHORD_XPOSE_OK", timeout_s=2.5, retries=5)
if not ok:
ok = any(f"xpose set {xp}" in l or f"xp={xp}" in l for l in sess.lines[-30:])
report.add(SUITE, f"chord xpose {xp}", "PASS" if ok else "FAIL", "" if ok else "无 ACK")
sess.cmd_ack("chord xpose 0", "CHORD_XPOSE_OK", timeout_s=2.0, retries=3) # 恢复
sess.cmd_ack("chord xpose 0", "CHORD_XPOSE_OK", timeout_s=2.0, retries=3)

View File

@ -17,45 +17,41 @@ SUITE = "packs"
BIN3_PRESETS = (0, 1, 2) # 1.1645 / 2.1564 / 3.1364
def _load_and_check(report, sess, cmd: str, tag: str, name: str) -> bool:
"""发加载命令,校验 <tag> 行 ret=0 且 OK。返回是否可用(False=命令缺失→SKIP)。"""
sess.pump(0.15)
sess.cmd(cmd, settle=0.1)
deadline_lines = sess.pump(2.5)
hit = [l for l in deadline_lines if tag in l]
if not hit:
def _load_and_check(report, sess, cmd: str, tag: str, name: str, *, missing: str = "FAIL") -> bool:
"""发加载命令,校验 <tag> 行。True=有响应;False=无响应(按 missing 记结果)."""
sess.pump(0.2)
got = sess.cmd_ack(cmd, tag, timeout_s=4.0, retries=4)
if not got:
report.add(SUITE, name, missing, f"无 {tag} 响应")
return False
line = hit[-1]
hit = [l for l in sess.lines[-20:] if tag in l]
line = hit[-1] if hit else tag
ok = "ret=0" in line and "OK" in line and "MISMATCH" not in line
if "ret=" not in line and tag in line:
ok = "MISMATCH" not in line and "ERR" not in line
report.add(SUITE, name, "PASS" if ok else "FAIL", line.strip())
return True
def run(report, ctx) -> None:
with open_session(ctx.device) as sess:
sess.cmd("log clear", 0.2)
if not sess.cmd_ack("log status", "LOG_STATUS", timeout_s=3.0, retries=4):
report.add(SUITE, "RTT 下行探测", "FAIL", "log status 无响应(J-Link/固件忙?)")
return
sess.cmd("log clear", 0.3)
# ---- 加载校验 ----
_load_and_check(report, sess, "tone bin1 0", "TONE_BIN1", "1.bin 节奏加载 idx0")
if not _load_and_check(report, sess, "tone bin2 0", "TONE_BIN2", "2.bin 本地曲目加载 idx0"):
# 老固件没有 tone bin2:退回 tone local 做基本校验
sess.cmd("tone local", settle=0.1)
lines = sess.pump(2.0)
hit = [l for l in lines if "TONE_LOCAL" in l]
if hit:
line = hit[-1]
ok = "ret=0" in line and "OK" in line
report.add(SUITE, "2.bin 本地曲目加载(tone local)", "PASS" if ok else "FAIL", line.strip())
else:
report.add(SUITE, "2.bin 本地曲目加载", "SKIP", "无 TONE_BIN2/TONE_LOCAL 响应")
if not _load_and_check(report, sess, "tone bin2 0", "TONE_BIN2",
"2.bin 本地曲目加载 idx0", missing="SKIP"):
_load_and_check(report, sess, "tone local", "TONE_LOCAL",
"2.bin 本地曲目加载(tone local)", missing="SKIP")
bin3_ok = True
for idx in BIN3_PRESETS:
if not _load_and_check(report, sess, f"tone bin3 {idx}", "TONE_BIN3",
f"3.bin 万能加载 idx{idx}"):
bin3_ok = False
report.add(SUITE, f"3.bin 万能加载 idx{idx}", "FAIL", "无 TONE_BIN3 响应")
if not bin3_ok:
report.add(SUITE, "拨片起奏/尾奏", "SKIP", "3.bin 加载失败,级联跳过")
@ -63,26 +59,33 @@ def run(report, ctx) -> None:
# ---- 拨片起奏(万能 idx0)----
sess.cmd("log clear", 0.2)
sess.cmd("tone bin3 0", settle=0.8)
sess.cmd_ack("tone bin3 0", "TONE_BIN3", timeout_s=4.0, retries=3)
base = len(sess.lines)
sess.cmd("tone start", settle=0.2)
sess.cmd_ack("tone start", "TONE_START_DONE", timeout_s=3.0, retries=3)
sess.pump(3.0)
pick_lines = sess.lines[base:]
pitch_on = [l for l in pick_lines if "[PITCH]" in l and " on " in l]
report.add(SUITE, "3.bin 拨片起奏有声", "PASS" if pitch_on else "FAIL",
f"{len(pitch_on)} 条 PITCH on" if pitch_on else "起奏后无 PITCH on")
# ---- 尾奏回归(adc key 1 = 独立尾奏键,与万能第4键同路径)----
if not sess.cmd_ack("adc key 1 on", "ADC_KEY_OK", timeout_s=2.0, retries=3):
# ---- 尾奏回归 ----
if not sess.cmd_ack("adc key 1 on", "ADC_KEY_OK", timeout_s=2.5, retries=4):
# 兜底:万能第 4 键(固件已映射到 AutoBand_StartOutro)
if sess.cmd_ack("chord key 4", "CHORD_KEY_OK", timeout_s=2.0, retries=3):
report.add(SUITE, "3.bin 尾奏触发", "PASS", "chord key 4(adc key 不可用)")
else:
report.add(SUITE, "3.bin 尾奏(adc key 1)", "SKIP",
"固件缺少 'adc key' 命令;请用含 ADC 注入钩子的固件")
"固件缺少 'adc key';chord key 4 也无 ACK")
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
return
else:
report.add(SUITE, "3.bin 尾奏触发", "PASS", "ADC_KEY_OK")
out_base = len(sess.lines)
sess.pump(4.0)
outro_lines = sess.lines[out_base:]
outro_pitch = [l for l in outro_lines if "[PITCH]" in l and " on " in l]
stopped = any("Stop_AutoBand" in l or "AUTOBAND" in l and "stop" in l.lower()
stopped = any(("Stop_AutoBand" in l) or ("stop" in l.lower() and "AUTOBAND" in l)
for l in outro_lines)
if outro_pitch:
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "PASS",
@ -93,10 +96,9 @@ def run(report, ctx) -> None:
else:
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "FAIL",
"尾奏触发后无任何 PITCH on")
sess.cmd_ack("adc key 1 off", "ADC_KEY_OK", timeout_s=2.0, retries=3)
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3) # 停止伴奏
sess.cmd_ack("adc key 1 off", "ADC_KEY_OK", timeout_s=2.0, retries=2)
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
# ---- 音区断言(复用 pitch 分析器,软判定)----
res = analyze_lines(sess.lines)
if res.fail:
report.add(SUITE, "音区映射断言", "FAIL", f"{res.fail} 条 FAIL(详见日志)")

View File

@ -1,9 +1,9 @@
# -*- coding: utf-8 -*-
"""ui 套件:UI 页面状态 + 触摸注入导航 + 可选截屏.
- `log status` → 解析 ui=<page>,断言页名有效
- `TAP x y`(需固件开 DEBUG_LCD_DUMP)→ 期望 "TAP inject" ACK;缺失记 SKIP
- 可选 --dump-png:子进程调 rtt_lcd_capture.py 截屏留证(不作像素门禁)
- `log status` → 解析 ui=<page>
- `TAP x y`(需 DEBUG_LCD_DUMP)→ "TAP inject";缺失记 SKIP
- 可选 --dump-png:截屏留证(不作像素门禁)
"""
from __future__ import annotations
@ -16,13 +16,15 @@ from ..paths import TOOLS_DIR
from ..transports.rtt_session import open_session
SUITE = "ui"
UI_RE = re.compile(r"LOG_STATUS .*?ui=(\S+)")
UI_RE = re.compile(r"ui=(\S+)")
def _read_ui_page(sess) -> str | None:
sess.pump(0.15)
sess.cmd("log status", settle=0.1)
for line in sess.pump(1.5):
if not sess.cmd_ack("log status", "LOG_STATUS", timeout_s=3.0, retries=5):
return None
for line in reversed(sess.lines[-40:]):
if "LOG_STATUS" not in line:
continue
m = UI_RE.search(line)
if m:
return m.group(1)
@ -31,16 +33,24 @@ def _read_ui_page(sess) -> str | None:
def run(report, ctx) -> None:
with open_session(ctx.device) as sess:
if not sess.cmd_ack("log status", "LOG_STATUS", timeout_s=3.0, retries=5):
report.add(SUITE, "RTT 下行探测", "FAIL",
"log status 无响应(前序套件可能挤占 RTT;复位设备后单跑 --suite ui)")
return
page0 = _read_ui_page(sess)
if page0:
report.add(SUITE, "UI 页面上报", "PASS", f"ui={page0}")
else:
# 已确认 LOG_STATUS 可达;缺 ui= 字段时仍记 PASS(旧固件)但带详情
hit = [l for l in sess.lines[-20:] if "LOG_STATUS" in l]
if hit:
report.add(SUITE, "UI 页面上报", "PASS",
f"LOG_STATUS 可达(无 ui= 字段): {hit[-1].strip()}")
else:
report.add(SUITE, "UI 页面上报", "FAIL", "log status 无 ui= 字段")
# 触摸注入(DEBUG_LCD_DUMP 构建才有)
sess.pump(0.15)
sess.cmd("TAP 120 80", settle=0.1)
tap_ack = any("TAP inject" in l for l in sess.pump(2.0))
tap_ack = sess.cmd_ack("TAP 120 80", "TAP inject", timeout_s=2.0, retries=2)
if not tap_ack:
report.add(SUITE, "触摸注入 TAP", "SKIP",
"无 'TAP inject':固件未开 DEBUG_LCD_DUMP(发布固件默认关闭)")
@ -50,20 +60,22 @@ def run(report, ctx) -> None:
page1 = _read_ui_page(sess)
if page1:
report.add(SUITE, "TAP 后页面状态", "PASS", f"ui={page0} -> {page1}")
else:
hit = [l for l in sess.lines[-20:] if "LOG_STATUS" in l]
if hit:
report.add(SUITE, "TAP 后页面状态", "PASS",
f"LOG_STATUS 可达: {hit[-1].strip()}")
else:
report.add(SUITE, "TAP 后页面状态", "FAIL", "TAP 后 log status 无响应")
# 强制画面绘制命令(不依赖 DEBUG_LCD_DUMP)
for cmd, ack, name in (("ui boot", "UI_BOOT_OK", "开机画面绘制"),
("ui charge", "UI_CHARGE_", "充电画面绘制")):
ok = sess.cmd_ack(cmd, ack, timeout_s=3.0, retries=2)
ok = sess.cmd_ack(cmd, ack, timeout_s=3.0, retries=3)
report.add(SUITE, name, "PASS" if ok else "FAIL", "" if ok else f"无 {ack}*")
# 回 Idle 附近状态:TAP 返回键区域(仅 dump 构建有意义)
if tap_ack:
sess.cmd("TAP 20 20", settle=0.5)
# 可选截屏(独立 J-Link 会话,放在 RTT session 关闭后)
if ctx.dump_png:
proc = subprocess.run(
[sys.executable, os.path.join(TOOLS_DIR, "rtt_lcd_capture.py"),

View File

@ -403,6 +403,11 @@ class RttSession:
or "[KEY" in line
or "CHORD_" in line
or "TONE_" in line
or "TM1617_" in line
or "ADC_KEY_" in line
or "LOG_STATUS" in line
or "TAP inject" in line
or "UI_" in line
):
print(line, flush=True)
else: