# -*- coding: utf-8 -*- """ test_problem2_song_am.py — 问题2回归:曲目 LED 引导不得把 Am 改成 AM7 场景复现(修复前): 曲目模式 UI 显示 6m(Am),App 发 LED 引导帧 F0 60 04 05 02 02 F7 (协议文档示例,p3=0x02=大七),BL_Set_led 把 VI 中间键(17)写成 AM7, 拨弦发出 E2 A2 E3 G#3 C#4 E4 而非 Am 的 E2 A2 E3 A3 C4 E4。 本脚本(等效 MIDI-OX 抓包): RTT `string N` 与硅胶弦同一 SendNoteOn 路径,ACK 即线上实际音符。 1) 映射页路径设键17=Am(0x08) → chord key 17 → string 6..1 抓音符 2) BLE 发 LED 帧 04 05 02 02(模拟曲目 6m 引导) 3) 重新 chord key 17 → string 6..1 再抓,必须仍是 Am 4) 02 01 读回键17必须仍是 0x08 用法: python test_problem2_song_am.py 报告: Doc/reports/problem2_song_am_<时间戳>.md """ from __future__ import annotations import datetime import os import re import sys import time sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) from test_protocol_app_sim import build, hexs, Results # noqa: E402 from test_protocol_ble import BleMidiSim # noqa: E402 REPORTS_DIR = os.path.join( os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "Doc", "reports" ) KEY = 17 # VI 组中间键(报告问题键) PROTO_AM = 0x08 # 小三 # Am 期望: 弦6..1 = E2 A2 E3 A3 C4 E4 AM_NOTES = {6: 0x28, 5: 0x2D, 4: 0x34, 3: 0x39, 2: 0x3C, 1: 0x40} # AM7(修复前的错误发声): 弦3=0x38(G#3) 弦2=0x3D(C#4) AM7_NOTES = {6: 0x28, 5: 0x2D, 4: 0x34, 3: 0x38, 2: 0x3D, 1: 0x40} NOTE_NAME = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] def name(n: int) -> str: return f"{NOTE_NAME[n % 12]}{n // 12 - 1}" def read_map(sim): r = sim.query(build(0x02, 0x01), timeout=3.0) if r is None or len(r) != 47 or r[2] != 0x02 or r[3] != 0x01: return None return bytes(r[4:-1]) def chord_of(m: bytes, key1: int) -> int: return m[21 + key1 - 1] def _wait_new(sess, pattern: str, since: int, timeout: float = 3.0) -> str | None: """在 since 之后的新 RTT 行里等 pattern(伴奏日志洪水下不误判旧行)""" deadline = time.monotonic() + timeout rx = re.compile(pattern) while time.monotonic() < deadline: sess.pump(0.05) for line in sess.lines[since:]: if rx.search(line): return line return None def press_key(sess, key: int) -> None: """chord key N hold, 并等固件真正处理完 inject(USE_MIDI 已装载)""" since = len(sess.lines) sess.cmd(f"chord key {key} hold", settle=0.1) _wait_new(sess, rf"inject key={key}\b", since, 3.0) sess.pump(0.4) def release_key(sess) -> None: since = len(sess.lines) sess.cmd("chord key 0", settle=0.1) _wait_new(sess, r"inject key=0\b", since, 3.0) sess.pump(0.2) def strum_all(sess) -> dict[int, int]: """string 6..1 -> {弦号: 实际 MIDI 音符号}; 逐条等本条 ACK""" out: dict[int, int] = {} for s in (6, 5, 4, 3, 2, 1): since = len(sess.lines) sess.cmd(f"string {s}", settle=0.1) line = _wait_new(sess, rf"STRING_OK s={s} note=(\d+)", since, 3.0) if line: out[s] = int(re.search(r"note=(\d+)", line).group(1)) return out def fmt(notes: dict[int, int]) -> str: return " ".join(f"s{s}={name(n)}({n})" for s, n in sorted(notes.items(), reverse=True)) def run(R: Results, sim: BleMidiSim) -> None: from k1_harness.transports.rtt_session import open_session r = sim.query(build(0x01, 0x01)) if not (r and r[:3] == bytes([0xF0, 0x60, 0x01])): R.add("01 01 连接设备", "FAIL", hexs(r)) return R.add("01 01 连接设备", "PASS", "") orig = read_map(sim) if not orig: R.add("02 01 读原始映射表", "FAIL", "无应答") return orig_key = chord_of(orig, KEY) R.add("02 01 读原始映射表", "PASS", f"键{KEY} 原值=0x{orig_key:02X}") try: # 映射页路径: 键17 设为 Am(0x08) sim.send(build(0x02, 0x03, KEY - 1, PROTO_AM)) time.sleep(0.15) m = read_map(sim) ok = m is not None and chord_of(m, KEY) == PROTO_AM R.add(f"02 03 设键{KEY}=Am(0x08)", "PASS" if ok else "FAIL", f"读回=0x{chord_of(m, KEY):02X}" if m else "无应答") with open_session(reset=False) as sess: sess.cmd("tone pick", settle=1.0) # 专业+本地曲目(歌曲模式) sess.pump(1.0) # ---- 第 1 轮: LED 之前, 基线发声 ---- press_key(sess, KEY) base = strum_all(sess) ok = base == AM_NOTES R.add("基线: 键17 拨弦=Am", "PASS" if ok else "FAIL", fmt(base) + (" (Am 正确)" if ok else " (得到 AM7!)" if base == AM7_NOTES else " (异常)")) # ---- 模拟曲目 6m 的 LED 引导帧(文档示例 p3=0x02) ---- sim.send(build(0x04, 0x05, 0x02, 0x02)) time.sleep(0.3) m = read_map(sim) kept = m is not None and chord_of(m, KEY) == PROTO_AM R.add("LED 04 05 02 02 后映射表不被改写", "PASS" if kept else "FAIL", f"键{KEY}=0x{chord_of(m, KEY):02X}" if m else "无应答") # ---- 第 2 轮: LED 之后, 再按同键拨弦 ---- release_key(sess) press_key(sess, KEY) after = strum_all(sess) ok = after == AM_NOTES R.add("问题2: LED 后拨弦仍为 Am", "PASS" if ok else "FAIL", fmt(after) + (" (Am 正确, 修复生效)" if ok else " (变成 AM7, 问题仍在!)" if after == AM7_NOTES else " (异常)")) release_key(sess) finally: sim.send(build(0x02, 0x03, KEY - 1, orig_key)) time.sleep(0.15) m = read_map(sim) ok = m is not None and chord_of(m, KEY) == orig_key R.add(f"恢复键{KEY} 原始和弦", "PASS" if ok else "FAIL", "已恢复" if ok else "恢复失败, 请用 App 检查") def write_report(R: Results, path: str) -> None: n = {s: sum(1 for r in R.rows if r[1] == s) for s in ("PASS", "FAIL", "SKIP")} lines = [ "# 问题2回归报告: 曲目 6m(Am) 被 LED 改成 AM7", "", f"- 日期: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}", "- 测试脚本: tools/test_problem2_song_am.py", "- 方法: RTT `string N` 与硅胶弦同一 SendNoteOn 路径, ACK 即线上实际音符(等效 MIDI-OX 抓包)", "", f"PASS={n['PASS']} FAIL={n['FAIL']} SKIP={n['SKIP']}", "", "| # | 用例 | 结果 | 详情 |", "|---|------|------|------|", ] for i, (name_, status, detail) in enumerate(R.rows, 1): lines.append(f"| {i} | {name_} | {status} | {str(detail).replace('|', '/')} |") os.makedirs(os.path.dirname(os.path.abspath(path)), exist_ok=True) with open(path, "w", encoding="utf-8") as fp: fp.write("\n".join(lines) + "\n") def main() -> int: sim = BleMidiSim(transport="midi", unpair=False) try: if not sim.probe_or_rebind(): print("设备无应答, 终止") return 1 R = Results() run(R, sim) print(R.summary()) ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") path = os.path.join(REPORTS_DIR, f"problem2_song_am_{ts}.md") write_report(R, path) print(f"报告: {os.path.abspath(path)}") return 1 if any(r[1] == "FAIL" for r in R.rows) else 0 finally: sim.close() if __name__ == "__main__": sys.exit(main())