diff --git a/APP/app_log.c b/APP/app_log.c index 9be7ba1..244057a 100644 --- a/APP/app_log.c +++ b/APP/app_log.c @@ -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; } diff --git a/APP/app_tm1617.c b/APP/app_tm1617.c index d0b681f..0a65348 100644 --- a/APP/app_tm1617.c +++ b/APP/app_tm1617.c @@ -75,7 +75,7 @@ uint8_t app_tm1617_scan_key(void) key = (uint8_t)TM1617_ScanFkey(); rt_mutex_release(TM1617_Mutex); - if (key != key_last) + if (key != key_last) { key_last = key; LOG_I("KEY", "tm1617 key=%u", (unsigned)key); @@ -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); +} diff --git a/APP/app_tm1617.h b/APP/app_tm1617.h index 21366cc..cc22c0d 100644 --- a/APP/app_tm1617.h +++ b/APP/app_tm1617.h @@ -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 */ diff --git a/APP/app_tm1629.c b/APP/app_tm1629.c index 7d63b68..f18fb85 100644 --- a/APP/app_tm1629.c +++ b/APP/app_tm1629.c @@ -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_TypeDef0~7COLOR_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 @@ -27,7 +28,10 @@ uint8_t app_tm1629_Scan_Key(void) rt_mutex_take(TM1629_Mutex, RT_WAITING_FOREVER); 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) { diff --git a/APP/app_tm1629.h b/APP/app_tm1629.h index 0798980..0f50565 100644 --- a/APP/app_tm1629.h +++ b/APP/app_tm1629.h @@ -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); diff --git a/tools/k1_harness/README.md b/tools/k1_harness/README.md new file mode 100644 index 0000000..b929b73 --- /dev/null +++ b/tools/k1_harness/README.md @@ -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 并在详情中提示重烧。 diff --git a/tools/k1_harness/__init__.py b/tools/k1_harness/__init__.py new file mode 100644 index 0000000..0eadf9e --- /dev/null +++ b/tools/k1_harness/__init__.py @@ -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 +""" diff --git a/tools/k1_harness/__main__.py b/tools/k1_harness/__main__.py new file mode 100644 index 0000000..f9c7580 --- /dev/null +++ b/tools/k1_harness/__main__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +from .cli import main + +if __name__ == "__main__": + main() diff --git a/tools/k1_harness/__pycache__/__init__.cpython-311.pyc b/tools/k1_harness/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..1b2dcee Binary files /dev/null and b/tools/k1_harness/__pycache__/__init__.cpython-311.pyc differ diff --git a/tools/k1_harness/__pycache__/__main__.cpython-311.pyc b/tools/k1_harness/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..16a8d43 Binary files /dev/null and b/tools/k1_harness/__pycache__/__main__.cpython-311.pyc differ diff --git a/tools/k1_harness/__pycache__/cli.cpython-311.pyc b/tools/k1_harness/__pycache__/cli.cpython-311.pyc new file mode 100644 index 0000000..0e5057d Binary files /dev/null and b/tools/k1_harness/__pycache__/cli.cpython-311.pyc differ diff --git a/tools/k1_harness/__pycache__/paths.cpython-311.pyc b/tools/k1_harness/__pycache__/paths.cpython-311.pyc new file mode 100644 index 0000000..4ee2a2b Binary files /dev/null and b/tools/k1_harness/__pycache__/paths.cpython-311.pyc differ diff --git a/tools/k1_harness/cli.py b/tools/k1_harness/cli.py new file mode 100644 index 0000000..d02b9c5 --- /dev/null +++ b/tools/k1_harness/cli.py @@ -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)) diff --git a/tools/k1_harness/paths.py b/tools/k1_harness/paths.py new file mode 100644 index 0000000..860f6b2 --- /dev/null +++ b/tools/k1_harness/paths.py @@ -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 diff --git a/tools/k1_harness/report/__init__.py b/tools/k1_harness/report/__init__.py new file mode 100644 index 0000000..30a562f --- /dev/null +++ b/tools/k1_harness/report/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +from .markdown import CaseResult, HarnessReport + +__all__ = ["CaseResult", "HarnessReport"] diff --git a/tools/k1_harness/report/__pycache__/__init__.cpython-311.pyc b/tools/k1_harness/report/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..5f4565f Binary files /dev/null and b/tools/k1_harness/report/__pycache__/__init__.cpython-311.pyc differ diff --git a/tools/k1_harness/report/__pycache__/markdown.cpython-311.pyc b/tools/k1_harness/report/__pycache__/markdown.cpython-311.pyc new file mode 100644 index 0000000..ad2e278 Binary files /dev/null and b/tools/k1_harness/report/__pycache__/markdown.cpython-311.pyc differ diff --git a/tools/k1_harness/report/markdown.py b/tools/k1_harness/report/markdown.py new file mode 100644 index 0000000..dd22ce0 --- /dev/null +++ b/tools/k1_harness/report/markdown.py @@ -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 diff --git a/tools/k1_harness/suites/__init__.py b/tools/k1_harness/suites/__init__.py new file mode 100644 index 0000000..3a87cf2 --- /dev/null +++ b/tools/k1_harness/suites/__init__.py @@ -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") diff --git a/tools/k1_harness/suites/__pycache__/__init__.cpython-311.pyc b/tools/k1_harness/suites/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..6909163 Binary files /dev/null and b/tools/k1_harness/suites/__pycache__/__init__.cpython-311.pyc differ diff --git a/tools/k1_harness/suites/__pycache__/app_protocol.cpython-311.pyc b/tools/k1_harness/suites/__pycache__/app_protocol.cpython-311.pyc new file mode 100644 index 0000000..4338006 Binary files /dev/null and b/tools/k1_harness/suites/__pycache__/app_protocol.cpython-311.pyc differ diff --git a/tools/k1_harness/suites/__pycache__/ble_smoke.cpython-311.pyc b/tools/k1_harness/suites/__pycache__/ble_smoke.cpython-311.pyc new file mode 100644 index 0000000..875e167 Binary files /dev/null and b/tools/k1_harness/suites/__pycache__/ble_smoke.cpython-311.pyc differ diff --git a/tools/k1_harness/suites/__pycache__/keys_tm1617.cpython-311.pyc b/tools/k1_harness/suites/__pycache__/keys_tm1617.cpython-311.pyc new file mode 100644 index 0000000..5bb2e1e Binary files /dev/null and b/tools/k1_harness/suites/__pycache__/keys_tm1617.cpython-311.pyc differ diff --git a/tools/k1_harness/suites/__pycache__/keys_tm1629.cpython-311.pyc b/tools/k1_harness/suites/__pycache__/keys_tm1629.cpython-311.pyc new file mode 100644 index 0000000..9ba4853 Binary files /dev/null and b/tools/k1_harness/suites/__pycache__/keys_tm1629.cpython-311.pyc differ diff --git a/tools/k1_harness/suites/__pycache__/packs_bin123.cpython-311.pyc b/tools/k1_harness/suites/__pycache__/packs_bin123.cpython-311.pyc new file mode 100644 index 0000000..422cd83 Binary files /dev/null and b/tools/k1_harness/suites/__pycache__/packs_bin123.cpython-311.pyc differ diff --git a/tools/k1_harness/suites/__pycache__/ui_nav.cpython-311.pyc b/tools/k1_harness/suites/__pycache__/ui_nav.cpython-311.pyc new file mode 100644 index 0000000..82b1d64 Binary files /dev/null and b/tools/k1_harness/suites/__pycache__/ui_nav.cpython-311.pyc differ diff --git a/tools/k1_harness/suites/app_protocol.py b/tools/k1_harness/suites/app_protocol.py new file mode 100644 index 0000000..397e46a --- /dev/null +++ b/tools/k1_harness/suites/app_protocol.py @@ -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:]}") diff --git a/tools/k1_harness/suites/ble_smoke.py b/tools/k1_harness/suites/ble_smoke.py new file mode 100644 index 0000000..c07370c --- /dev/null +++ b/tools/k1_harness/suites/ble_smoke.py @@ -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:]}") diff --git a/tools/k1_harness/suites/keys_tm1617.py b/tools/k1_harness/suites/keys_tm1617.py new file mode 100644 index 0000000..45361ba --- /dev/null +++ b/tools/k1_harness/suites/keys_tm1617.py @@ -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") diff --git a/tools/k1_harness/suites/keys_tm1629.py b/tools/k1_harness/suites/keys_tm1629.py new file mode 100644 index 0000000..66299c7 --- /dev/null +++ b/tools/k1_harness/suites/keys_tm1629.py @@ -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) # 恢复 diff --git a/tools/k1_harness/suites/packs_bin123.py b/tools/k1_harness/suites/packs_bin123.py new file mode 100644 index 0000000..a406880 --- /dev/null +++ b/tools/k1_harness/suites/packs_bin123.py @@ -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: + """发加载命令,校验 行 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 样本") diff --git a/tools/k1_harness/suites/ui_nav.py b/tools/k1_harness/suites/ui_nav.py new file mode 100644 index 0000000..ab9afb0 --- /dev/null +++ b/tools/k1_harness/suites/ui_nav.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""ui 套件:UI 页面状态 + 触摸注入导航 + 可选截屏. + +- `log status` → 解析 ui=,断言页名有效 +- `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 固件)") diff --git a/tools/k1_harness/transports/__init__.py b/tools/k1_harness/transports/__init__.py new file mode 100644 index 0000000..40a96af --- /dev/null +++ b/tools/k1_harness/transports/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/tools/k1_harness/transports/__pycache__/__init__.cpython-311.pyc b/tools/k1_harness/transports/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..e6413f9 Binary files /dev/null and b/tools/k1_harness/transports/__pycache__/__init__.cpython-311.pyc differ diff --git a/tools/k1_harness/transports/__pycache__/rtt_session.cpython-311.pyc b/tools/k1_harness/transports/__pycache__/rtt_session.cpython-311.pyc new file mode 100644 index 0000000..b35b70d Binary files /dev/null and b/tools/k1_harness/transports/__pycache__/rtt_session.cpython-311.pyc differ diff --git a/tools/k1_harness/transports/rtt_session.py b/tools/k1_harness/transports/rtt_session.py new file mode 100644 index 0000000..a54ccc5 --- /dev/null +++ b/tools/k1_harness/transports/rtt_session.py @@ -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()