test: unified K1 HIL harness + RTT inject hooks (tm1617/adc/bin2/sticky)
tools/k1_harness: one CLI over existing COM/BLE/RTT scripts covering App SysEx 01-06, BLE smoke, TM1629/TM1617 key inject, 1/2/3.bin pack checks with pick/outro regression, and UI nav (log status/TAP/DUMP). Unified PASS/FAIL/SKIP/SENT markdown report to Doc/reports. firmware hooks in app_log.c: 'tm1617 key N', 'adc key N on|off', 'tone bin2 [idx]', 'chord key N hold' (sticky inject survives physical idle scan so normal-mode pick tests can hold a chord). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
daee47a457
commit
a495717883
|
|
@ -427,6 +427,35 @@ uint8_t app_log_try_command(const char *cmd)
|
|||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||
return 1U;
|
||||
}
|
||||
/* tone bin2 [idx] → 2.bin 本地曲目(专业+本地曲目,带索引扫描) */
|
||||
if (strncmp(cmd, "tone bin2", 9) == 0) {
|
||||
char line[160];
|
||||
int ret;
|
||||
unsigned idx = 0;
|
||||
int count;
|
||||
if (cmd[9] == ' ' && cmd[10] != '\0')
|
||||
idx = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||
mGuiData[GUI_TAB_INDEX].Current = 1; /* 专业 */
|
||||
mGuiData[GUI_AUTOBAND_SW].Current = 1; /* 本地曲目→2.bin */
|
||||
ParamGuiData[EXPRESS_MODE_PARAM].Current = (uint8_t)idx;
|
||||
UI_ApplyToneAddress();
|
||||
AutoBandTop1_Stop();
|
||||
StartFlag = 0;
|
||||
count = AutoBandTop1_GetPresetItemCount();
|
||||
if (count > 0 && (int)idx >= count)
|
||||
idx = (unsigned)(count - 1);
|
||||
ParamGuiData[EXPRESS_MODE_PARAM].Current = (uint8_t)idx;
|
||||
ret = AutoBandTop1_LoadPresetItemFromFlash((int)idx);
|
||||
snprintf(line, sizeof(line),
|
||||
"TONE_BIN2 ret=%d idx=%u addr=0x%08lX map=BIN2@0x%08lX name=%s count=%d %s\n",
|
||||
ret, idx, (unsigned long)ADDRESS,
|
||||
(unsigned long)EXTFLASH_BIN2_SONG_HAITIAN_ADDR,
|
||||
AutoBandTop1_GetPresetName() ? AutoBandTop1_GetPresetName() : "(null)",
|
||||
AutoBandTop1_GetPresetItemCount(),
|
||||
(ADDRESS == EXTFLASH_BIN2_SONG_HAITIAN_ADDR) ? "OK" : "MISMATCH");
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||
return 1U;
|
||||
}
|
||||
/* tone bin3 [idx] → 3.bin 万能和弦走向 */
|
||||
if (strncmp(cmd, "tone bin3", 9) == 0) {
|
||||
char line[160];
|
||||
|
|
@ -495,12 +524,43 @@ uint8_t app_log_try_command(const char *cmd)
|
|||
if (strncmp(cmd, "chord key ", 10) == 0 && cmd[10] != '\0') {
|
||||
unsigned key = (unsigned)strtoul(cmd + 10, NULL, 10);
|
||||
char line[48];
|
||||
int hold = (strstr(cmd + 10, "hold") != NULL);
|
||||
if (key > 23U) {
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, "CHORD_KEY_BAD\n");
|
||||
return 1U;
|
||||
}
|
||||
app_tm1629_inject_key((uint8_t)key);
|
||||
snprintf(line, sizeof(line), "CHORD_KEY_OK key=%u\n", key);
|
||||
if (hold)
|
||||
app_tm1629_inject_hold((uint8_t)key);
|
||||
else
|
||||
app_tm1629_inject_key((uint8_t)key);
|
||||
snprintf(line, sizeof(line), "CHORD_KEY_OK key=%u%s\n", key, hold ? " hold" : "");
|
||||
SEGGER_RTT_WriteString(APP_LOG_RTT_CHANNEL, line);
|
||||
return 1U;
|
||||
}
|
||||
/* 测试注入:tm1617 key N(0~3=MAIN_D/C/B/A 段落或导航键,4=释放) */
|
||||
if (strncmp(cmd, "tm1617 key ", 11) == 0 && cmd[11] != '\0') {
|
||||
unsigned key = (unsigned)strtoul(cmd + 11, NULL, 10);
|
||||
char line[48];
|
||||
if (key > 4U) {
|
||||
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);
|
||||
return 1U;
|
||||
}
|
||||
/* 测试注入:adc key N on|off(0~3;1=独立尾奏键,与万能第4键同路径) */
|
||||
if (strncmp(cmd, "adc key ", 8) == 0 && cmd[8] != '\0') {
|
||||
unsigned idx = (unsigned)strtoul(cmd + 8, NULL, 10);
|
||||
char line[48];
|
||||
int on = (strstr(cmd + 8, "on") != NULL);
|
||||
if (idx > 3U || (!on && strstr(cmd + 8, "off") == NULL)) {
|
||||
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);
|
||||
return 1U;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,3 +84,12 @@ uint8_t app_tm1617_scan_key(void)
|
|||
}
|
||||
return KEY_NULL;
|
||||
}
|
||||
|
||||
void app_tm1617_inject_key(uint8_t key)
|
||||
{
|
||||
if (key >= KEY_NULL)
|
||||
key = KEY_NULL;
|
||||
key_last = key;
|
||||
LOG_I("KEY", "tm1617 inject key=%u", (unsigned)key);
|
||||
TM1617_Handle(key);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,4 +11,8 @@ void app_tm1617_auto_loop(void);
|
|||
|
||||
uint8_t app_tm1617_scan_key(void);
|
||||
|
||||
/* RTT/test: bypass physical scan and inject a key event directly.
|
||||
* key: 0=KEY_MAIN_D 1=KEY_MAIN_C 2=KEY_MAIN_B 3=KEY_MAIN_A 4+=release(KEY_NULL) */
|
||||
void app_tm1617_inject_key(uint8_t key);
|
||||
|
||||
#endif /* APP_TM1617_H */
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
#include "includes.h"
|
||||
|
||||
static uint8_t key_last = KEY_NONE;
|
||||
static uint8_t key_hold_sticky = 0; /* 1 = ????????????????(KEY_NONE)????? */
|
||||
|
||||
|
||||
/* LED 期望颜色缓存:索引 = LedNum_TypeDef(0~7),COLOR_OTHER 表示灭 */
|
||||
/* LED ???????????????? = LedNum_TypeDef??0~7????COLOR_OTHER ????? */
|
||||
//static LedColor_TypeDef led_cache[8] = {
|
||||
// COLOR_OTHER, COLOR_OTHER, COLOR_OTHER, COLOR_OTHER,
|
||||
// COLOR_OTHER, COLOR_OTHER, COLOR_OTHER, COLOR_OTHER
|
||||
|
|
@ -28,6 +29,9 @@ uint8_t app_tm1629_Scan_Key(void)
|
|||
key = TM1629D_GetKey();
|
||||
rt_mutex_release(TM1629_Mutex);
|
||||
|
||||
if(key_hold_sticky && key == KEY_NONE)
|
||||
return KEY_NONE; /* ??????§ľ??????????????????????? */
|
||||
|
||||
if(key != key_last)
|
||||
{
|
||||
key_last = key;
|
||||
|
|
@ -40,11 +44,25 @@ uint8_t app_tm1629_Scan_Key(void)
|
|||
|
||||
void app_tm1629_inject_key(uint8_t key)
|
||||
{
|
||||
key_hold_sticky = 0;
|
||||
key_last = key;
|
||||
LOG_I("KEY", "inject key=%u", (unsigned)key);
|
||||
TM1629_Handle(key);
|
||||
}
|
||||
|
||||
void app_tm1629_inject_hold(uint8_t key)
|
||||
{
|
||||
if(key == KEY_NONE)
|
||||
{
|
||||
app_tm1629_inject_key(KEY_NONE);
|
||||
return;
|
||||
}
|
||||
key_hold_sticky = 1;
|
||||
key_last = key;
|
||||
LOG_I("KEY", "inject hold key=%u", (unsigned)key);
|
||||
TM1629_Handle(key);
|
||||
}
|
||||
|
||||
|
||||
static void app_tm1629_led_set(LedNum_TypeDef led, LedColor_TypeDef color, uint8_t on)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
void app_tm1629_init(void);
|
||||
uint8_t app_tm1629_Scan_Key(void);
|
||||
void app_tm1629_inject_key(uint8_t key); /* RTT/测试:绕过扫描直接注入 */
|
||||
void app_tm1629_inject_hold(uint8_t key); /* RTT/测试:注入并保持(物理空扫不释放,chord key 0 解除) */
|
||||
void app_tm1629_set_chord_led(uint8_t key_idx, LedColor_TypeDef color);
|
||||
void app_tm1629_set_led(LedNum_TypeDef led, LedColor_TypeDef color);
|
||||
void app_tm1629_all_off(void);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
# k1_harness — K1 吉他统一 HIL 自动化测试
|
||||
|
||||
覆盖 K1 电吉他(YNGJ-GT1-M)的 **UI、按键、资源包、BLE、App 协议** 验证。
|
||||
复用现有脚本,不重写用例:
|
||||
|
||||
- App SysEx 协议:`test_protocol_app_sim.run_tests`(COM)/ `test_protocol_ble.py`(BLE)
|
||||
- RTT 注入/日志:`rtt_pitch_reg_test` 的 J-Link 连接与 `RttSession`
|
||||
|
||||
## 前置条件
|
||||
|
||||
| 套件 | 硬件 |
|
||||
|------|------|
|
||||
| `app`(com) | USB 转串口接 UART4(BLE 桥),115200 8N1 |
|
||||
| `app`(ble)/ `ble` | 笔记本蓝牙,设备广播名 `Smart Guitar MIDI` |
|
||||
| `keys` / `keys1617` / `packs` / `ui` | J-Link + SWD,固件含 `app_log` 注入命令 |
|
||||
|
||||
依赖:`pip install pyserial bleak pylink-square pillow`
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
cd tools
|
||||
|
||||
# 无硬件自检
|
||||
python -m k1_harness selftest
|
||||
|
||||
# App 协议(BLE 通道,默认)
|
||||
python -m k1_harness run --suite app --transport ble
|
||||
|
||||
# App 协议(串口通道)
|
||||
python -m k1_harness run --suite app --transport com --port COM5
|
||||
|
||||
# BLE 链路 smoke
|
||||
python -m k1_harness 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 -m k1_harness run --suite all --allow-poweroff
|
||||
```
|
||||
|
||||
报告默认写到 `Doc/reports/k1_hil_<时间戳>.md`,可用 `--report` 指定。
|
||||
退出码:有 FAIL 为 1,否则 0。
|
||||
|
||||
## 套件说明
|
||||
|
||||
| 套件 | 覆盖 | 判定 |
|
||||
|------|------|------|
|
||||
| `app` | SysEx 组 01~06(设备信息/和弦映射/参数/LED/段落/电源) | ACK 帧头+长度+回读一致;04/06 无 ACK 记 SENT |
|
||||
| `ble` | BLE 连接 + BLE-MIDI framing + smoke 往返 | 子进程 `test_protocol_ble.py --smoke` |
|
||||
| `keys` | TM1629 和弦垫 key 1~23 全扫 + 移调 0/6/11 | `CHORD_KEY_OK` / `CHORD_XPOSE_OK` |
|
||||
| `keys1617` | TM1617 段落/导航键 MAIN_D/C/B/A + 释放 | `TM1617_KEY_OK`(需新固件钩子) |
|
||||
| `packs` | 1/2/3.bin 加载(地址 OK + ret=0)、拨片起奏、3.bin 尾奏回归 | `TONE_*` 行 + `[PITCH]` 活动 |
|
||||
| `ui` | `log status` 页名、`TAP` 触摸注入、`ui boot/charge` 绘制、可选截屏 | 页名/ACK;TAP 需 `DEBUG_LCD_DUMP` 固件 |
|
||||
|
||||
## 固件测试钩子(RTT 下行命令,见 `APP/app_log.c`)
|
||||
|
||||
| 命令 | 说明 |
|
||||
|------|------|
|
||||
| `chord key N` / `chord key N hold` | TM1629 注入;`hold` 保持不被物理空扫释放(`chord key 0` 解除) |
|
||||
| `chord xpose N` | 移调 0~11 |
|
||||
| `tm1617 key N` | TM1617 注入(0~3=MAIN_D/C/B/A,4=释放) |
|
||||
| `adc key N on\|off` | ADC 键注入(1=独立尾奏键,与万能第4键同路径) |
|
||||
| `tone bin1/bin2/bin3 [idx]` | 资源包加载校验 |
|
||||
| `tone start` / `tone pick [uni]` | 拨片起奏 |
|
||||
| `log status` / `log dump` / `sys reset` | 状态/日志/复位 |
|
||||
| `TAP x y` / `DUMP` | 触摸注入/截屏(需 `DEBUG_LCD_DUMP`) |
|
||||
|
||||
## 备注
|
||||
|
||||
- 手机 App 源码不在本仓;**App 功能验证 = BLE SysEx 端到端**(与真机 App 同协议)。
|
||||
- 3.bin 尾奏回归对应音师需求 `Doc/音师需求_万能3.bin尾奏_20260914.md`:
|
||||
尾奏触发后 4s 内应仍有 `[PITCH] on`,否则判定「一按立刻静音」FAIL。
|
||||
- 老固件缺少新命令时相关用例记 SKIP 并在详情中提示重烧。
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""k1_harness — K1 吉他统一 HIL 自动化测试套件.
|
||||
|
||||
复用现有脚本的两根脊梁,不重写协议用例:
|
||||
- App SysEx 协议: test_protocol_app_sim.run_tests (COM) / test_protocol_ble (BLE)
|
||||
- RTT 注入/日志: rtt_pitch_reg_test 的 J-Link 连接与 RttSession
|
||||
|
||||
用法见 README.md 或: python -m k1_harness --help
|
||||
"""
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .cli import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,140 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""k1_harness CLI.
|
||||
|
||||
python -m k1_harness selftest
|
||||
python -m k1_harness run --suite app --transport com --port COM5
|
||||
python -m k1_harness run --suite app --transport ble
|
||||
python -m k1_harness run --suite ble
|
||||
python -m k1_harness run --suite keys,packs,ui --device AT32F403AC
|
||||
python -m k1_harness run --suite all --profile nightly
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from .paths import default_report_dir
|
||||
from .report import HarnessReport
|
||||
from .suites import SUITES, ALL_ORDER
|
||||
|
||||
|
||||
@dataclass
|
||||
class Ctx:
|
||||
transport: str = "ble"
|
||||
port: str = ""
|
||||
baud: int = 115200
|
||||
device: str = "AT32F403AC"
|
||||
allow_poweroff: bool = False
|
||||
dump_png: str = ""
|
||||
|
||||
|
||||
def _run_selftest() -> int:
|
||||
report = HarnessReport(title="K1 harness selftest(无硬件)")
|
||||
|
||||
# 1) 协议帧编解码自检(复用现有 selftest 逻辑,零硬件)
|
||||
import io
|
||||
import contextlib
|
||||
import test_protocol_app_sim as appsim
|
||||
|
||||
buf = io.StringIO()
|
||||
with contextlib.redirect_stdout(buf):
|
||||
code = appsim.selftest()
|
||||
for line in buf.getvalue().splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith("[") and "]" in line:
|
||||
status = line[1:line.index("]")].strip()
|
||||
name = line[line.index("]") + 1:].split("|")[0].strip()
|
||||
report.add("selftest", name, status if status in ("PASS", "FAIL", "SKIP", "SENT") else "FAIL")
|
||||
report.add("selftest", "app_sim selftest 退出码", "PASS" if code == 0 else "FAIL", f"exit={code}")
|
||||
|
||||
# 2) pitch 分析器离线自检:喂一条合法/非法 PITCH 行
|
||||
from .transports.rtt_session import analyze_lines
|
||||
good = ("[PITCH] on ch8(bass) 52->40 d=-12 deg=3 chord=8 xp=2 xf=0 fl=0x03 FIX\n"
|
||||
"[PITCH] on ch1(chord) 64->64 d=0 deg=1 chord=1 xp=0 xf=0 fl=0x00 PASS\n")
|
||||
res = analyze_lines(good.splitlines())
|
||||
report.add("selftest", "pitch 分析器(合法样本)", "PASS" if res.fail == 0 and res.ok >= 2 else "FAIL",
|
||||
f"ok={res.ok} fail={res.fail}")
|
||||
bad = "[PITCH] on ch8(bass) 52->30 d=-22 deg=3 chord=8 xp=2 xf=0 fl=0x03 FIX\n"
|
||||
res2 = analyze_lines(bad.splitlines())
|
||||
report.add("selftest", "pitch 分析器(非法样本检出)", "PASS" if res2.fail >= 1 else "FAIL",
|
||||
f"fail={res2.fail}")
|
||||
|
||||
# 3) 报告落盘自检
|
||||
out = os.path.join(default_report_dir(), "k1_selftest.md")
|
||||
report.write_markdown(out)
|
||||
report.print_summary()
|
||||
return report.exit_code()
|
||||
|
||||
|
||||
def _run_suites(args) -> int:
|
||||
names = []
|
||||
for part in args.suite.split(","):
|
||||
part = part.strip().lower()
|
||||
if part == "all":
|
||||
names.extend(n for n in ALL_ORDER if n not in names)
|
||||
elif part in SUITES:
|
||||
if part not in names:
|
||||
names.append(part)
|
||||
else:
|
||||
print(f"未知套件: {part}(可选: {', '.join(SUITES)} / all)", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
ctx = Ctx(
|
||||
transport=args.transport,
|
||||
port=args.port or "",
|
||||
baud=args.baud,
|
||||
device=args.device,
|
||||
allow_poweroff=args.allow_poweroff or args.profile == "destructive",
|
||||
dump_png=args.dump_png or "",
|
||||
)
|
||||
|
||||
report = HarnessReport(title="K1 HIL 自动化测试报告")
|
||||
report.notes.append(f"套件: {', '.join(names)}")
|
||||
report.notes.append(f"transport={ctx.transport} device={ctx.device} profile={args.profile}")
|
||||
|
||||
for name in names:
|
||||
mod, needs = SUITES[name]
|
||||
if "transport" in needs and ctx.transport == "com" and not ctx.port:
|
||||
report.add(name, "前置条件", "SKIP", "transport=com 需 --port;已跳过该套件")
|
||||
continue
|
||||
print(f"\n===== 套件 {name} =====", flush=True)
|
||||
try:
|
||||
mod.run(report, ctx)
|
||||
except SystemExit as exc:
|
||||
report.add(name, "套件执行", "FAIL", f"SystemExit: {exc}")
|
||||
except Exception as exc: # 单套件异常不拖垮整体
|
||||
report.add(name, "套件执行", "FAIL", f"{type(exc).__name__}: {exc}")
|
||||
|
||||
report.print_summary()
|
||||
ts = report.started.strftime("%Y%m%d_%H%M%S")
|
||||
out = args.report or os.path.join(default_report_dir(), f"k1_hil_{ts}.md")
|
||||
report.write_markdown(out)
|
||||
return report.exit_code()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser(prog="k1_harness", description="K1 吉他统一 HIL 自动化测试")
|
||||
sub = ap.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
sub.add_parser("selftest", help="无硬件自检(帧编解码 + 分析器 + 报告)")
|
||||
|
||||
runp = sub.add_parser("run", help="跑测试套件")
|
||||
runp.add_argument("--suite", required=True,
|
||||
help="逗号分隔: app,ble,keys,keys1617,packs,ui 或 all")
|
||||
runp.add_argument("--transport", choices=("com", "ble"), default="ble",
|
||||
help="app 套件通道(默认 ble)")
|
||||
runp.add_argument("--port", default="", help="COM 端口(transport=com 时必填)")
|
||||
runp.add_argument("--baud", type=int, default=115200)
|
||||
runp.add_argument("--device", default="AT32F403AC", help="J-Link 器件名")
|
||||
runp.add_argument("--profile", choices=("smoke", "nightly", "destructive"), default="nightly")
|
||||
runp.add_argument("--allow-poweroff", action="store_true", help="允许 05 00 关机用例")
|
||||
runp.add_argument("--dump-png", default="", help="ui 套件截屏输出路径(需 DEBUG_LCD_DUMP 固件)")
|
||||
runp.add_argument("--report", default="", help="Markdown 报告输出路径")
|
||||
|
||||
args = ap.parse_args()
|
||||
if args.cmd == "selftest":
|
||||
raise SystemExit(_run_selftest())
|
||||
raise SystemExit(_run_suites(args))
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""路径与 sys.path 引导:让 harness 能 import tools/ 下的现有脚本."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
HARNESS_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
TOOLS_DIR = os.path.dirname(HARNESS_DIR) # .../tools
|
||||
REPO_DIR = os.path.dirname(TOOLS_DIR) # .../YNGJ-GT1-M - AT32F403ARCT7
|
||||
WORKSPACE_DIR = os.path.dirname(os.path.dirname(REPO_DIR)) # .../一诺国际吉他
|
||||
DOC_REPORTS_DIR = os.path.join(WORKSPACE_DIR, "Doc", "reports")
|
||||
LOCAL_REPORTS_DIR = os.path.join(TOOLS_DIR, "out", "reports")
|
||||
|
||||
if TOOLS_DIR not in sys.path:
|
||||
sys.path.insert(0, TOOLS_DIR)
|
||||
|
||||
|
||||
def default_report_dir() -> str:
|
||||
"""报告默认落到 Doc/reports(与工作区现有 BLE 报告一致);不存在则用 tools/out/reports."""
|
||||
if os.path.isdir(os.path.join(WORKSPACE_DIR, "Doc")):
|
||||
os.makedirs(DOC_REPORTS_DIR, exist_ok=True)
|
||||
return DOC_REPORTS_DIR
|
||||
os.makedirs(LOCAL_REPORTS_DIR, exist_ok=True)
|
||||
return LOCAL_REPORTS_DIR
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from .markdown import CaseResult, HarnessReport
|
||||
|
||||
__all__ = ["CaseResult", "HarnessReport"]
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,77 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""统一测试报告:PASS/FAIL/SKIP/SENT 四态 + Markdown 输出 + 退出码."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
STATUSES = ("PASS", "FAIL", "SKIP", "SENT")
|
||||
|
||||
|
||||
@dataclass
|
||||
class CaseResult:
|
||||
suite: str
|
||||
name: str
|
||||
status: str
|
||||
detail: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class HarnessReport:
|
||||
title: str = "K1 HIL 自动化测试报告"
|
||||
cases: list[CaseResult] = field(default_factory=list)
|
||||
notes: list[str] = field(default_factory=list)
|
||||
started: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def add(self, suite: str, name: str, status: str, detail: str = "") -> None:
|
||||
status = status.upper()
|
||||
assert status in STATUSES, f"bad status {status}"
|
||||
self.cases.append(CaseResult(suite, name, status, detail))
|
||||
print(f"[{status:>4}] {suite}/{name}" + (f" | {detail}" if detail else ""), flush=True)
|
||||
|
||||
def extend_rows(self, suite: str, rows) -> None:
|
||||
"""吸收 test_protocol_app_sim.Results 风格的 (name, status, detail) 行."""
|
||||
for name, status, detail in rows:
|
||||
self.add(suite, name, status, detail)
|
||||
|
||||
def counts(self) -> dict:
|
||||
return {s: sum(1 for c in self.cases if c.status == s) for s in STATUSES}
|
||||
|
||||
def exit_code(self) -> int:
|
||||
return 1 if self.counts()["FAIL"] else 0
|
||||
|
||||
def summary_text(self) -> str:
|
||||
n = self.counts()
|
||||
return f"PASS={n['PASS']} FAIL={n['FAIL']} SKIP={n['SKIP']} SENT={n['SENT']}"
|
||||
|
||||
def print_summary(self) -> None:
|
||||
print("\n===== 汇总 =====", flush=True)
|
||||
for suite in dict.fromkeys(c.suite for c in self.cases):
|
||||
sub = [c for c in self.cases if c.suite == suite]
|
||||
n = {s: sum(1 for c in sub if c.status == s) for s in STATUSES}
|
||||
print(f" {suite:<12} PASS={n['PASS']} FAIL={n['FAIL']} SKIP={n['SKIP']} SENT={n['SENT']}", flush=True)
|
||||
print(f" {'TOTAL':<12} {self.summary_text()}", flush=True)
|
||||
for c in self.cases:
|
||||
if c.status == "FAIL":
|
||||
print(f" FAIL: {c.suite}/{c.name} | {c.detail}", flush=True)
|
||||
|
||||
def write_markdown(self, path: str) -> str:
|
||||
os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True)
|
||||
lines = [
|
||||
f"# {self.title}",
|
||||
"",
|
||||
f"- 时间: {self.started.isoformat(timespec='seconds')}",
|
||||
f"- 结果: {self.summary_text()}",
|
||||
]
|
||||
for note in self.notes:
|
||||
lines.append(f"- {note}")
|
||||
lines += ["", "| 套件 | 用例 | 结果 | 详情 |", "|------|------|------|------|"]
|
||||
for c in self.cases:
|
||||
detail = c.detail.replace("|", "\\|").replace("\n", " ")
|
||||
lines.append(f"| {c.suite} | {c.name} | {c.status} | {detail} |")
|
||||
lines.append("")
|
||||
with open(path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write("\n".join(lines))
|
||||
print(f"报告已保存: {os.path.abspath(path)}", flush=True)
|
||||
return path
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""套件注册表。每个套件模块提供 run(report, ctx) -> None.
|
||||
|
||||
ctx 字段(cli 注入):
|
||||
transport: "com" | "ble" —— app 套件使用
|
||||
port: str —— COM 端口(transport=com 时必填)
|
||||
baud: int
|
||||
device: str —— J-Link 器件名
|
||||
allow_poweroff: bool —— @destructive 用例开关
|
||||
dump_png: str —— ui 套件可选截图输出路径
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from . import app_protocol, ble_smoke, keys_tm1629, keys_tm1617, packs_bin123, ui_nav
|
||||
|
||||
# name -> (module, 需要的资源标签)
|
||||
SUITES = {
|
||||
"app": (app_protocol, {"transport"}),
|
||||
"ble": (ble_smoke, {"ble"}),
|
||||
"keys": (keys_tm1629, {"rtt"}),
|
||||
"keys1617": (keys_tm1617, {"rtt"}),
|
||||
"packs": (packs_bin123, {"rtt"}),
|
||||
"ui": (ui_nav, {"rtt"}),
|
||||
}
|
||||
|
||||
ALL_ORDER = ("app", "ble", "keys", "keys1617", "packs", "ui")
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,61 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""app 套件:App SysEx 全协议(组 01~06)。
|
||||
|
||||
- transport=com: 进程内复用 test_protocol_app_sim.run_tests(UART4 桥)
|
||||
- transport=ble: 子进程跑 test_protocol_ble.py( bleak + BLE-MIDI framing),
|
||||
解析其 [PASS]/[FAIL]/[SKIP]/[SENT] 输出行进统一报告
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from ..paths import TOOLS_DIR
|
||||
|
||||
ROW_RE = re.compile(r"^\[(PASS|FAIL|SKIP|SENT)\s*\]\s*(.+?)(?:\s*\|\s*(.*))?$")
|
||||
|
||||
|
||||
def _absorb_output(report, suite: str, text: str) -> None:
|
||||
n = 0
|
||||
for line in text.splitlines():
|
||||
m = ROW_RE.match(line.strip())
|
||||
if m:
|
||||
report.add(suite, m.group(2), m.group(1), m.group(3) or "")
|
||||
n += 1
|
||||
if n == 0:
|
||||
tail = "\\n".join(text.splitlines()[-5:])
|
||||
report.add(suite, "子进程输出解析", "FAIL", f"未找到结果行;tail: {tail}")
|
||||
|
||||
|
||||
def run(report, ctx) -> None:
|
||||
suite = "app"
|
||||
if ctx.transport == "com":
|
||||
if not ctx.port:
|
||||
report.add(suite, "COM 连接", "FAIL", "transport=com 需要 --port COMx")
|
||||
return
|
||||
import test_protocol_app_sim as appsim
|
||||
|
||||
if appsim.serial is None:
|
||||
report.add(suite, "pyserial", "FAIL", "pip install pyserial")
|
||||
return
|
||||
sim = appsim.AppSim(ctx.port, ctx.baud)
|
||||
try:
|
||||
res = appsim.run_tests(sim, allow_poweroff=ctx.allow_poweroff)
|
||||
finally:
|
||||
sim.close()
|
||||
report.extend_rows(suite, res.rows)
|
||||
return
|
||||
|
||||
# BLE:子进程调 test_protocol_ble.py(其内部已处理 bleak 异步/解配对/报告)
|
||||
cmd = [
|
||||
sys.executable, os.path.join(TOOLS_DIR, "test_protocol_ble.py"),
|
||||
"--transport", "midi", "--no-unpair",
|
||||
]
|
||||
if ctx.allow_poweroff:
|
||||
cmd.append("--allow-poweroff")
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
|
||||
_absorb_output(report, suite, proc.stdout)
|
||||
if proc.returncode != 0 and not any(c.suite == suite and c.status == "FAIL" for c in report.cases):
|
||||
report.add(suite, "BLE 协议子进程", "FAIL", f"exit={proc.returncode} {proc.stderr[-300:]}")
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""ble 套件:BLE 链路 smoke(连接 + BLE-MIDI framing + 少量协议往返)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from ..paths import TOOLS_DIR
|
||||
from .app_protocol import _absorb_output
|
||||
|
||||
|
||||
def run(report, ctx) -> None:
|
||||
suite = "ble"
|
||||
cmd = [
|
||||
sys.executable, os.path.join(TOOLS_DIR, "test_protocol_ble.py"),
|
||||
"--transport", "midi", "--smoke", "--no-unpair",
|
||||
]
|
||||
try:
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
|
||||
except subprocess.TimeoutExpired:
|
||||
report.add(suite, "BLE smoke", "FAIL", "timeout 180s(设备未广播/未配对?)")
|
||||
return
|
||||
before = len(report.cases)
|
||||
_absorb_output(report, suite, proc.stdout)
|
||||
new = report.cases[before:]
|
||||
if not any(c.status == "FAIL" for c in new) and proc.returncode != 0:
|
||||
report.add(suite, "BLE smoke 退出码", "FAIL", f"exit={proc.returncode} {proc.stderr[-300:]}")
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# -*- 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。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
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 钩子)")
|
||||
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)
|
||||
|
||||
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")
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""keys 套件:TM1629 和弦垫全键注入(RTT `chord key N`,0 释放 / 1~21 和弦 / 22 拍速 / 23 停止).
|
||||
|
||||
判定:固件回 CHORD_KEY_OK 且 [KEY] 日志出现对应键值。
|
||||
若完全无 CHORD_KEY_OK → 记 SKIP(固件过旧,需重烧含 app_log 注入命令的固件)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..transports.rtt_session import open_session
|
||||
|
||||
SUITE = "keys"
|
||||
|
||||
|
||||
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):
|
||||
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] 日志未捕获(可能级别/时序)")
|
||||
sess.pump(0.05)
|
||||
|
||||
ok = sess.cmd_ack("chord key 0", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
|
||||
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)
|
||||
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) # 恢复
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""packs 套件:资源包 1.bin/2.bin/3.bin 加载校验 + 拨片起奏 + 3.bin 尾奏回归.
|
||||
|
||||
- 加载: `tone bin1 N` / `tone bin2 N` / `tone bin3 N` → 期望 TONE_* 行 ret=0 且地址 OK
|
||||
(`tone bin2` 依赖新固件命令;缺失时 2.bin 用例记 SKIP)
|
||||
- 起奏: `tone start` 后 3s 内应有 [PITCH] on 事件
|
||||
- 尾奏(音师需求 20260914): 万能 3.bin 起奏后 `adc key 1 on`(独立尾奏键,
|
||||
与万能第4段落键同路径 AutoBand_StartOutro)→ 之后 4s 内仍应出现 [PITCH] on,
|
||||
不再「一按立刻全静音」;结束 `adc key 1 off`
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..transports.rtt_session import open_session, analyze_lines
|
||||
|
||||
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:
|
||||
return False
|
||||
line = hit[-1]
|
||||
ok = "ret=0" in line and "OK" in line and "MISMATCH" 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)
|
||||
|
||||
# ---- 加载校验 ----
|
||||
_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 响应")
|
||||
|
||||
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 加载失败,级联跳过")
|
||||
return
|
||||
|
||||
# ---- 拨片起奏(万能 idx0)----
|
||||
sess.cmd("log clear", 0.2)
|
||||
sess.cmd("tone bin3 0", settle=0.8)
|
||||
base = len(sess.lines)
|
||||
sess.cmd("tone start", settle=0.2)
|
||||
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):
|
||||
report.add(SUITE, "3.bin 尾奏(adc key 1)", "SKIP",
|
||||
"固件缺少 'adc key' 命令;请用含 ADC 注入钩子的固件")
|
||||
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
|
||||
return
|
||||
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()
|
||||
for l in outro_lines)
|
||||
if outro_pitch:
|
||||
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "PASS",
|
||||
f"尾奏触发后 {len(outro_pitch)} 条 PITCH on")
|
||||
elif stopped:
|
||||
report.add(SUITE, "3.bin 尾奏有声(非立刻静音)", "FAIL",
|
||||
"尾奏触发后立刻停止(3.bin Postamble 数据为空?见音师需求 20260914)")
|
||||
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) # 停止伴奏
|
||||
|
||||
# ---- 音区断言(复用 pitch 分析器,软判定)----
|
||||
res = analyze_lines(sess.lines)
|
||||
if res.fail:
|
||||
report.add(SUITE, "音区映射断言", "FAIL", f"{res.fail} 条 FAIL(详见日志)")
|
||||
elif res.ok:
|
||||
report.add(SUITE, "音区映射断言", "PASS", f"{res.ok} OK / {res.skip} SKIP")
|
||||
else:
|
||||
report.add(SUITE, "音区映射断言", "SKIP", "无可分析 PITCH 样本")
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
# -*- 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 截屏留证(不作像素门禁)
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
from ..paths import TOOLS_DIR
|
||||
from ..transports.rtt_session import open_session
|
||||
|
||||
SUITE = "ui"
|
||||
UI_RE = re.compile(r"LOG_STATUS .*?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):
|
||||
m = UI_RE.search(line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return None
|
||||
|
||||
|
||||
def run(report, ctx) -> None:
|
||||
with open_session(ctx.device) as sess:
|
||||
page0 = _read_ui_page(sess)
|
||||
if page0:
|
||||
report.add(SUITE, "UI 页面上报", "PASS", f"ui={page0}")
|
||||
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))
|
||||
if not tap_ack:
|
||||
report.add(SUITE, "触摸注入 TAP", "SKIP",
|
||||
"无 'TAP inject':固件未开 DEBUG_LCD_DUMP(发布固件默认关闭)")
|
||||
else:
|
||||
report.add(SUITE, "触摸注入 TAP", "PASS", "TAP inject")
|
||||
sess.pump(1.0)
|
||||
page1 = _read_ui_page(sess)
|
||||
if page1:
|
||||
report.add(SUITE, "TAP 后页面状态", "PASS", f"ui={page0} -> {page1}")
|
||||
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)
|
||||
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"),
|
||||
"--out", ctx.dump_png, "--device", ctx.device],
|
||||
capture_output=True, text=True, timeout=120)
|
||||
ok = proc.returncode == 0 and os.path.isfile(ctx.dump_png)
|
||||
report.add(SUITE, "LCD 截屏", "PASS" if ok else "SKIP",
|
||||
ctx.dump_png if ok else "截屏失败(需 DEBUG_LCD_DUMP 固件)")
|
||||
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,40 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""RTT 传输:薄封装 rtt_pitch_reg_test 的 J-Link/RTT 实现(单一事实来源,避免复制)."""
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ..paths import TOOLS_DIR # noqa: F401 (确保 sys.path 已注入)
|
||||
|
||||
import rtt_pitch_reg_test as _rtt
|
||||
|
||||
# 直接复用,保持零行为变更
|
||||
connect_jlink = _rtt.connect_jlink
|
||||
find_rtt_control_block = _rtt.find_rtt_control_block
|
||||
wait_rtt_ready = _rtt.wait_rtt_ready
|
||||
RttSession = _rtt.RttSession
|
||||
analyze_lines = _rtt.analyze_lines
|
||||
CheckResult = _rtt.CheckResult
|
||||
|
||||
|
||||
@contextmanager
|
||||
def open_session(device: str = "AT32F403AC"):
|
||||
"""连接 J-Link + 启动 RTT,yield RttSession;退出时清理."""
|
||||
_rtt.import_deps()
|
||||
jlink = connect_jlink(device)
|
||||
try:
|
||||
cb = find_rtt_control_block(jlink)
|
||||
if cb is None:
|
||||
raise SystemExit("SEGGER RTT control block 未找到(固件未运行?)")
|
||||
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||
jlink.rtt_start(cb)
|
||||
wait_rtt_ready(jlink)
|
||||
sess = RttSession(jlink)
|
||||
sess.pump(0.3)
|
||||
yield sess
|
||||
finally:
|
||||
try:
|
||||
jlink.rtt_stop()
|
||||
except Exception:
|
||||
pass
|
||||
jlink.close()
|
||||
Loading…
Reference in New Issue