288 lines
12 KiB
Python
288 lines
12 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
||
|
|
"""
|
||
|
|
test_chord_map_fix.py — 和弦映射 m7/M7 等读回编码修复的专项验证
|
||
|
|
|
||
|
|
背景:
|
||
|
|
固件读路径(handleReadRhythmMap / Return_Light_buff)曾把内部 type 与协议码对调:
|
||
|
|
m7(type2) 误报 0x02(M7), M7(type4) 误报 0x0A(m7),
|
||
|
|
ad9/sus2/sus4/m7-5 同样错位; 属七写只认 0x13 不认文档的 0x0D。
|
||
|
|
本脚本验证修复后的固件: 写->读 回环必须逐字节一致。
|
||
|
|
|
||
|
|
链路: 笔记本 BLE <-> "Smart Guitar MIDI" <-> 吉他 UART4
|
||
|
|
用法:
|
||
|
|
python test_chord_map_fix.py # 协议回环 + 交互按键上报验证
|
||
|
|
python test_chord_map_fix.py --skip-manual # 只跑协议回环(无需碰琴)
|
||
|
|
python test_chord_map_fix.py --transport uart # 自定义串口通道
|
||
|
|
|
||
|
|
报告: Doc/reports/chord_map_fix_<时间戳>.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, HEAD, DEV_ID # noqa: E402
|
||
|
|
from test_protocol_ble import BleMidiSim # noqa: E402
|
||
|
|
|
||
|
|
# 协议码 -> (名称, 内部type) 依据 Doc/指令测试.docx 和弦指令映射表
|
||
|
|
CHORDS = [
|
||
|
|
(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):
|
||
|
|
"""02 01 -> 42B (21 pitch + 21 chord) 或 None"""
|
||
|
|
r = sim.query(build(0x02, 0x01), timeout=3.0)
|
||
|
|
if r is None or len(r) != 47 or r[0] != HEAD or r[1] != 0x60 or r[2] != 0x02 or r[3] != 0x01:
|
||
|
|
return None
|
||
|
|
return bytes(r[4:-1])
|
||
|
|
|
||
|
|
|
||
|
|
def chord_of(m: bytes, key1: int) -> int:
|
||
|
|
"""key1: 1..21 -> 协议和弦码"""
|
||
|
|
return m[21 + key1 - 1]
|
||
|
|
|
||
|
|
|
||
|
|
def run(report: Results, sim: BleMidiSim, skip_manual: bool, auto: bool = False) -> None:
|
||
|
|
# ---- 0. 连接与版本 ----
|
||
|
|
r = sim.query(build(0x01, 0x01))
|
||
|
|
ok = r is not None and r[:3] == bytes([HEAD, 0x60, 0x01])
|
||
|
|
report.add("01 01 连接设备", "PASS" if ok else "FAIL", hexs(r))
|
||
|
|
if not ok:
|
||
|
|
report.add("后续用例", "SKIP", "连接失败, 级联跳过")
|
||
|
|
return
|
||
|
|
|
||
|
|
r = sim.query(build(0x01, 0x0C))
|
||
|
|
build_id = bytes(r[4:-1]).decode("ascii", "replace") if r and len(r) > 5 else "?"
|
||
|
|
report.add("01 0C 固件构建", "PASS" if r else "FAIL", build_id)
|
||
|
|
|
||
|
|
# ---- 1. 读原始映射表并保存(测试完恢复) ----
|
||
|
|
orig = read_map(sim)
|
||
|
|
report.add("02 01 读原始映射表", "PASS" if orig else "FAIL",
|
||
|
|
hexs(bytes([HEAD, 0x60, 0x02, 0x01]) + orig + bytes([0xF7])) if orig else "无应答")
|
||
|
|
if not orig:
|
||
|
|
report.add("后续用例", "SKIP", "读表失败, 级联跳过")
|
||
|
|
return
|
||
|
|
|
||
|
|
KEY = 1 # 用 1 号键(I 区上C)做逐码回环
|
||
|
|
try:
|
||
|
|
# ---- 2. 单键逐码写->读回环(核心回归: m7/M7/sus2/sus4/ad9/m7-5/属七) ----
|
||
|
|
for code, name in CHORDS:
|
||
|
|
sim.send(build(0x02, 0x03, KEY - 1, code))
|
||
|
|
time.sleep(0.15)
|
||
|
|
m = read_map(sim)
|
||
|
|
got = chord_of(m, KEY) if m else None
|
||
|
|
ok = got == code
|
||
|
|
report.add(
|
||
|
|
f"02 03 写 {name} (0x{code:02X}) 读回一致",
|
||
|
|
"PASS" if ok else "FAIL",
|
||
|
|
f"读回=0x{got:02X}" if got is not None else "读表失败",
|
||
|
|
)
|
||
|
|
|
||
|
|
# 属七旧码 0x13 兼容: 写入后应按文档码 0x0D 读回
|
||
|
|
sim.send(build(0x02, 0x03, KEY - 1, 0x13))
|
||
|
|
time.sleep(0.15)
|
||
|
|
m = read_map(sim)
|
||
|
|
got = chord_of(m, KEY) if m else None
|
||
|
|
report.add(
|
||
|
|
"02 03 写属七旧码 0x13 -> 读回 0x0D",
|
||
|
|
"PASS" if got == 0x0D else "FAIL",
|
||
|
|
f"读回=0x{got:02X}" if got is not None else "读表失败",
|
||
|
|
)
|
||
|
|
|
||
|
|
# ---- 3. 02 04 整表写入(含全部 9 种和弦码) -> 回读逐字节一致 ----
|
||
|
|
pitch = bytes([0x01] * 21)
|
||
|
|
codes = bytes(c for c, _ in CHORDS)
|
||
|
|
chord = bytes(codes[i % len(codes)] for i in range(21))
|
||
|
|
full = pitch + chord
|
||
|
|
r = sim.query(build(0x02, 0x04, *full), timeout=3.0)
|
||
|
|
ok = r is not None and len(r) == 47 and bytes(r[4:-1]) == full
|
||
|
|
report.add("02 04 整表写入(回读帧一致)", "PASS" if ok else "FAIL", hexs(r))
|
||
|
|
m = read_map(sim)
|
||
|
|
ok2 = m == full
|
||
|
|
report.add(
|
||
|
|
"02 04 写入后独立读回一致",
|
||
|
|
"PASS" if ok2 else "FAIL",
|
||
|
|
"42B 全等" if ok2 else f"diff at {[i for i in range(42) if m and m[i] != full[i]]}",
|
||
|
|
)
|
||
|
|
|
||
|
|
# ---- 4. F0 51 04 按键上报(Return_Light_buff)编码验证 ----
|
||
|
|
if skip_manual:
|
||
|
|
report.add("F0 51 04 按键上报编码", "SKIP", "--skip-manual")
|
||
|
|
elif auto:
|
||
|
|
run_auto(report, sim, orig)
|
||
|
|
else:
|
||
|
|
run_manual(report, sim, orig)
|
||
|
|
finally:
|
||
|
|
# ---- 5. 恢复原始映射表 ----
|
||
|
|
r = sim.query(build(0x02, 0x04, *orig), timeout=3.0)
|
||
|
|
m = read_map(sim)
|
||
|
|
ok = m == orig
|
||
|
|
report.add("恢复原始映射表", "PASS" if ok else "FAIL",
|
||
|
|
"已恢复" if ok else "恢复失败, 请用 App 重置")
|
||
|
|
|
||
|
|
|
||
|
|
def run_manual(report: Results, sim: BleMidiSim, orig: bytes) -> None:
|
||
|
|
"""交互验证 F0 51 04 按键上报编码; 结束后恢复原始映射表"""
|
||
|
|
try:
|
||
|
|
for code, name in ((0x02, "M7"), (0x0A, "m7")):
|
||
|
|
# 04 00: LED_L1 -> Led=0, 且把 2 号键(中间C) type 设为该和弦
|
||
|
|
sim.send(build(0x04, 0x00, 0x00, code))
|
||
|
|
time.sleep(0.2)
|
||
|
|
sim.drain()
|
||
|
|
print(f"\n>>> 下一轮验证 {name}: 5 秒后开始监听, 听到倒计时结束请【按住】I 区中间键(2号键) 并【拨弦】")
|
||
|
|
for i in range(5, 0, -1):
|
||
|
|
print(f" {i} ...")
|
||
|
|
time.sleep(1)
|
||
|
|
print(f">>> 请操作! 监听中 (30s) ...")
|
||
|
|
# 先抓任意设备上报帧做诊断, 再判定内容
|
||
|
|
f = sim.read_frame(timeout=30.0, want_dev=True)
|
||
|
|
if f is None:
|
||
|
|
report.add(f"F0 51 04 按键上报 {name} (期望 0x{code:02X})", "FAIL",
|
||
|
|
"30s 内无任何设备上报帧")
|
||
|
|
continue
|
||
|
|
ok = (
|
||
|
|
len(f) == 7
|
||
|
|
and f[1] == DEV_ID and f[2] == 0x04
|
||
|
|
and f[3] == 0x00 and f[5] == code
|
||
|
|
)
|
||
|
|
report.add(
|
||
|
|
f"F0 51 04 按键上报 {name} (期望 0x{code:02X})",
|
||
|
|
"PASS" if ok else "FAIL",
|
||
|
|
hexs(f),
|
||
|
|
)
|
||
|
|
finally:
|
||
|
|
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 run_auto(report: Results, sim: BleMidiSim, orig: bytes) -> None:
|
||
|
|
"""RTT 注入自动验证: 无需人工操作。
|
||
|
|
|
||
|
|
流程: RTT `tone pick` 进入专业模式播放态 -> BLE `04 00` 设 Led=0 并改 2 号键和弦
|
||
|
|
-> RTT `chord key 2` 注入指板按压(与物理按键同路径 TM1629_Handle)
|
||
|
|
-> BLE 捕获 F0 51 04 上报帧验证编码; RTT 日志确认和弦切换。
|
||
|
|
"""
|
||
|
|
try:
|
||
|
|
from k1_harness.transports.rtt_session import open_session
|
||
|
|
except Exception as e:
|
||
|
|
report.add("RTT 自动验证", "FAIL", f"无法加载 RTT 传输: {e}")
|
||
|
|
return
|
||
|
|
try:
|
||
|
|
with open_session(reset=False) as sess:
|
||
|
|
sess.cmd("tone pick", settle=1.0) # 专业+本地曲目, 起奏 I 和弦
|
||
|
|
sess.pump(2.0)
|
||
|
|
for code, name in ((0x02, "M7"), (0x0A, "m7")):
|
||
|
|
sim.send(build(0x04, 0x00, 0x00, code)) # Led=0, map[2]=该和弦
|
||
|
|
time.sleep(0.2)
|
||
|
|
sim.drain()
|
||
|
|
sess.cmd("chord key 2", settle=0.5) # 注入按 I 区中间键
|
||
|
|
f = sim.read_frame(timeout=5.0, want_dev=True)
|
||
|
|
ok = (
|
||
|
|
f is not None and len(f) == 7
|
||
|
|
and f[1] == DEV_ID and f[2] == 0x04
|
||
|
|
and f[3] == 0x00 and f[5] == code
|
||
|
|
)
|
||
|
|
report.add(
|
||
|
|
f"F0 51 04 注入按键上报 {name} (期望 0x{code:02X})",
|
||
|
|
"PASS" if ok else "FAIL",
|
||
|
|
hexs(f) if f else "5s 内无设备上报帧",
|
||
|
|
)
|
||
|
|
recent = [l for l in sess.lines[-30:] if "chord" in l or "pick" in l]
|
||
|
|
report.add("RTT 日志(和弦切换)", "PASS" if recent else "SKIP",
|
||
|
|
" / ".join(recent[-4:]) if recent else "无相关日志")
|
||
|
|
except SystemExit as e:
|
||
|
|
report.add("RTT 自动验证", "FAIL", str(e))
|
||
|
|
except Exception as e:
|
||
|
|
report.add("RTT 自动验证", "FAIL", f"{type(e).__name__}: {e}")
|
||
|
|
finally:
|
||
|
|
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, info: dict, path: str) -> None:
|
||
|
|
n = {s: sum(1 for r in R.rows if r[1] == s) for s in ("PASS", "FAIL", "SKIP", "SENT")}
|
||
|
|
lines = [
|
||
|
|
"# 和弦映射修复验证报告",
|
||
|
|
"",
|
||
|
|
f"- 日期: {datetime.datetime.now():%Y-%m-%d %H:%M:%S}",
|
||
|
|
f"- 设备: {info.get('name')} ({info.get('address')})",
|
||
|
|
f"- 传输: {info.get('transport')} / bleak, payload={info.get('max_payload')}",
|
||
|
|
"- 测试脚本: tools/test_chord_map_fix.py",
|
||
|
|
"- 修复范围: handleReadRhythmMap / Return_Light_buff 读回编码; 属七 0x0D 兼容",
|
||
|
|
"",
|
||
|
|
"## 汇总",
|
||
|
|
"",
|
||
|
|
"| PASS | FAIL | SKIP |",
|
||
|
|
"|------|------|------|",
|
||
|
|
f"| {n['PASS']} | {n['FAIL']} | {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("--transport", choices=["midi", "uart"], default="midi")
|
||
|
|
ap.add_argument("--skip-manual", action="store_true", help="跳过按键上报交互验证")
|
||
|
|
ap.add_argument("--manual-only", action="store_true", help="只跑按键上报交互验证")
|
||
|
|
ap.add_argument("--auto", action="store_true",
|
||
|
|
help="用 RTT(J-Link) 注入按键代替人工操作, 全自动")
|
||
|
|
ap.add_argument("--address", help="直接指定 BLE 地址")
|
||
|
|
args = ap.parse_args()
|
||
|
|
|
||
|
|
sim = BleMidiSim(transport=args.transport, address=args.address, unpair=False)
|
||
|
|
try:
|
||
|
|
if not sim.probe_or_rebind():
|
||
|
|
print("设备无应答, 终止")
|
||
|
|
return 1
|
||
|
|
R = Results()
|
||
|
|
if args.manual_only:
|
||
|
|
orig = read_map(sim)
|
||
|
|
if not orig:
|
||
|
|
print("读映射表失败, 终止")
|
||
|
|
return 1
|
||
|
|
(run_auto if args.auto else run_manual)(R, sim, orig)
|
||
|
|
else:
|
||
|
|
run(R, sim, args.skip_manual, auto=args.auto)
|
||
|
|
print(R.summary())
|
||
|
|
ts = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
|
|
path = os.path.join(REPORTS_DIR, f"chord_map_fix_{ts}.md")
|
||
|
|
write_report(R, sim.info, 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())
|