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