From a495717883c02509b9672252b433c378c32b7dfe Mon Sep 17 00:00:00 2001 From: yuquanjun Date: Mon, 14 Sep 2026 15:05:17 +0800 Subject: [PATCH] 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 --- APP/app_log.c | 64 +++++++- APP/app_tm1617.c | 11 +- APP/app_tm1617.h | 4 + APP/app_tm1629.c | 22 ++- APP/app_tm1629.h | 1 + tools/k1_harness/README.md | 78 ++++++++++ tools/k1_harness/__init__.py | 9 ++ tools/k1_harness/__main__.py | 5 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 587 bytes .../__pycache__/__main__.cpython-311.pyc | Bin 0 -> 357 bytes .../__pycache__/cli.cpython-311.pyc | Bin 0 -> 10320 bytes .../__pycache__/paths.cpython-311.pyc | Bin 0 -> 1943 bytes tools/k1_harness/cli.py | 140 ++++++++++++++++++ tools/k1_harness/paths.py | 25 ++++ tools/k1_harness/report/__init__.py | 4 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 356 bytes .../__pycache__/markdown.cpython-311.pyc | Bin 0 -> 8333 bytes tools/k1_harness/report/markdown.py | 77 ++++++++++ tools/k1_harness/suites/__init__.py | 26 ++++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1171 bytes .../__pycache__/app_protocol.cpython-311.pyc | Bin 0 -> 4249 bytes .../__pycache__/ble_smoke.cpython-311.pyc | Bin 0 -> 2221 bytes .../__pycache__/keys_tm1617.cpython-311.pyc | Bin 0 -> 2950 bytes .../__pycache__/keys_tm1629.cpython-311.pyc | Bin 0 -> 3381 bytes .../__pycache__/packs_bin123.cpython-311.pyc | Bin 0 -> 8045 bytes .../suites/__pycache__/ui_nav.cpython-311.pyc | Bin 0 -> 4817 bytes tools/k1_harness/suites/app_protocol.py | 61 ++++++++ tools/k1_harness/suites/ble_smoke.py | 28 ++++ tools/k1_harness/suites/keys_tm1617.py | 34 +++++ tools/k1_harness/suites/keys_tm1629.py | 42 ++++++ tools/k1_harness/suites/packs_bin123.py | 106 +++++++++++++ tools/k1_harness/suites/ui_nav.py | 74 +++++++++ tools/k1_harness/transports/__init__.py | 1 + .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 238 bytes .../__pycache__/rtt_session.cpython-311.pyc | Bin 0 -> 2285 bytes tools/k1_harness/transports/rtt_session.py | 40 +++++ 36 files changed, 847 insertions(+), 5 deletions(-) create mode 100644 tools/k1_harness/README.md create mode 100644 tools/k1_harness/__init__.py create mode 100644 tools/k1_harness/__main__.py create mode 100644 tools/k1_harness/__pycache__/__init__.cpython-311.pyc create mode 100644 tools/k1_harness/__pycache__/__main__.cpython-311.pyc create mode 100644 tools/k1_harness/__pycache__/cli.cpython-311.pyc create mode 100644 tools/k1_harness/__pycache__/paths.cpython-311.pyc create mode 100644 tools/k1_harness/cli.py create mode 100644 tools/k1_harness/paths.py create mode 100644 tools/k1_harness/report/__init__.py create mode 100644 tools/k1_harness/report/__pycache__/__init__.cpython-311.pyc create mode 100644 tools/k1_harness/report/__pycache__/markdown.cpython-311.pyc create mode 100644 tools/k1_harness/report/markdown.py create mode 100644 tools/k1_harness/suites/__init__.py create mode 100644 tools/k1_harness/suites/__pycache__/__init__.cpython-311.pyc create mode 100644 tools/k1_harness/suites/__pycache__/app_protocol.cpython-311.pyc create mode 100644 tools/k1_harness/suites/__pycache__/ble_smoke.cpython-311.pyc create mode 100644 tools/k1_harness/suites/__pycache__/keys_tm1617.cpython-311.pyc create mode 100644 tools/k1_harness/suites/__pycache__/keys_tm1629.cpython-311.pyc create mode 100644 tools/k1_harness/suites/__pycache__/packs_bin123.cpython-311.pyc create mode 100644 tools/k1_harness/suites/__pycache__/ui_nav.cpython-311.pyc create mode 100644 tools/k1_harness/suites/app_protocol.py create mode 100644 tools/k1_harness/suites/ble_smoke.py create mode 100644 tools/k1_harness/suites/keys_tm1617.py create mode 100644 tools/k1_harness/suites/keys_tm1629.py create mode 100644 tools/k1_harness/suites/packs_bin123.py create mode 100644 tools/k1_harness/suites/ui_nav.py create mode 100644 tools/k1_harness/transports/__init__.py create mode 100644 tools/k1_harness/transports/__pycache__/__init__.cpython-311.pyc create mode 100644 tools/k1_harness/transports/__pycache__/rtt_session.cpython-311.pyc create mode 100644 tools/k1_harness/transports/rtt_session.py 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 0000000000000000000000000000000000000000..1b2dceebab56c43fc121f7ac396d43b93bab6d2e GIT binary patch literal 587 zcmZ8f-D}fO98I)13G5H>?f1Aoq_a*CiU_sqrnB9kt%wr1)F#-hX>QEDLHo8r=iEoD z4_nK+p&~1-BAYOj&QAA#_^3&mJ*_Z&@yVMG@#8!k_;C)0!@0LLJS=bw`vv^#Tf<7NW9 z#kbCK+gYu-A6C5Gb*EXkyB|Y>;A~fVYwce3y}PyKl^gEXyI!MgcedSE`(An3eLa7C zuxfWIM~fBb$%a#T=I!h-zx`$9_@E&Gh!8alNS8?bF`TLqpbDYLFfqkBtf8!7AfgsS zrlliaAutk~Nd{r`Ki_>#fsyNpcyN%K%4ERZYdZ_gQMcZ74(kyBld@Qq=JLo?9t^U8 zb%iN0q3Q+j4qv#{rroJRiqbTjLdCiuu&(ajy4RYAR6IJKjE9OcxKC>lFiJF!br6g2 z*Lb3sS2RPI4)R}z5R!2Yp*P%#Kl~5EC>D`!6U8Lb!`YH0jpLkERCG$DG`4j4PF5=@ zro{I4c0W0rpN}>cP9H&vVOf#xPESsWlbNuX^f$|lU7EOjaV(mOWv)mRV~t3EYDfsF rx=IlW8Ks}+uNJXvY05S89Bbfz19E{;|18h*{lE|(=yU&&8DQ!!8-wZb literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..16a8d43f2a2aea4186326b7590d336f9882714ed GIT binary patch literal 357 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 AGXMYp literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..4ee2a2b620edfe7728a885563e8800964e9e1dd9 GIT binary patch literal 1943 zcma)6-Af}^6u*-hlgW2XRVcM~Tv)oo>egEDgKi~_R$H@XNi4D*L-xB@X@{IqrZgDo3quI zM=hUu3}t&5iy4=Ww1L8?iVYMZ0(+r(OqcM}QR@HrnQ0x3oip?`!oE{FU$Yc<$; z)@?xcpTb^fkar!Wjbffk3Uz}9efSpqo_nz+{}Ih@KP0$uu4&B24-;}~QQk@&rIvHg zpAf@7HbiqAk^63k@CU~jmM3_I32--v?Dh`_%h7{}$^F&oTy%MV^;y%QvQYyf>3yLP z!~1wY6XNE4AUE8l-MC* zSU3RJ@6W8uKg}GzTFO0toSWZ~r;-H3_r@u_N2w{urTk(&yR$0KzAUgqeCp^X3Vzw` zNN)1?qtx_KY9+hFyqb`w9v#FIr@TUXiou##_+J47kWifd*3a=IGunVlDu$CV zrBI{_KS%jl2^(SjA;~!CboIG!+w9$B*XJ$?5Bf%FXyqhxm(xzVx^FvO1Ma-3(&4n* z9HpP0fD;9YzsI1kcX+?#XqNMSH8LLd!qmb+I>d8cHxmv~gTBBh&3ctAwqD9BJBN?H zm*=OmuYUI08H)Da>F@cXv1g#U@fOiY*aliY{OqF-T5K-+K&v-jwBFHX^0c76N(Qjt z$0aL8-}8k7JXwH{gokEp;6dJkog<-$09m7relt1oU}Dh~8;pAwyz7n0YN5JStZo%F zZK9^_7^o`V+r@RW`bB4gO5-g8-Xh{HP}w!trSZC5Odaj}(^8-4N@L9e)+}PpP}$Yj zrmuT`o@w4n6zj;Z(U7oCeBc6hV+Z;7$W~23$#(zk5I%$v+vOi{zg@ zlV$e)95cf#-Ce!^3$x55hK0ITv947xwTY&-h(2R5&GyVWXPirYg5jEIxE9f7^v2oN zxz3r+rFKDoRn%XN;2DcGriq&u%xkL0noaYs<|Hlj_rUthSs zG?Ac_cLZyvXzh%cGgURQp15PdvG#VNJJ~K&-4v^Cra|Q%YM8gjdedkvd^4CP+B@ld c;GDP3j|*76h}EaD`eU^kdG{DxAo4){3+A@d^#A|> literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5f4565fc1b68b54f25125069c2435d75f525c3cc GIT binary patch literal 356 zcmZ3^%ge<81V?8t&ngGfk3k$5V1hC}%K;hF8B!Qh7;_kM8KW3;nWC5&8B&;1nA4e} zm{VAS88lg6G6EH8GTq{GPApChN-ZwUDY?b#kywuEPxa237=BDPA z6vqVTm*%B}C+1|Q7R5Z-(ePsZuBWs2zMR?mbVBEo-P2;6^HWk|BK_RGblpP?b$u0d z6&yp1joeHOj2(lVL(F4J^7C_wW3mn7GayckDFVAOCO$qhFS8^*Uaz3?7l%!5eoARh qs$CH%j6nV?)&>$Em>C%vA28@&z=j@hi_eh1z@^#14uVA-K=lBzBWdmc literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..ad2e278ace7705a2252dd5b268043aa0088babe7 GIT binary patch literal 8333 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 F{{?P3oG}0Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..69091633183f87f497b941430bc981340374a76d GIT binary patch literal 1171 zcmb7D-*4Mg6u!<6$959L5J-$6aTO3O5}8N~s|Zk0b){nJI;lT|uq<7<@$Kft_BGdb zOA!wdqOb~3BQX(Ag@+1F3!5Mh6Wg?Q*}ss-;3vf5tbJOfi5H%@Zo-(*Ubw#c&N;Tv z_Z^?>b2*!p09(IpT|T!H2jFfL{IgO6hkIZ%rw%kA1C7&S24}|P7zebtmN4RGLQa@T zIcf4TZ>HpwDae8;$|47FLQ9_ImSZ@n@duXIQU^9Qw8GGe1FH$&^RlD~a#|DTL0fN2|y=#r|i?8>#8+WUp^_rXAn^%VS8{6Rz_1>9A-n-fiH-BW(50&0wIPLSt=3~ME_DRD`e7=g$-jU1Cy zaFDLxH3zw7@%YG_j|9cBAY-fLii4JPt2iS2~&|PWn)7sy5b^j`XuBDb@!Js(k+`kR7jz(M1olV*dt&CZXd1 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..43380060d52187fb7829fd677ebf598895bc20f0 GIT binary patch literal 4249 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..875e1671adee263d10881feb1d283ba9920d83fc GIT binary patch literal 2221 zcmaJ>T}%{L6uz@F`wzQ-O3?bVZAsgW?t%sFPtb&dAgw|ow901Nao8DQm))6leq^Cb zvrRQ(gLT`QN<}LVing@MgK7Pt{v>_q<6!n-GYN@FS<5~_Vr+;Hwdc;d3-rM|XU;wM zoO{nb_q*rZ%f-bu1Z{Toa_Aotp}+7T-wZi{c-#uY8N?zVVhPp|CL#u(fxt2uCViyd zQ$9-XjXonwvBt0|V)mI~j)^sgEfK5F3bLKG9i@CWmTX7vB4r;q?Fw_Q%%wB4w|||V zI{V(9U9Rl6cjkV(=8_^pFE>9mG>_!F|rf&5%QX)HE|Te z*ywE>P?#tnF=&+6<}Cyk|(zKD2kgJ z(Kx6k3hI@6OFLjMXWqyFU4Zfl6z&d|V#I22#%}@P14Ka9$I-{mK_X6s zK$B}M>eVy$BXou!&~OP$Kye$yQn0o{#hE0iK}B*KG=sNVBLWhh>~cN%6uO3Y{U1u4 zDESYE`W1hp5Ktla>myVE@!w z*Uj-IspTNf4rOjz3KE$Cl@2qibLIrtJtbyNY3F++!kV|cz$mp}hp DNjqJh literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..5bb2e1e1b26b713666cb6f5a0aa32b8aa7d34ac5 GIT binary patch literal 2950 zcmb7GYitu&7QQncKe5THBn?m)pu1pJxHuuu-Q{HxLX!|D!cMxRj;5ApNb~SGI}Zwx zqs^|0>ZEMrBB&{$+NdgN+U!zGt9G*xcG*8wevT#outusBZLv4mKe(bRMg7B`J9Zog znq7M5>bvKjx%b>N=broB`NU?kASnNy_@uAhfY3L%vr_aqfp~NngcxE`7h(yP93X;Z z7fB#i$5I2jAk{^ItnbpZ2G+=$STk$6Np%_664v$>)n#NidXT+TUIr8W+(prO(0PbX z--CfjDBcCl@y`A~x6ql`my5UKw5O%1{hN{BS(b1d?S#6)&+{S4FZG6aaf!_7tf4`U zXGBgEL93cpLZljU-;{6b^X-Xz>#|br&Ci)7!DUfCgy*e~ZhRMldODmJKT);W-NBtCdFoV6&iukiIn#Arr zRV3_1zT~oH?MwohA%`Ew(RY}Y7+`^NPP|Xz74XFE; z4^|&D4C~hYddsL`0)0rbvL`)*sF(|j771+m+7$_Od1~DV z0+*~4yJyLtiZGl3)(qZRH3|FqR0P-0rd`O1MCwiQg6=GOlOWJpM8Jdd%>JBv$XY}? z2UJ^*rx;8OZ7n_d?>#P9L9&yoS?UdPAz5NX)d&Yr=;cHiZ^;U~X>68$@7{yEA7|eE zDD%ZTDut;*?t(FxGS*lJQp@qpzvk|wc9+L9v|)8V zJ!H&xRI1U{bXqp%9RSV>6plRT@Zqh+WbsoFt`RK|m>3+t?hV5Pi$+PDu3_Rl>O?UJ z++{0EK+wqIm5#VSMRuJ^I`*s7Krhd!I!tWXEKQI6XBgW0U-&viP7r;+?z<@a8bSd% z$nlcs^MqucebqnE&k4Q(9**ZO3<^hw zDg~K$93BV-`~%`qM?t?8d=#Ak%Zq3cjUc769co-ss`gHL0H98@N>xoxUyP4n66*cu zOk7aItmihg)#drVGbK?VC{(4NVg2DL>dh#Z+ zTLgR?s|J=k-y7gmV<5;f{y@K~^Ruj~_jELQPpW3^;&Qc~^r(9H>Yxa3`HQL^?|`UM zgK}`tZp3%QU`UWuT|l~^QuyXkDc&FCR5H|$`y4N9hNVQD9JE%B3tRBoI*63u010!F z_)Fl&kfqH_Hk#H2S>QZXJL3tx9nx7`;NZ$~7YDCta=bY_vx zT#>Stk2jAsUz3$>Kc79AsuI7|5jL6l4q?n{xL_^Mnz5;z!5n_88UIehVtrn=AalvM zWz2G|PT5=+KR^4MgsClQYEw*Y>FS+-*nVSsqIz$#dT$n)%v&@VZHm-KpG%c(ifz5J zRoUj8t_Oe`KQ#@VJs zX=AdqQ7LUql|KJoPRNStPNfyD%+EQ746B2_DDlfBplO}vdsZC zZb-ebL#cWtDntck_bWIy&01`lwb(Qh3)a%{rm?1IVAOrt9f4=TRyOV%^IdmLzjWWD zobg0_37a=*^G3*&Y2!*nYG(zdt~K5n@4SCr>3CIXJ2x-P3rhF7gsCTK>QPKR*`NH# zfUmO=%gf6@uscr;7N3VCdjI7{QumK74V$6)iliD1#;?o-pxaIgHh3>=1hAV_Bg3$v z0K=#jhB+t09}dSb0%jAnQWE^Ui1S%=2of-G?ng>HEW%D~ViyLSwE{j~!V4I1I)86E z91OAY0C!ZVfi9*$F$9p+5d@K?D1yo&3>HM}Qqa!gx`3R@+PYvWAJN11U;owgp7;gD TdK~JAIcumOa1Q@3)ExRR=$<1Z literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9ba48532501b5d35a8c572e4c512ec5ca58b2ec7 GIT binary patch literal 3381 zcmb7GZ%`9w8h>}Q37e2W5C#8GXT^>V(~%NTJUbjM1x2NTCebSxO=I5GA|au>iB%?a zxT_ukQKEx}ifx@v1$x|RW^S&fh%NW2+{}I0%;W>JGoA5TH{q@*iK6^OTTdLD3fzOp&*Fc^!JPCr8;ZL~Q-A(2?d&ACV zds72kecKZYje^GU>)r953lBaVRf6N3qoc#_@|@sO->6)=d;iYo z53Zd}9RY2Lpg6H|2_K-n4Ki>F*I(8BecL z2=>DL&o>eeSdBG*&KhCh1pHD>e8rD$q(=+07i zmU(M7W}dy(Yj~;!)?>rs+w0Jgkv`3mUzyXZ8Acz|FaFjpm0u-pSe9lB_vB*Zv-7|QXPM!)m`e z>CH7Siri&tGJnT%^y#l(Qj@j#g*pJXE|~!vkqL>+`}Ap5GkTw*P&1<7A-PequCY@~ zkjLrb9m0ugACdNt*E_SH;%k|#&xFMj^Im)-PLjKXmbKp2cHZOj+C@1FAV+BBMIQ;c zc@)otwAjVZVt+d?GXxQR4Y21~%uIvvgQ-a+e2LqfR%0_qU_XV7fSz-=ze=HJ>}oiL zk}|Wu#$t&Rw2H3=(y2?IS@RNWY8#wsRD(J4Ar%Ar|KkV``b>2REowgU_z!#%VfsY>gHt?0b+7H z$94aw3*NP_*$#@lAllwLdB$g}^EiF&yxS|-TP{x0<%>*{sqhu4usW=ncsZPtTFs=7;3#VS;mMOHGV=WfcrY~=yWab7 zYB;l!#;*S)mZ3DWpV%t-h3o}@w#sX-15|u3w6E( zavB!m8bY#xbm~)^PIq`jo|AJ@A^Z0JCd*E!<(-Di)v^+upoTND2J@#{o!}>DJC?J6 z*d5L`S>?c3R$C74vF?=Bg2R0RHaOg8WHs@rC^H?t_KuP)QeQhff)@^ZPs-W=0e_!=Xx~`%v_&#o0{%#@HJWQpAeAoo_<9lmk3CSNztE@B7aWO%=vlVh_tx_x<1{N zZPMCp;ZqYIMA*hC+bFS(O8)AhyrDNQRSi}p5UVXpl0fZj-l|a13wrIXBP?uqbv;q@#Qu$lqP2o+`$7f&K zAQd`-LQs%4JBWpxNeelX7IISe^u~cbeS3nqx2eA=0M9)`-hi#oHne4I{d9%2|3JVN zF_@zUbAX;@a~AAUI#Dc@?G9_gn(0l_ezUaKF;hHKEWPcBu*ah8F^N3}zRQ0JzFA9L zK`0nu!A_LmiFg9fdtleVX1iVU{Wy!j<95=|hs`ptKLqm}kt= zVI0x(Q9UmuI|(0spFU`pHdRh;Z{Akl6k!17L~3c>MVB3!>$@%_1oW zS8HoBqUS-+s*yFU@Wu4uA+y{SQdAnU8 zASxGlAsa=KbYiLCg@Tj%znjcuVI#SM literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..422cd8386728594656bbc887b6d96b4322c3007f GIT binary patch literal 8045 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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..82b1d6421bb4fe222c3c66b24abc4c5f6d6cd9d8 GIT binary patch literal 4817 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 UR1t7?WQu|+0 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 0000000000000000000000000000000000000000..e6413f9a397ffe76651b514cf69bc670c673e72e GIT binary patch literal 238 zcmZ3^%ge<81Se)M&yobvk3k$5V1zP0vjG{?8B!R688jLFRx%VZ0r{Unl3(k9gtJvl zXmM&$aZF)WWoe8{esXDUYF&ryk0@&FAkgB{FKt1RJ$TppkqL8F6IXkAD9^#89y+ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..b35b70dec501cec355d47d971910509c2a5d8001 GIT binary patch literal 2285 zcmc&#UrbY17(e$<%k7`r0>eh4xU1-9U2rm&Y$J113L|q46JUR6n%vahD->?;?e0CD zU@)l*ZiS(eEK?@au}sm;MSU^`nI`JP9^0%B%}q#1MzK86#h8#i?K=gE#q4d#&OQD5 z{X5_JopZkPovS==N6>yBzuH4v5c-n{tBqY|s3(_!IgcddM-nB`3KgOKG(~tuVIr)b z1)h~GiX~$8TMK%tQs(DmuE(|-%5MXyFcZ=E?I1adHU~(~qT~dLFG@T}uA=0US;-+e zCHtUd5|r0h4ao17tnJ9l$GSk(*3lu%O;6*Ri6jpm*C4;gzEH#TS4dN_+=nAUHP|KV3-q#=t>f_FL+xVI z`)#eJ{Xoaw*WP=5&)x$*(<;fIheNW-7+4QcB;-o7!hbD7)RQKN@ByOW1n@KVo+4km z6BNR5j;n0|O@iO`qVEaHhH%-c6`=uYot++_<8E9$H-sd*)Z+^Z7f%kX%m6L1A5L&;NYI!Zfo;8o{NM|V2iB$F&>SH?5F$y9Tb9c?}`A8Dg4#)X_%)E-! z%fF=$uDyl!$GcwMWJS2JrXw7QYC0Ar zIck_ZOr^>pEcPgHWlhh~uqqKRaRq`{fMD9&`wc8dn)|}ov_$o=icK5wGH_7Grt4TR zj0>Z5IVkm;_O;cTY%~^$n%uIp7By|neIYqYZnkL&DViaBS)GH`o0eq+bm)l58n9Ic zsg(k>rze5&H3U8~WZejS*3%ygG-;t&L{_m8XxCz@^hr?ZmGuDBm)y;%?D%gB-<`^i z4$a*?7w~D49O!IqIat@yQD66=P$v}YM4PYUjR4j(#R&A)i`_w8g-HWg52^;)9wShI zynJT6qy45+i^{4EzMS?SWlYuBw5dlvwG~Mlyhs^ zxi#LLr`;CM3|~9ZoZ@$-`CSP*7Bge&8!OxVenXBk^X##wQ;b+|R<+?;l9P8RyojC*4qQMQUTl5v(Nn(_?j zmX?-gVRFZ?V~8K&6NPUnZ~IRoL2RQD_Hwe3^3|dTJF6P^p$GfyK0EW!MiE*|HND6@ z+)?FgU?09!;2Z2sWo3_88uX8>6yeJ@0`H}CQi;8EA*FOu!@T8YnJ7wHNEA)GC?1XB zm@bQ=&VbFduRJVD_=rx@4RSk(yA7R`ShJEOKqt zmqoZnvSB)W-Eye6O*Uc*F67G3=n1$Xy#~nN+}jaNiYf9t`c4qZOJdvr0vk+d@;^gbV4(m2 literal 0 HcmV?d00001 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()