209 lines
7.7 KiB
Python
209 lines
7.7 KiB
Python
|
|
# -*- 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())
|