K1Guitar/tools/test_problem2_song_am.py

209 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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())