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); SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
return 1U; 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') { if (strncmp(cmd, "tm1617 key ", 11) == 0 && cmd[11] != '\0') {
unsigned key = (unsigned)strtoul(cmd + 11, NULL, 10); unsigned key = (unsigned)strtoul(cmd + 11, NULL, 10);
char line[48]; 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"); SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "TM1617_KEY_BAD\n");
return 1U; return 1U;
} }
app_tm1617_inject_key((uint8_t)key);
snprintf(line, sizeof(line), "TM1617_KEY_OK key=%u\n", key); snprintf(line, sizeof(line), "TM1617_KEY_OK key=%u\n", key);
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line); SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
app_tm1617_inject_key((uint8_t)key);
return 1U; return 1U;
} }
/* 测试注入:adc key N on|off(0~3;1=独立尾奏键,与万能第4键同路径) */ /* 测试注入: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"); SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "ADC_KEY_BAD\n");
return 1U; 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); snprintf(line, sizeof(line), "ADC_KEY_OK idx=%u on=%d\n", idx, on);
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line); SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
ADC_IN1_KEY_Handle((uint8_t)idx, on ? true : false);
return 1U; return 1U;
} }
/* 测试注入:chord xpose N(0~11,C=0) */ /* 测试注入: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 = KEY_NULL;
key_last = key; key_last = key;
LOG_I("KEY", "tm1617 inject key=%u", (unsigned)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 ```bash
cd tools cd tools
# 无硬件自检
python -m k1_harness selftest 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 通道,默认) # App 协议(BLE 通道,默认)
python -m k1_harness run --suite app --transport ble python run_k1_harness.py run --suite app --transport ble
# App 协议(串口通道) # 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 # BLE 链路 smoke
python -m k1_harness run --suite ble python run_k1_harness.py run --suite ble
# 按键 / 资源包 / UI(J-Link RTT) # 按键 / 资源包 / UI(J-Link RTT)
python -m k1_harness run --suite keys,keys1617,packs,ui python run_k1_harness.py run --suite keys,keys1617,packs,ui
# 全量(nightly)
python -m k1_harness run --suite all --profile nightly
# 含关机用例(破坏性,跑完设备软关机) # 含关机用例(破坏性,跑完设备软关机)
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` 指定。 报告默认写到 `Doc/reports/k1_hil_<时间戳>.md`,可用 `--report` 指定。

View File

@ -31,6 +31,23 @@ class Ctx:
dump_png: str = "" 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: def _run_selftest() -> int:
report = HarnessReport(title="K1 harness selftest(无硬件)") 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"套件: {', '.join(names)}")
report.notes.append(f"transport={ctx.transport} device={ctx.device} profile={args.profile}") report.notes.append(f"transport={ctx.transport} device={ctx.device} profile={args.profile}")
prev_rtt = False
for name in names: for name in names:
mod, needs = SUITES[name] 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: if "transport" in needs and ctx.transport == "com" and not ctx.port:
report.add(name, "前置条件", "SKIP", "transport=com 需 --port;已跳过该套件") report.add(name, "前置条件", "SKIP", "transport=com 需 --port;已跳过该套件")
continue continue
@ -107,6 +128,7 @@ def _run_suites(args) -> int:
report.add(name, "套件执行", "FAIL", f"SystemExit: {exc}") report.add(name, "套件执行", "FAIL", f"SystemExit: {exc}")
except Exception as exc: # 单套件异常不拖垮整体 except Exception as exc: # 单套件异常不拖垮整体
report.add(name, "套件执行", "FAIL", f"{type(exc).__name__}: {exc}") report.add(name, "套件执行", "FAIL", f"{type(exc).__name__}: {exc}")
prev_rtt = is_rtt
report.print_summary() report.print_summary()
ts = report.started.strftime("%Y%m%d_%H%M%S") ts = report.started.strftime("%Y%m%d_%H%M%S")

View File

@ -23,4 +23,4 @@ SUITES = {
"ui": (ui_nav, {"rtt"}), "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 -*- # -*- coding: utf-8 -*-
"""keys1617 套件:TM1617 段落/导航键注入(RTT `tm1617 key N`). """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) 键值:0=KEY_MAIN_D 1=KEY_MAIN_C 2=KEY_MAIN_B 3=KEY_MAIN_A 4=释放(KEY_NULL)
需要固件含 app_tm1617_inject_key 钩子;无 ACK 时记 SKIP。 每个键单独开 RTT 会话,失败时硬件复位后重试一次,避免触摸洪泛饿死后续命令。
""" """
from __future__ import annotations from __future__ import annotations
import os
import subprocess
import sys
import time
from ..paths import TOOLS_DIR
from ..transports.rtt_session import open_session from ..transports.rtt_session import open_session
SUITE = "keys1617" SUITE = "keys1617"
KEY_NAMES = {0: "MAIN_D", 1: "MAIN_C", 2: "MAIN_B", 3: "MAIN_A"} KEY_NAMES = {0: "MAIN_D", 1: "MAIN_C", 2: "MAIN_B", 3: "MAIN_A"}
def run(report, ctx) -> None: def _hw_reset(device: str) -> None:
with open_session(ctx.device) as sess: script = os.path.join(TOOLS_DIR, "rtt_reset.py")
if not sess.cmd_ack("tm1617 key 4", "TM1617_KEY_OK", timeout_s=2.0, retries=3): subprocess.run(
report.add(SUITE, "TM1617 注入命令探测", "SKIP", [sys.executable, script, "--device", device, "--wait-boot", "5"],
"无 TM1617_KEY_OK:固件缺少 'tm1617 key' 命令(需 app_tm1617_inject_key 钩子)") 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 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: if not ok:
report.add(SUITE, f"tm1617 {name}", "FAIL", "无 ACK") report.add(SUITE, "TM1617 注入命令探测", "SKIP",
continue "无 TM1617_KEY_OK:固件缺少 'tm1617 key'(需含钩子固件)")
seen = any(f"tm1617 key={key}" in line for line in sess.lines[-30:]) return
report.add(SUITE, f"tm1617 {name}", "PASS", report.add(SUITE, "TM1617 注入命令探测", "PASS", detail)
"" if seen else "ACK 有;[KEY] 日志未捕获")
sess.pump(0.1)
ok = sess.cmd_ack("tm1617 key 4", "TM1617_KEY_OK", timeout_s=2.0, retries=3) for key, label in KEY_NAMES.items():
report.add(SUITE, "tm1617 释放(KEY_NULL)", "PASS" if ok else "FAIL", "" if ok else "无 ACK") _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 -*- # -*- 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,或 [KEY] inject 日志(RTT 丢 ACK 时兜底)。
若完全无 CHORD_KEY_OK → 记 SKIP(固件过旧,需重烧含 app_log 注入命令的固件)。
""" """
from __future__ import annotations from __future__ import annotations
@ -11,32 +10,35 @@ from ..transports.rtt_session import open_session
SUITE = "keys" 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: def run(report, ctx) -> None:
with open_session(ctx.device) as sess: with open_session(ctx.device) as sess:
sess.cmd("log clear", 0.2) sess.cmd("log clear", 0.2)
# 探测固件是否支持注入命令 if not _chord_ok(sess, 0):
if not sess.cmd_ack("chord key 0", "CHORD_KEY_OK", timeout_s=2.0, retries=3):
report.add(SUITE, "TM1629 注入命令探测", "SKIP", report.add(SUITE, "TM1629 注入命令探测", "SKIP",
"无 CHORD_KEY_OK:固件缺少 'chord key' RTT 命令,请重烧新固件") "无 CHORD_KEY_OK:固件缺少 'chord key' RTT 命令,请重烧新固件")
return return
report.add(SUITE, "TM1629 注入命令探测", "PASS", "CHORD_KEY_OK") report.add(SUITE, "TM1629 注入命令探测", "PASS", "CHORD_KEY_OK")
for key in range(1, 24): for key in range(1, 24):
ok = sess.cmd_ack(f"chord key {key}", "CHORD_KEY_OK", timeout_s=2.0, retries=3) ok = _chord_ok(sess, key)
if not ok: report.add(SUITE, f"chord key {key}", "PASS" if ok else "FAIL",
report.add(SUITE, f"chord key {key}", "FAIL", "无 ACK") "" if ok else "无 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] 日志未捕获(可能级别/时序)")
sess.pump(0.05) 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") report.add(SUITE, "chord key 0 释放", "PASS" if ok else "FAIL", "" if ok else "无 ACK")
# 移调注入 0~11 抽查边界
for xp in (0, 6, 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") 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 BIN3_PRESETS = (0, 1, 2) # 1.1645 / 2.1564 / 3.1364
def _load_and_check(report, sess, cmd: str, tag: str, name: str) -> bool: def _load_and_check(report, sess, cmd: str, tag: str, name: str, *, missing: str = "FAIL") -> bool:
"""发加载命令,校验 <tag> 行 ret=0 且 OK。返回是否可用(False=命令缺失→SKIP)。""" """发加载命令,校验 <tag> 行。True=有响应;False=无响应(按 missing 记结果)."""
sess.pump(0.15) sess.pump(0.2)
sess.cmd(cmd, settle=0.1) got = sess.cmd_ack(cmd, tag, timeout_s=4.0, retries=4)
deadline_lines = sess.pump(2.5) if not got:
hit = [l for l in deadline_lines if tag in l] report.add(SUITE, name, missing, f"无 {tag} 响应")
if not hit:
return False 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 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()) report.add(SUITE, name, "PASS" if ok else "FAIL", line.strip())
return True return True
def run(report, ctx) -> None: def run(report, ctx) -> None:
with open_session(ctx.device) as sess: 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") _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"): if not _load_and_check(report, sess, "tone bin2 0", "TONE_BIN2",
# 老固件没有 tone bin2:退回 tone local 做基本校验 "2.bin 本地曲目加载 idx0", missing="SKIP"):
sess.cmd("tone local", settle=0.1) _load_and_check(report, sess, "tone local", "TONE_LOCAL",
lines = sess.pump(2.0) "2.bin 本地曲目加载(tone local)", missing="SKIP")
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 响应")
bin3_ok = True bin3_ok = True
for idx in BIN3_PRESETS: for idx in BIN3_PRESETS:
if not _load_and_check(report, sess, f"tone bin3 {idx}", "TONE_BIN3", if not _load_and_check(report, sess, f"tone bin3 {idx}", "TONE_BIN3",
f"3.bin 万能加载 idx{idx}"): f"3.bin 万能加载 idx{idx}"):
bin3_ok = False bin3_ok = False
report.add(SUITE, f"3.bin 万能加载 idx{idx}", "FAIL", "无 TONE_BIN3 响应")
if not bin3_ok: if not bin3_ok:
report.add(SUITE, "拨片起奏/尾奏", "SKIP", "3.bin 加载失败,级联跳过") report.add(SUITE, "拨片起奏/尾奏", "SKIP", "3.bin 加载失败,级联跳过")
@ -63,26 +59,33 @@ def run(report, ctx) -> None:
# ---- 拨片起奏(万能 idx0)---- # ---- 拨片起奏(万能 idx0)----
sess.cmd("log clear", 0.2) 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) 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) sess.pump(3.0)
pick_lines = sess.lines[base:] pick_lines = sess.lines[base:]
pitch_on = [l for l in pick_lines if "[PITCH]" in l and " on " in l] 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", report.add(SUITE, "3.bin 拨片起奏有声", "PASS" if pitch_on else "FAIL",
f"{len(pitch_on)} 条 PITCH on" if pitch_on else "起奏后无 PITCH on") 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", 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) sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
return return
else:
report.add(SUITE, "3.bin 尾奏触发", "PASS", "ADC_KEY_OK")
out_base = len(sess.lines) out_base = len(sess.lines)
sess.pump(4.0) sess.pump(4.0)
outro_lines = sess.lines[out_base:] outro_lines = sess.lines[out_base:]
outro_pitch = [l for l in outro_lines if "[PITCH]" in l and " on " in l] 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) for l in outro_lines)
if outro_pitch: if outro_pitch:
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "PASS", report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "PASS",
@ -93,10 +96,9 @@ def run(report, ctx) -> None:
else: else:
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "FAIL", report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "FAIL",
"尾奏触发后无任何 PITCH on") "尾奏触发后无任何 PITCH on")
sess.cmd_ack("adc key 1 off", "ADC_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) # 停止伴奏 sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
# ---- 音区断言(复用 pitch 分析器,软判定)----
res = analyze_lines(sess.lines) res = analyze_lines(sess.lines)
if res.fail: if res.fail:
report.add(SUITE, "音区映射断言", "FAIL", f"{res.fail} 条 FAIL(详见日志)") report.add(SUITE, "音区映射断言", "FAIL", f"{res.fail} 条 FAIL(详见日志)")

View File

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

View File

@ -403,6 +403,11 @@ class RttSession:
or "[KEY" in line or "[KEY" in line
or "CHORD_" in line or "CHORD_" in line
or "TONE_" 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) print(line, flush=True)
else: else: