# -*- coding: utf-8 -*- """ test_fret_positions.py — 同区三键(左/中/右)发声一致性的自动化实验 针对用户反馈: 1) App 重置后指板1三键都是 C, 但分别按左/中/右+拨同一根弦, 音高不同 2) App 把指板1三键配成不同和弦, 拨同一根弦, 音却相同 原理(固件): 按键 N -> TM1629_Handle -> group=(N-1)/3, type=chord_type_index_map[N].type -> USE_MIDI = Chord_Midi_Table[group][type] (同组三键共享一张表) -> 拨弦发声 = USE_MIDI.Midi_x (无任何位置相关偏移) 判定: - 三键 map 项相同而 USE_MIDI 不同 => 固件 bug - 三键 map 项相同且 USE_MIDI 相同 => 固件正常; 听到的差异来自 App 实际写入 (App 操作后立即用 --snapshot 读设备真实映射表即可见分晓) - 写入不同和弦后 USE_MIDI 相同 => 固件 bug; 不同 => 固件正常, 查 App 写链路 用法: python test_fret_positions.py # 完整实验(需 BLE + J-Link RTT) python test_fret_positions.py --snapshot # 只读当前映射表并打印 报告: Doc/reports/fret_positions_<时间戳>.md """ from __future__ import annotations import argparse import datetime import os 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 USE_MIDI_ADDR = 0x20000944 # STRING_MIDI USE_MIDI: Midi_6..Midi_1 + name* MAP_ADDR = 0x200006DC # CHORD_TYPE_INDEX chord_type_index_map[22] PROTO_NAME = {0x00: "M", 0x08: "m", 0x02: "M7", 0x0D: "7", 0x0A: "m7", 0x1F: "su2", 0x1E: "sus4", 0x04: "ad9", 0x0B: "m7-5"} REPORTS_DIR = os.path.join( os.path.dirname(os.path.abspath(__file__)), "..", "..", "..", "Doc", "reports" ) 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]) # 21B pitch + 21B chord def snapshot(sim) -> bool: m = read_map(sim) if not m: print("读映射表失败") return False print("键位 | 升降 | 协议码 | 和弦") for k in range(1, 22): pitch = m[k - 1] code = m[21 + k - 1] pname = PROTO_NAME.get(code, f"?0x{code:02X}") print(f"{k:3d} | {'升半音' if pitch == 2 else '原位'} | 0x{code:02X} | {pname}") return True def set_key(sim, key: int, proto_code: int, pitch: int = 1): """02 02 升降 + 02 03 和弦 (key: 1..21)""" sim.send(build(0x02, 0x02, key - 1, pitch)) time.sleep(0.08) sim.send(build(0x02, 0x03, key - 1, proto_code)) time.sleep(0.12) def read_use_midi(jlink) -> list[int]: """USE_MIDI 前 6 字节: [Midi_6, Midi_5, Midi_4, Midi_3, Midi_2, Midi_1]""" return list(jlink.memory_read8(USE_MIDI_ADDR, 6)) def read_map_entry(jlink, key: int) -> tuple[int, int]: """chord_type_index_map[key] -> (type, PitchOffset)""" b = jlink.memory_read8(MAP_ADDR + 4 * key, 4) return b[0], b[2] def run(report: 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])): report.add("01 01 连接设备", "FAIL", hexs(r)) return report.add("01 01 连接设备", "PASS", "") orig = read_map(sim) if not orig: report.add("02 01 读原始映射表", "FAIL", "无应答") return report.add("02 01 读原始映射表", "PASS", "") try: with open_session(reset=False) as sess: jlink = sess.jlink sess.cmd("tone pick f", settle=1.0) # 普通模式, 指板按压即装载 USE_MIDI sess.pump(1.5) # ---- 场景 A: 三键都设 C 大三原位(模拟"重置后都是 C") ---- for k in (1, 2, 3): set_key(sim, k, 0x00, 1) time.sleep(0.3) rows_a = [] for k in (1, 2, 3): sess.cmd(f"chord key {k}", settle=0.4) notes = read_use_midi(jlink) typ, pitch = read_map_entry(jlink, k) rows_a.append((k, typ, pitch, notes)) report.add( f"A: 键{k} 按压后 USE_MIDI", "PASS", f"type={typ} pitch={pitch} notes[6..1]={notes}", ) same_a = rows_a[0][3] == rows_a[1][3] == rows_a[2][3] report.add( "A: 三键同配置 -> 发声音符一致", "PASS" if same_a else "FAIL", "一致(固件正常)" if same_a else "不一致 => 固件 bug", ) # ---- 场景 B: 三键设不同和弦(M / m / m7) ---- set_key(sim, 1, 0x00) # M set_key(sim, 2, 0x08) # m set_key(sim, 3, 0x0A) # m7 time.sleep(0.3) rows_b = [] for k in (1, 2, 3): sess.cmd(f"chord key {k}", settle=0.4) notes = read_use_midi(jlink) typ, pitch = read_map_entry(jlink, k) rows_b.append((k, typ, pitch, notes)) report.add( f"B: 键{k} 按压后 USE_MIDI", "PASS", f"type={typ} pitch={pitch} notes[6..1]={notes}", ) diff_b = not (rows_b[0][3] == rows_b[1][3] == rows_b[2][3]) expect_types = [0, 1, 2] types_ok = [r[1] for r in rows_b] == expect_types report.add( "B: 三键不同配置 -> 发声音符不同", "PASS" if (diff_b and types_ok) else "FAIL", f"types={[r[1] for r in rows_b]} " + ("各不相同(固件正常)" if diff_b else "完全相同 => 固件未应用 => 固件 bug"), ) finally: r = sim.query(build(0x02, 0x04, *orig), timeout=3.0) ok = read_map(sim) == orig report.add("恢复原始映射表", "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 = [ "# 同区三键发声一致性实验报告", "", f"- 日期: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}", "- 测试脚本: tools/test_fret_positions.py", "", 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: ap = argparse.ArgumentParser(description="同区三键发声一致性实验") ap.add_argument("--snapshot", action="store_true", help="只打印当前映射表") args = ap.parse_args() sim = BleMidiSim(transport="midi", unpair=False) try: if not sim.probe_or_rebind(): print("设备无应答, 终止") return 1 if args.snapshot: return 0 if snapshot(sim) else 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"fret_positions_{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())