tools: add BLE/RTT/pick test and flash helper scripts
- ble_log_pull.py: pull field logs over BLE (midi/uart transport) - rtt_flash_log_dump.py / log_decode.py: dump and decode RAM/flash logs via J-Link RTT - _test_normal_pick.py: semi-automated normal-mode pick regression (RTT + physical pad) - _ble_channel_matrix.py / _probe_ble_rtt.py / _usb_midi_sysex_probe.py: GATT/BLE/USB-MIDI diagnostics - build_artist_kit.py: package artist tone kit - flash_boot_app_v024.py: flash boot+app images Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
f841d9e7b1
commit
daee47a457
|
|
@ -0,0 +1,140 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Systematic BLE channel matrix for Smart Guitar MIDI App SysEx."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import time
|
||||
from bleak import BleakClient, BleakScanner
|
||||
|
||||
NAME = "Smart Guitar MIDI"
|
||||
MIDI = "7772e5db-3868-4112-a1a9-f2669d106bf3"
|
||||
UARTW = "e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
UARTN = "e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
EFF2 = "0000eff2-0000-1000-8000-00805f9b34fb"
|
||||
|
||||
SYSEX = bytes.fromhex("F0600101F7")
|
||||
FRAMED = bytes.fromhex("8080F0600101F7")
|
||||
FRAMED_TSF7 = bytes.fromhex("8080F060010180F7")
|
||||
|
||||
|
||||
def hx(b: bytes) -> str:
|
||||
return " ".join(f"{x:02X}" for x in b) if b else "(none)"
|
||||
|
||||
|
||||
async def find(timeout=60.0):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
d = await BleakScanner.find_device_by_filter(
|
||||
lambda d, a: d.name and NAME.lower() in d.name.lower(), timeout=8
|
||||
)
|
||||
if d:
|
||||
return d
|
||||
print("waiting advertise...")
|
||||
return None
|
||||
|
||||
|
||||
async def unpair_if_needed(addr: str):
|
||||
try:
|
||||
c = BleakClient(addr, timeout=15)
|
||||
await c.connect()
|
||||
try:
|
||||
await c.unpair()
|
||||
print("unpaired")
|
||||
except Exception as e:
|
||||
print("unpair skip:", e)
|
||||
try:
|
||||
await c.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
await asyncio.sleep(1.5)
|
||||
except Exception as e:
|
||||
print("unpair session:", e)
|
||||
|
||||
|
||||
async def one_session(tag: str, write_uuid: str, payload: bytes, notify_uuids, settle=2.5):
|
||||
d = await find(45)
|
||||
if not d:
|
||||
print(f"[{tag}] NO DEVICE")
|
||||
return {"tag": tag, "ok": False, "err": "no device", "notifs": []}
|
||||
print(f"\n=== {tag} === write {write_uuid[:8]} {hx(payload)}")
|
||||
notifs = []
|
||||
err = None
|
||||
connected_end = False
|
||||
try:
|
||||
async with BleakClient(d, timeout=25) as c:
|
||||
print("connected", c.is_connected)
|
||||
|
||||
def cb(sender, data):
|
||||
b = bytes(data)
|
||||
notifs.append(b)
|
||||
print(f" NOTIFY {hx(b)}")
|
||||
|
||||
for u in notify_uuids:
|
||||
try:
|
||||
await c.start_notify(u, cb)
|
||||
print(" notify on", u[:8])
|
||||
except Exception as e:
|
||||
print(" notify fail", u[:8], e)
|
||||
await asyncio.sleep(0.3)
|
||||
try:
|
||||
await c.write_gatt_char(write_uuid, payload, response=False)
|
||||
print(" write ok")
|
||||
except Exception as e:
|
||||
err = f"write: {e}"
|
||||
print(" write fail", e)
|
||||
await asyncio.sleep(settle)
|
||||
connected_end = c.is_connected
|
||||
print(" end conn=", connected_end, "notifs=", len(notifs))
|
||||
except Exception as e:
|
||||
err = str(e)
|
||||
print(" session err", e)
|
||||
return {
|
||||
"tag": tag,
|
||||
"ok": any(b[:3] == b"\xF0\x60\x01" or b[:4] == b"\x80\x80\xF0\x60" for b in notifs),
|
||||
"notifs": [hx(b) for b in notifs],
|
||||
"conn_end": connected_end,
|
||||
"err": err,
|
||||
}
|
||||
|
||||
|
||||
async def main():
|
||||
d = await find(60)
|
||||
if not d:
|
||||
print("FATAL: device not advertising")
|
||||
return
|
||||
print("found", d.name, d.address)
|
||||
await unpair_if_needed(d.address)
|
||||
await asyncio.sleep(2)
|
||||
|
||||
results = []
|
||||
# matrix: do NOT put uartw first if it kills radio; do midi first
|
||||
cases = [
|
||||
("midi-framed", MIDI, FRAMED, [MIDI, UARTN, EFF2]),
|
||||
("midi-framed-tsF7", MIDI, FRAMED_TSF7, [MIDI]),
|
||||
("midi-raw", MIDI, SYSEX, [MIDI]),
|
||||
("eff2-raw", EFF2, SYSEX, [EFF2, MIDI, UARTN]),
|
||||
("eff2-framed", EFF2, FRAMED, [EFF2, MIDI]),
|
||||
("uartw-raw", UARTW, SYSEX, [UARTN, MIDI, EFF2]),
|
||||
("uartw-framed", UARTW, FRAMED, [UARTN, MIDI]),
|
||||
]
|
||||
for tag, wu, payload, nus in cases:
|
||||
r = await one_session(tag, wu, payload, nus)
|
||||
results.append(r)
|
||||
# if uart killed advertising, wait/reset hint
|
||||
if not r.get("conn_end", True):
|
||||
print("link dropped; waiting re-advertise...")
|
||||
await asyncio.sleep(3)
|
||||
if not await find(30):
|
||||
print("device gone after drop — stop matrix (need JLink reset)")
|
||||
break
|
||||
else:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
print("\n===== MATRIX SUMMARY =====")
|
||||
for r in results:
|
||||
status = "PASS" if r["ok"] else "FAIL"
|
||||
print(f"{status:4} {r['tag']:18} conn_end={r.get('conn_end')} notifs={r['notifs'] or ['(none)']} err={r.get('err')}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Probe: RTT log + BLE uartw write to see if MCU gets F0 60."""
|
||||
import asyncio
|
||||
import threading
|
||||
import time
|
||||
import pylink
|
||||
from bleak import BleakScanner, BleakClient
|
||||
|
||||
UARTW = "e49a25e0-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
UARTN = "e49a28e1-f69a-11e8-8eb2-f2801f1b9fd1"
|
||||
MIDI = "7772e5db-3868-4112-a1a9-f2669d106bf3"
|
||||
|
||||
|
||||
def open_jlink():
|
||||
j = pylink.JLink()
|
||||
j.open()
|
||||
try:
|
||||
j.exec_command("HideDeviceSelection = 1")
|
||||
except Exception:
|
||||
pass
|
||||
j.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||
last = None
|
||||
for dev in ("Cortex-M4", "AT32F403AC", "AT32F403A"):
|
||||
try:
|
||||
try:
|
||||
j.exec_command(f"Device = {dev}")
|
||||
except Exception:
|
||||
pass
|
||||
j.connect(dev)
|
||||
print("JLink connected", dev, flush=True)
|
||||
try:
|
||||
j.restart(halt=False)
|
||||
except Exception:
|
||||
try:
|
||||
j.go()
|
||||
except Exception:
|
||||
pass
|
||||
return j
|
||||
except Exception as e:
|
||||
last = e
|
||||
raise SystemExit(f"JLink connect failed: {last}")
|
||||
|
||||
|
||||
async def find_dev(timeout=60):
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < timeout:
|
||||
d = await BleakScanner.find_device_by_filter(
|
||||
lambda d, a: d.name and "Smart Guitar" in d.name, timeout=8
|
||||
)
|
||||
if d:
|
||||
return d
|
||||
print("waiting BLE...", flush=True)
|
||||
return None
|
||||
|
||||
|
||||
async def main():
|
||||
j = open_jlink()
|
||||
j.rtt_start()
|
||||
stop = False
|
||||
lines = []
|
||||
|
||||
def reader():
|
||||
while not stop:
|
||||
try:
|
||||
data = j.rtt_read(0, 4096)
|
||||
if data:
|
||||
s = bytes(data).decode("utf-8", "replace")
|
||||
lines.append(s)
|
||||
print("RTT>", s, end="" if s.endswith("\n") else "\n", flush=True)
|
||||
except Exception as e:
|
||||
print("rtt err", e, flush=True)
|
||||
break
|
||||
time.sleep(0.03)
|
||||
|
||||
th = threading.Thread(target=reader, daemon=True)
|
||||
th.start()
|
||||
print("collect boot logs 3s...", flush=True)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
d = await find_dev(90)
|
||||
if not d:
|
||||
print("NO BLE DEVICE", flush=True)
|
||||
stop = True
|
||||
j.close()
|
||||
return
|
||||
print("BLE", d.name, d.address, flush=True)
|
||||
|
||||
try:
|
||||
async with BleakClient(d, timeout=30) as c:
|
||||
print("connected", flush=True)
|
||||
notifs = []
|
||||
|
||||
def cb(_s, data):
|
||||
notifs.append(bytes(data))
|
||||
print("NOTIFY", data.hex(), flush=True)
|
||||
|
||||
for u in (UARTN, MIDI):
|
||||
try:
|
||||
await c.start_notify(u, cb)
|
||||
print("notify ok", u[:8], flush=True)
|
||||
except Exception as e:
|
||||
print("notify fail", u[:8], e, flush=True)
|
||||
|
||||
await asyncio.sleep(0.5)
|
||||
print("WRITE uartw F0 60 01 01 F7", flush=True)
|
||||
try:
|
||||
await c.write_gatt_char(UARTW, bytes.fromhex("F0600101F7"), response=False)
|
||||
print("write returned", flush=True)
|
||||
except Exception as e:
|
||||
print("write err", e, flush=True)
|
||||
await asyncio.sleep(4)
|
||||
print("notifs", len(notifs), "conn", c.is_connected, flush=True)
|
||||
except Exception as e:
|
||||
print("BLE session err", e, flush=True)
|
||||
|
||||
await asyncio.sleep(1)
|
||||
stop = True
|
||||
time.sleep(0.3)
|
||||
j.close()
|
||||
print("RTT total chunks", len(lines), flush=True)
|
||||
ble_hits = [x for x in lines if "BLE" in x or "sysex" in x]
|
||||
print("BLE-related RTT lines:", ble_hits, flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
#!/usr/bin/env python3
|
||||
"""RTT verify normal-mode: hold pad + pick sounds; release stops.
|
||||
|
||||
Uses real physical pad hold (inject is cleared by TM1629 scan within one tick).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import struct
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, ".")
|
||||
from rtt_pitch_reg_test import ( # noqa: E402
|
||||
RttSession,
|
||||
connect_jlink,
|
||||
find_rtt_control_block,
|
||||
wait_rtt_ready,
|
||||
)
|
||||
|
||||
SPEED_TAIL = struct.pack("<HHH", 40, 260, 1)
|
||||
GUI_SWITCH_SIZE = 8
|
||||
TAB_INDEX = 5
|
||||
|
||||
|
||||
def count_pitch(lines: list[str]) -> int:
|
||||
return sum(1 for L in lines if "[PITCH]" in L and " on " in L)
|
||||
|
||||
|
||||
def parse_tab(lines: list[str]) -> int | None:
|
||||
for L in reversed(lines):
|
||||
if "TONE tab=" not in L:
|
||||
continue
|
||||
try:
|
||||
return int(L.split("tab=")[1].split()[0])
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def with_halt(jlink, fn):
|
||||
was = jlink.halted()
|
||||
if not was:
|
||||
jlink.halt()
|
||||
try:
|
||||
return fn()
|
||||
finally:
|
||||
if not was:
|
||||
if hasattr(jlink, "restart"):
|
||||
jlink.restart()
|
||||
else:
|
||||
jlink.go()
|
||||
time.sleep(0.02)
|
||||
|
||||
|
||||
def find_pattern(jlink, needle: bytes, ram_base=0x20000000, ram_size=0x18000) -> int | None:
|
||||
def _find():
|
||||
chunk = 0x1000
|
||||
for off in range(0, ram_size, chunk):
|
||||
data = bytes(jlink.memory_read8(ram_base + off, min(chunk, ram_size - off)))
|
||||
idx = data.find(needle)
|
||||
if idx >= 0:
|
||||
return ram_base + off + idx
|
||||
return None
|
||||
|
||||
return with_halt(jlink, _find)
|
||||
|
||||
|
||||
def mem_write(jlink, addr: int, data: bytes) -> None:
|
||||
with_halt(jlink, lambda: jlink.memory_write8(addr, list(data)))
|
||||
|
||||
|
||||
def mem_read(jlink, addr: int, n: int) -> bytes:
|
||||
return with_halt(jlink, lambda: bytes(jlink.memory_read8(addr, n)))
|
||||
|
||||
|
||||
def force_tab_normal(jlink) -> bool:
|
||||
hit = find_pattern(jlink, SPEED_TAIL)
|
||||
if hit is None:
|
||||
print("!! GUI_SPEED row not found", flush=True)
|
||||
return False
|
||||
base = (hit - 2) - GUI_SWITCH_SIZE
|
||||
tab_addr = base + TAB_INDEX * GUI_SWITCH_SIZE
|
||||
mem_write(jlink, tab_addr, struct.pack("<H", 2))
|
||||
got = struct.unpack("<H", mem_read(jlink, tab_addr, 2))[0]
|
||||
print(f"mGuiData @ 0x{base:08X} forced TAB={got}", flush=True)
|
||||
return got == 2
|
||||
|
||||
|
||||
def wait_enter(msg: str, seconds: float) -> None:
|
||||
print(f"\n>>> {msg}", flush=True)
|
||||
print(f" ({seconds:.0f}s countdown)", flush=True)
|
||||
for left in range(int(seconds), 0, -1):
|
||||
print(f" {left}...", flush=True)
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
j = connect_jlink("Cortex-M4")
|
||||
cb = find_rtt_control_block(j)
|
||||
if not cb:
|
||||
raise SystemExit("RTT CB not found")
|
||||
print(f"RTT CB @ 0x{cb:08X}", flush=True)
|
||||
j.rtt_start(cb)
|
||||
wait_rtt_ready(j)
|
||||
sess = RttSession(j)
|
||||
|
||||
print("\n=== Setup: 1.bin + TAB=普通(2) ===", flush=True)
|
||||
sess.cmd("log clear", 0.2)
|
||||
sess.cmd_ack("tone bin1 0", "TONE_BIN1", timeout_s=4.0, retries=4)
|
||||
if not force_tab_normal(j):
|
||||
j.rtt_stop()
|
||||
j.close()
|
||||
return 3
|
||||
sess.cmd_ack("tone status", "TONE tab=", timeout_s=3.0, retries=4)
|
||||
print(f"tab={parse_tab(sess.lines)}", flush=True)
|
||||
|
||||
# Stop any leftover
|
||||
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
|
||||
force_tab_normal(j)
|
||||
|
||||
print("\n=== TEST1: pick with NO pad hold (expect silence) ===", flush=True)
|
||||
print("请双手离开指板。", flush=True)
|
||||
wait_enter("双手离开指板后等待自动拨片测试", 3)
|
||||
force_tab_normal(j)
|
||||
sess.cmd("log clear", 0.2)
|
||||
sess.cmd_ack("tone start", "TONE_START_DONE", timeout_s=3.0, retries=5)
|
||||
n1 = count_pitch(sess.pump(1.5))
|
||||
print(f"TEST1 PITCH on={n1} (expect 0)", flush=True)
|
||||
|
||||
print("\n=== TEST2: hold pad + pick (expect sound) ===", flush=True)
|
||||
wait_enter("请按住指板任意和弦键不放(建议按住第2排中部)", 5)
|
||||
force_tab_normal(j)
|
||||
sess.cmd("log clear", 0.2)
|
||||
# keep KEY from physical hold; tone start without clearing if we use 'k'
|
||||
# But we don't know KEY — physical hold already set KEY_ID+PressFlag via scan.
|
||||
# tone start WITHOUT k clears KEY_ID! Must use tone start k — but then KEY kept.
|
||||
# If user is holding, KEY_ID is set; PressFlag is set. tone start sets StartFlag=0
|
||||
# then clears KEY unless k. Use: don't clear — need tone start k AND physical KEY already set.
|
||||
sess.cmd_ack("tone start k", "TONE_START_DONE", timeout_s=3.0, retries=5)
|
||||
play = sess.pump(2.5)
|
||||
n2 = count_pitch(play)
|
||||
print(f"TEST2 PITCH on={n2} (expect >0) — keep holding!", flush=True)
|
||||
for L in play:
|
||||
if "[PITCH]" in L:
|
||||
print(" ", L, flush=True)
|
||||
break
|
||||
|
||||
print("\n=== TEST3: release pad (expect stop) ===", flush=True)
|
||||
wait_enter("请松开指板(松手)", 3)
|
||||
force_tab_normal(j)
|
||||
sess.cmd("log clear", 0.15)
|
||||
post = sess.pump(2.0)
|
||||
n3 = count_pitch(post)
|
||||
print(f"TEST3 PITCH on after release={n3} (expect 0)", flush=True)
|
||||
|
||||
print("\n=== TEST4: hold again + pick (expect sound) ===", flush=True)
|
||||
wait_enter("请再次按住指板和弦键不放", 5)
|
||||
force_tab_normal(j)
|
||||
sess.cmd("log clear", 0.2)
|
||||
sess.cmd_ack("tone start k", "TONE_START_DONE", timeout_s=3.0, retries=5)
|
||||
n4 = count_pitch(sess.pump(2.0))
|
||||
print(f"TEST4 PITCH on={n4} (expect >0)", flush=True)
|
||||
print("可松开指板。", flush=True)
|
||||
time.sleep(1.0)
|
||||
sess.cmd_ack("chord key 23", "CHORD_KEY_OK", timeout_s=2.0, retries=3)
|
||||
|
||||
t1, t2, t3, t4 = (n1 == 0), (n2 >= 1), (n3 == 0), (n4 >= 1)
|
||||
print("\n======== RESULT ========", flush=True)
|
||||
print(f"TEST1 no-hold silence: {'PASS' if t1 else 'FAIL'} (n={n1})", flush=True)
|
||||
print(f"TEST2 hold+pick sound: {'PASS' if t2 else 'FAIL'} (n={n2})", flush=True)
|
||||
print(f"TEST3 release stop: {'PASS' if t3 else 'FAIL'} (n={n3})", flush=True)
|
||||
print(f"TEST4 re-trigger: {'PASS' if t4 else 'FAIL'} (n={n4})", flush=True)
|
||||
ok_all = t1 and t2 and t3 and t4
|
||||
print("OVERALL:", "PASS" if ok_all else "FAIL", flush=True)
|
||||
j.rtt_stop()
|
||||
j.close()
|
||||
return 0 if ok_all else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Quick USB MIDI (SAM5704) SysEx probe for App F0 60 frames."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ctypes
|
||||
import ctypes.wintypes as w
|
||||
import sys
|
||||
import time
|
||||
|
||||
winmm = ctypes.WinDLL("winmm")
|
||||
CALLBACK_FUNCTION = 0x30000
|
||||
MIM_DATA, MIM_LONGDATA = 0x3C3, 0x3C4
|
||||
MHDR_DONE = 0x01
|
||||
|
||||
|
||||
class MIDIINCAPS(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wMid", w.WORD), ("wPid", w.WORD), ("vDriverVersion", w.DWORD),
|
||||
("szPname", ctypes.c_char * 32), ("dwSupport", w.DWORD),
|
||||
]
|
||||
|
||||
|
||||
class MIDIOUTCAPS(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wMid", w.WORD), ("wPid", w.WORD), ("vDriverVersion", w.DWORD),
|
||||
("szPname", ctypes.c_char * 32), ("wTechnology", w.WORD),
|
||||
("wVoices", w.WORD), ("wNotes", w.WORD), ("wChannelMask", w.WORD),
|
||||
("dwSupport", w.DWORD),
|
||||
]
|
||||
|
||||
|
||||
class MIDIHDR(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("lpData", ctypes.c_void_p),
|
||||
("dwBufferLength", w.DWORD),
|
||||
("dwBytesRecorded", w.DWORD),
|
||||
("dwUser", ctypes.c_void_p),
|
||||
("dwFlags", w.DWORD),
|
||||
("lpNext", ctypes.c_void_p),
|
||||
("reserved", ctypes.c_void_p),
|
||||
("dwOffset", w.DWORD),
|
||||
("dwReserved", ctypes.c_size_t * 8),
|
||||
]
|
||||
|
||||
|
||||
def find_sam(kind: str):
|
||||
n = winmm.midiInGetNumDevs() if kind == "in" else winmm.midiOutGetNumDevs()
|
||||
for i in range(n):
|
||||
if kind == "in":
|
||||
c = MIDIINCAPS()
|
||||
winmm.midiInGetDevCapsA(i, ctypes.byref(c), ctypes.sizeof(c))
|
||||
else:
|
||||
c = MIDIOUTCAPS()
|
||||
winmm.midiOutGetDevCapsA(i, ctypes.byref(c), ctypes.sizeof(c))
|
||||
name = c.szPname.decode("mbcs", "replace")
|
||||
if "SAM5704" in name:
|
||||
return i, name
|
||||
return None, None
|
||||
|
||||
|
||||
def hx(b: bytes) -> str:
|
||||
return " ".join(f"{x:02X}" for x in b)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
||||
in_i, in_n = find_sam("in")
|
||||
out_i, out_n = find_sam("out")
|
||||
print(f"IN={in_i} {in_n} OUT={out_i} {out_n}", flush=True)
|
||||
if in_i is None or out_i is None:
|
||||
print("SAM5704 not found")
|
||||
return 1
|
||||
|
||||
received: list[bytes] = []
|
||||
keep = [] # keep buffer refs alive
|
||||
|
||||
MidiInProc = ctypes.WINFUNCTYPE(
|
||||
None, w.HANDLE, w.UINT, ctypes.c_void_p, ctypes.c_size_t, ctypes.c_size_t
|
||||
)
|
||||
|
||||
@MidiInProc
|
||||
def on_midi_in(h, msg, _inst, p1, p2):
|
||||
if msg == MIM_LONGDATA:
|
||||
hdr = ctypes.cast(p1, ctypes.POINTER(MIDIHDR)).contents
|
||||
n = hdr.dwBytesRecorded
|
||||
if n and hdr.lpData:
|
||||
data = ctypes.string_at(hdr.lpData, n)
|
||||
received.append(data)
|
||||
print("RX", hx(data), flush=True)
|
||||
winmm.midiInAddBuffer(h, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||
elif msg == MIM_DATA:
|
||||
print(
|
||||
f"RX SHORT {(p1 & 0xFF):02X} {((p1 >> 8) & 0xFF):02X} {((p1 >> 16) & 0xFF):02X}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
h_in = w.HANDLE()
|
||||
r = winmm.midiInOpen(ctypes.byref(h_in), in_i, on_midi_in, 0, CALLBACK_FUNCTION)
|
||||
print("midiInOpen", r, flush=True)
|
||||
if r != 0:
|
||||
return r
|
||||
|
||||
for _ in range(4):
|
||||
buf = ctypes.create_string_buffer(1024)
|
||||
hdr = MIDIHDR()
|
||||
hdr.lpData = ctypes.cast(buf, ctypes.c_void_p)
|
||||
hdr.dwBufferLength = 1024
|
||||
keep.append((buf, hdr))
|
||||
winmm.midiInPrepareHeader(h_in, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||
winmm.midiInAddBuffer(h_in, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||
winmm.midiInStart(h_in)
|
||||
|
||||
h_out = w.HANDLE()
|
||||
r = winmm.midiOutOpen(ctypes.byref(h_out), out_i, 0, 0, 0)
|
||||
print("midiOutOpen", r, flush=True)
|
||||
if r != 0:
|
||||
return r
|
||||
|
||||
def send_sysex(msg: bytes):
|
||||
buf = ctypes.create_string_buffer(msg)
|
||||
hdr = MIDIHDR()
|
||||
hdr.lpData = ctypes.cast(buf, ctypes.c_void_p)
|
||||
hdr.dwBufferLength = len(msg)
|
||||
keep.append((buf, hdr))
|
||||
winmm.midiOutPrepareHeader(h_out, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||
print("TX", hx(msg), flush=True)
|
||||
winmm.midiOutLongMsg(h_out, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||
t0 = time.time()
|
||||
while not (hdr.dwFlags & MHDR_DONE) and time.time() - t0 < 2:
|
||||
time.sleep(0.01)
|
||||
winmm.midiOutUnprepareHeader(h_out, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||
|
||||
for label, msg in [
|
||||
("01 01", bytes.fromhex("F0600101F7")),
|
||||
("01 02", bytes.fromhex("F0600102F7")),
|
||||
("01 03", bytes.fromhex("F0600103F7")),
|
||||
]:
|
||||
print("---", label, flush=True)
|
||||
n0 = len(received)
|
||||
send_sysex(msg)
|
||||
time.sleep(1.2)
|
||||
print(" new rx", len(received) - n0, flush=True)
|
||||
|
||||
print("TOTAL RX", len(received), flush=True)
|
||||
app_like = [x for x in received if len(x) >= 2 and x[0] == 0xF0 and x[1] == 0x60]
|
||||
print("F0 60 replies", len(app_like), flush=True)
|
||||
|
||||
winmm.midiInStop(h_in)
|
||||
winmm.midiInReset(h_in)
|
||||
for buf, hdr in keep[:4]:
|
||||
winmm.midiInUnprepareHeader(h_in, ctypes.byref(hdr), ctypes.sizeof(MIDIHDR))
|
||||
winmm.midiInClose(h_in)
|
||||
winmm.midiOutClose(h_out)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -0,0 +1,343 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
ble_log_pull.py — 通过 BLE 拉取 K1 设备现场日志分区 (LOG.BIN)
|
||||
|
||||
协议 (SysEx, App->设备 F0 60 ... F7):
|
||||
07 00 查询 meta
|
||||
07 01 按 offset 读取分片 (每片最多 20 原始字节, nibble 编码)
|
||||
07 02 清空 (需确认码 55 2A)
|
||||
|
||||
注意: 本模组上 UART GATT 裸写容易导致断连;默认优先 BLE-MIDI。
|
||||
|
||||
依赖: bleak
|
||||
用法:
|
||||
python ble_log_pull.py --out LOG.BIN --decode crash.log -v
|
||||
python ble_log_pull.py --transport midi --address CB:4E:FD:F1:C0:79 --out LOG.BIN
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from test_protocol_ble import BleMidiSim, DEFAULT_NAME # noqa: E402
|
||||
from test_protocol_app_sim import HEAD, APP_ID, build, hexs # noqa: E402
|
||||
|
||||
CHUNK = 20
|
||||
|
||||
|
||||
def u32_to_7bit(v: int) -> bytes:
|
||||
v &= 0xFFFFFFFF
|
||||
return bytes(
|
||||
[
|
||||
(v >> 28) & 0x7F,
|
||||
(v >> 21) & 0x7F,
|
||||
(v >> 14) & 0x7F,
|
||||
(v >> 7) & 0x7F,
|
||||
v & 0x7F,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def u32_from_7bit(b: bytes, i: int = 0) -> int:
|
||||
return (
|
||||
((b[i] & 0x7F) << 28)
|
||||
| ((b[i + 1] & 0x7F) << 21)
|
||||
| ((b[i + 2] & 0x7F) << 14)
|
||||
| ((b[i + 3] & 0x7F) << 7)
|
||||
| (b[i + 4] & 0x7F)
|
||||
)
|
||||
|
||||
|
||||
def nibbles_to_bytes(nibs: bytes) -> bytes:
|
||||
out = bytearray()
|
||||
for i in range(0, len(nibs) - 1, 2):
|
||||
out.append(((nibs[i] & 0x0F) << 4) | (nibs[i + 1] & 0x0F))
|
||||
return bytes(out)
|
||||
|
||||
|
||||
def parse_meta(frame: bytes) -> dict:
|
||||
if len(frame) < 36 or frame[2] != 0x07 or frame[3] != 0x00:
|
||||
raise ValueError(f"bad meta frame len={len(frame)}: {hexs(frame)}")
|
||||
i = 4
|
||||
valid = frame[i]
|
||||
i += 1
|
||||
ver = frame[i]
|
||||
i += 1
|
||||
size = u32_from_7bit(frame, i)
|
||||
i += 5
|
||||
data_size = u32_from_7bit(frame, i)
|
||||
i += 5
|
||||
write_off = u32_from_7bit(frame, i)
|
||||
i += 5
|
||||
wrap = u32_from_7bit(frame, i)
|
||||
i += 5
|
||||
boot = u32_from_7bit(frame, i)
|
||||
i += 5
|
||||
lines = u32_from_7bit(frame, i)
|
||||
return {
|
||||
"valid": valid,
|
||||
"ver": ver,
|
||||
"partition_size": size,
|
||||
"data_size": data_size,
|
||||
"write_off": write_off,
|
||||
"wrap_count": wrap,
|
||||
"boot_count": boot,
|
||||
"line_count": lines,
|
||||
}
|
||||
|
||||
|
||||
def parse_chunk(frame: bytes) -> tuple[int, bytes]:
|
||||
if len(frame) < 11 or frame[2] != 0x07 or frame[3] != 0x01:
|
||||
raise ValueError(f"bad chunk frame: {hexs(frame)}")
|
||||
off = u32_from_7bit(frame, 4)
|
||||
n = frame[9]
|
||||
nibs = frame[10 : 10 + n]
|
||||
if len(nibs) < n:
|
||||
raise ValueError("truncated chunk")
|
||||
return off, nibbles_to_bytes(nibs)
|
||||
|
||||
|
||||
def safe_send(sim: BleMidiSim, frame: bytes) -> None:
|
||||
if not sim.is_connected():
|
||||
raise RuntimeError("Not connected")
|
||||
sim.send(frame)
|
||||
|
||||
|
||||
def wait_cmd(sim: BleMidiSim, cmd1: int, cmd2: int, timeout: float, verbose: bool):
|
||||
t0 = time.monotonic()
|
||||
others = []
|
||||
while time.monotonic() - t0 < timeout:
|
||||
if not sim.is_connected():
|
||||
if verbose:
|
||||
print(" (wait) link dropped")
|
||||
return None
|
||||
sim._pump()
|
||||
i = 0
|
||||
while i < len(sim.pending):
|
||||
fr = sim.pending[i]
|
||||
if (
|
||||
len(fr) >= 4
|
||||
and fr[0] == HEAD
|
||||
and fr[1] == APP_ID
|
||||
and fr[2] == cmd1
|
||||
and fr[3] == cmd2
|
||||
):
|
||||
sim.pending.pop(i)
|
||||
return fr
|
||||
others.append(sim.pending.pop(i))
|
||||
continue
|
||||
time.sleep(0.01)
|
||||
if verbose and others:
|
||||
print(f" (timeout) saw {len(others)} other frame(s), last={hexs(others[-1][:32])}")
|
||||
elif verbose:
|
||||
print(" (timeout) no frames received at all")
|
||||
return None
|
||||
|
||||
|
||||
def query_cmd(
|
||||
sim: BleMidiSim,
|
||||
frame: bytes,
|
||||
cmd1: int,
|
||||
cmd2: int,
|
||||
timeout: float,
|
||||
verbose: bool,
|
||||
retries: int = 2,
|
||||
):
|
||||
# 轻量 drain,避免 read_frame 清空 pending 时误伤
|
||||
t_drain = time.monotonic() + 0.15
|
||||
while time.monotonic() < t_drain:
|
||||
sim._pump()
|
||||
sim.pending.clear()
|
||||
time.sleep(0.02)
|
||||
|
||||
for attempt in range(retries + 1):
|
||||
if verbose:
|
||||
print(f" TX[{attempt}]: {hexs(frame)}")
|
||||
try:
|
||||
safe_send(sim, frame)
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
print(f" TX fail: {e}")
|
||||
return None
|
||||
fr = wait_cmd(sim, cmd1, cmd2, timeout=timeout, verbose=verbose)
|
||||
if fr is not None:
|
||||
if verbose:
|
||||
print(f" RX: {hexs(fr[:48])}{'...' if len(fr) > 48 else ''}")
|
||||
return fr
|
||||
time.sleep(0.25)
|
||||
return None
|
||||
|
||||
|
||||
def soft_probe(sim: BleMidiSim, verbose: bool = False) -> bool:
|
||||
"""只用 01 01 探活,失败时不立刻 unpair(Windows 上 unpair 易把模组打哑巴)。"""
|
||||
time.sleep(0.6) # 连接后给模组 settle
|
||||
r = query_cmd(sim, build(0x01, 0x01), 0x01, 0x01, timeout=3.0, verbose=verbose, retries=2)
|
||||
if r is not None and len(r) >= 5 and r[0] == HEAD and r[1] == APP_ID:
|
||||
return True
|
||||
if verbose:
|
||||
print(" soft probe failed; try probe_or_rebind once")
|
||||
try:
|
||||
return bool(
|
||||
sim.probe_or_rebind(
|
||||
name=sim.info.get("name") or DEFAULT_NAME,
|
||||
address=sim.info.get("address"),
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
if verbose:
|
||||
print(f" probe_or_rebind exception: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def pull(sim: BleMidiSim, out_path: str, timeout: float = 4.0, verbose: bool = False) -> dict:
|
||||
if not soft_probe(sim, verbose=verbose):
|
||||
raise RuntimeError(
|
||||
"BLE 协议无应答(01 01)。请确认琴已开机、蓝牙开关为开;"
|
||||
"若刚用过 --transport uart 导致断连,请断电复位后再用 --transport midi。"
|
||||
)
|
||||
|
||||
meta_frame = query_cmd(
|
||||
sim, build(0x07, 0x00), 0x07, 0x00, timeout=timeout, verbose=verbose, retries=3
|
||||
)
|
||||
if meta_frame is None:
|
||||
ver = query_cmd(
|
||||
sim, build(0x01, 0x03), 0x01, 0x03, timeout=2.0, verbose=verbose, retries=1
|
||||
)
|
||||
tip = ""
|
||||
if ver is not None:
|
||||
tip = (
|
||||
" 普通指令(01 03)正常,但 07 00 无应答 → 固件可能未含日志协议,请重新烧录。"
|
||||
)
|
||||
elif not sim.is_connected():
|
||||
tip = " 链路已断开(uart 传输常见)。请改用: python ble_log_pull.py --transport midi ..."
|
||||
raise RuntimeError("timeout waiting log meta (07 00)." + tip)
|
||||
|
||||
meta = parse_meta(meta_frame)
|
||||
print(
|
||||
f"meta valid={meta['valid']} ver={meta['ver']} size={meta['partition_size']} "
|
||||
f"write={meta['write_off']} wrap={meta['wrap_count']} "
|
||||
f"boot={meta['boot_count']} lines={meta['line_count']}"
|
||||
)
|
||||
if not meta["valid"] or meta["partition_size"] == 0:
|
||||
raise RuntimeError("device log partition invalid")
|
||||
|
||||
total = meta["partition_size"]
|
||||
buf = bytearray(b"\xff" * total)
|
||||
got = 0
|
||||
off = 0
|
||||
while off < total:
|
||||
want = min(CHUNK, total - off)
|
||||
req = bytes([HEAD, APP_ID, 0x07, 0x01]) + u32_to_7bit(off) + bytes([want, 0xF7])
|
||||
chunk = None
|
||||
for attempt in range(4):
|
||||
if not sim.is_connected():
|
||||
raise RuntimeError(f"link dropped at offset 0x{off:X}")
|
||||
try:
|
||||
safe_send(sim, req)
|
||||
except Exception as e:
|
||||
time.sleep(0.1 * (attempt + 1))
|
||||
if verbose:
|
||||
print(f" chunk TX fail @0x{off:X}: {e}")
|
||||
continue
|
||||
fr = wait_cmd(sim, 0x07, 0x01, timeout=timeout, verbose=False)
|
||||
if fr is None:
|
||||
time.sleep(0.05 * (attempt + 1))
|
||||
continue
|
||||
try:
|
||||
roff, data = parse_chunk(fr)
|
||||
except ValueError:
|
||||
continue
|
||||
if roff == off and data:
|
||||
chunk = data
|
||||
break
|
||||
if chunk is None:
|
||||
raise RuntimeError(f"timeout at offset 0x{off:X} (after retries)")
|
||||
buf[off : off + len(chunk)] = chunk
|
||||
got += len(chunk)
|
||||
off += len(chunk)
|
||||
if off % 4096 == 0 or off >= total:
|
||||
print(f" {off}/{total} ({100.0 * off / total:.1f}%)")
|
||||
|
||||
with open(out_path, "wb") as f:
|
||||
f.write(buf)
|
||||
print(f"wrote {out_path} ({got} bytes)")
|
||||
return meta
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Pull K1 field log over BLE")
|
||||
ap.add_argument("--out", default="LOG.BIN")
|
||||
ap.add_argument("--name", default=DEFAULT_NAME)
|
||||
ap.add_argument("--address", default=None)
|
||||
ap.add_argument(
|
||||
"--transport",
|
||||
choices=("midi", "uart", "auto"),
|
||||
default="midi",
|
||||
help="默认 midi(uart 裸写在本模组上容易 Not connected)",
|
||||
)
|
||||
ap.add_argument("--decode", default=None, help="also decode to this text path")
|
||||
ap.add_argument("--clear", action="store_true", help="clear flash log after pull")
|
||||
ap.add_argument("-v", "--verbose", action="store_true")
|
||||
ap.add_argument("--timeout", type=float, default=4.0)
|
||||
args = ap.parse_args()
|
||||
|
||||
transports = [args.transport]
|
||||
if args.transport == "auto":
|
||||
# midi 优先:uart GATT 写可能导致模组断连
|
||||
transports = ["midi", "uart"]
|
||||
elif args.transport == "uart":
|
||||
print(
|
||||
"警告: --transport uart 在本机模组上常导致 Not connected;"
|
||||
"若失败请改用 --transport midi"
|
||||
)
|
||||
|
||||
last_err = None
|
||||
for tr in transports:
|
||||
print(f"=== transport={tr} ===")
|
||||
sim = None
|
||||
try:
|
||||
sim = BleMidiSim(name=args.name, address=args.address, transport=tr)
|
||||
print(f"connected: {sim.info}")
|
||||
time.sleep(0.8)
|
||||
if not sim.is_connected():
|
||||
raise RuntimeError("connected then immediately dropped")
|
||||
meta = pull(sim, args.out, timeout=args.timeout, verbose=args.verbose)
|
||||
if args.decode:
|
||||
from log_decode import decode_file
|
||||
|
||||
decode_file(
|
||||
args.out,
|
||||
args.decode,
|
||||
meta.get("write_off", 0),
|
||||
meta.get("wrap_count", 0),
|
||||
)
|
||||
if args.clear:
|
||||
req = build(0x07, 0x02, 0x55, 0x2A)
|
||||
safe_send(sim, req)
|
||||
time.sleep(0.3)
|
||||
sim._pump()
|
||||
print("clear requested")
|
||||
return
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
print(f"FAILED ({tr}): {e}")
|
||||
finally:
|
||||
if sim is not None:
|
||||
try:
|
||||
sim.close()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(1.0)
|
||||
|
||||
raise SystemExit(
|
||||
f"all transports failed: {last_err}\n"
|
||||
"建议: 1) 断电复位吉他 2) python ble_log_pull.py --transport midi -v --out LOG.BIN --decode crash.log\n"
|
||||
"若有 J-Link: python rtt_flash_log_dump.py --out crash.log --scan"
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Assemble portable K1 artist tone-update kit + zip for handoff."""
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import zipfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parents[3] # 一诺国际吉他 (tools->proj->Code->repo)
|
||||
# __file__ = .../YNGJ.../tools/build_artist_kit.py → parents[0]=tools [1]=proj [2]=Code [3]=repo
|
||||
PROJ = Path(__file__).resolve().parents[1]
|
||||
TOOLS = Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def main() -> None:
|
||||
# Fix REPO: tools -> project -> Code -> 一诺国际吉他
|
||||
repo = TOOLS.parents[1] # Code's parent? TOOLS.parent=proj, TOOLS.parents[1]=Code, [2]=repo
|
||||
repo = TOOLS.parents[2]
|
||||
proj = TOOLS.parent
|
||||
day = datetime.now().strftime("%Y%m%d")
|
||||
kit_name = f"K1_音师音色更新工具_{day}"
|
||||
out_root = repo / "tools" / "out"
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
kit = out_root / kit_name
|
||||
if kit.exists():
|
||||
shutil.rmtree(kit)
|
||||
kit.mkdir(parents=True)
|
||||
|
||||
baseline = kit / "基线"
|
||||
drop = kit / "投放"
|
||||
outdir = kit / "输出"
|
||||
baseline.mkdir()
|
||||
drop.mkdir()
|
||||
outdir.mkdir()
|
||||
|
||||
shutil.copy2(TOOLS / "tone_artist_pack.py", kit / "tone_artist_pack.py")
|
||||
|
||||
bat = """@echo off
|
||||
chcp 65001 >nul
|
||||
setlocal
|
||||
cd /d "%~dp0"
|
||||
|
||||
echo.
|
||||
echo ========================================
|
||||
echo K1 音师音色更新工具 (USB / SoundWalkerIAP)
|
||||
echo 依赖: 仅 Python 3,无需 pip / 无需 J-Link
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
where python >nul 2>&1
|
||||
if errorlevel 1 (
|
||||
echo [错误] 未找到 python。请安装 Python 3,安装时勾选 Add python.exe to PATH。
|
||||
echo 下载: https://www.python.org/downloads/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo 默认读取「投放」目录中的 1.bin 2.bin 3.bin
|
||||
echo 也可传参,例如: 音师更新音色.bat --bin1 .\\1.bin --bin2 .\\2.bin --bin3 .\\3.bin
|
||||
echo.
|
||||
|
||||
python "%~dp0tone_artist_pack.py" --open %*
|
||||
set ERR=%ERRORLEVEL%
|
||||
echo.
|
||||
if not "%ERR%"=="0" (
|
||||
echo 打包失败。查看: python "%~dp0tone_artist_pack.py" -h
|
||||
pause
|
||||
exit /b %ERR%
|
||||
)
|
||||
echo 完成。请打开「输出\\音师音色包_日期」按 请这样刷.txt 用 SoundWalkerIAP 刷机。
|
||||
pause
|
||||
exit /b 0
|
||||
"""
|
||||
(kit / "音师更新音色.bat").write_text(bat, encoding="utf-8", newline="\r\n")
|
||||
|
||||
ziliao = next(
|
||||
p
|
||||
for p in repo.iterdir()
|
||||
if p.is_dir() and (p / "logo.bin").is_file() and (p / "Charg.bin").is_file()
|
||||
)
|
||||
shutil.copy2(ziliao / "logo.bin", baseline / "logo.bin")
|
||||
shutil.copy2(ziliao / "Charg.bin", baseline / "Charg.bin")
|
||||
shutil.copy2(proj / "tools" / "out" / "ui0902_res.bin", baseline / "ui0902_res.bin")
|
||||
|
||||
iap = repo / "升级" / "MCU主控升级"
|
||||
shutil.copy2(iap / "SoundWalkerIAP.exe", kit / "SoundWalkerIAP.exe")
|
||||
doc = iap / "升级步骤.docx"
|
||||
if doc.is_file():
|
||||
shutil.copy2(doc, kit / "升级步骤.docx")
|
||||
|
||||
src0909 = repo / "Doc" / "音色文件" / "0909"
|
||||
for n in ("1.bin", "2.bin", "3.bin"):
|
||||
shutil.copy2(src0909 / n, drop / n)
|
||||
(drop / "说明.txt").write_text(
|
||||
"请把新的 1.bin / 2.bin / 3.bin 放到本目录(覆盖即可),然后双击上一级「音师更新音色.bat」。\n"
|
||||
"\n"
|
||||
"也可用命令指定任意路径:\n"
|
||||
" 音师更新音色.bat --bin1 路径\\1.bin --bin2 路径\\2.bin --bin3 路径\\3.bin\n",
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
(kit / "使用说明.txt").write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"K1 音师音色更新工具 — 使用说明",
|
||||
"================================",
|
||||
"",
|
||||
"【需要准备】",
|
||||
" 1. 安装 Python 3(https://www.python.org/downloads/ ,勾选 Add to PATH)",
|
||||
" 只需标准库,不用 pip 装任何包",
|
||||
" 2. 本工具包(解压到任意目录,如桌面)",
|
||||
" 3. 吉他 + USB 线(SoundWalkerIAP 升级,不需要 J-Link)",
|
||||
"",
|
||||
"【目录说明】",
|
||||
" 投放\\ ← 把新的 1.bin 2.bin 3.bin 放这里",
|
||||
" 基线\\ ← logo / Charg / UI 底图(勿改)",
|
||||
" 输出\\ ← 打包结果(自动生成)",
|
||||
" 音师更新音色.bat",
|
||||
" tone_artist_pack.py",
|
||||
" SoundWalkerIAP.exe",
|
||||
" 升级步骤.docx",
|
||||
" 使用说明.txt ← 本文件",
|
||||
"",
|
||||
"【日常步骤】",
|
||||
" 1. 用新文件覆盖 投放\\1.bin、2.bin、3.bin",
|
||||
" 2. 双击「音师更新音色.bat」",
|
||||
" 3. 在弹出的「输出\\音师音色包_日期」里:",
|
||||
" - 打开 SoundWalkerIAP.exe",
|
||||
" - 吉他进入 USB 升级模式(见 升级步骤.docx)",
|
||||
" - 擦除外部 Flash",
|
||||
" - 把 extflash_ALL_artist_*.res 烧到地址 0x00000000",
|
||||
" 4. 退出升级模式,重启试听",
|
||||
"",
|
||||
"【命令参数示例】",
|
||||
" 音师更新音色.bat",
|
||||
" 音师更新音色.bat --drop 投放",
|
||||
" 音师更新音色.bat --bin1 .\\投放\\1.bin --bin2 .\\投放\\2.bin --bin3 .\\投放\\3.bin",
|
||||
" 音师更新音色.bat --bin1 D:\\我的音色\\1.bin --bin2 D:\\我的音色\\2.bin --bin3 D:\\我的音色\\3.bin",
|
||||
" python tone_artist_pack.py -h",
|
||||
"",
|
||||
"【生成的包格式】(与开发正式发布的 ALL.res 相同)",
|
||||
" 0x00000000 logo.bin",
|
||||
" 0x0000CB70 Charg.bin",
|
||||
" 0x0001B8F0 1.bin(节奏)",
|
||||
" 0x0009D07D 2.bin(本地曲,≤41KB)",
|
||||
" 0x000A71AC 3.bin(万能)",
|
||||
" 0x00100000 UI 界面图",
|
||||
"",
|
||||
"【注意】",
|
||||
" - 必须刷 ALL.res,不要只刷单独的 1/2/3.bin(否则模式选择花屏)",
|
||||
" - 一般只需刷外部 Flash,不用重刷 MCU 固件",
|
||||
" - 若增删本地曲目或改曲名,需联系开发同步固件后再测",
|
||||
" - 「投放」里已带示例 1/2/3.bin,可先双击 bat 试跑流程",
|
||||
"",
|
||||
"【出问题】",
|
||||
" - 提示找不到 python → 安装 Python 3 并勾选 PATH,重开窗口",
|
||||
" - DAB/大小校验失败 → 检查 bin 是否完整,2.bin 是否超过 41KB",
|
||||
" - 其它问题把黑色窗口全文截图发给开发",
|
||||
"",
|
||||
]
|
||||
),
|
||||
encoding="utf-8",
|
||||
newline="\n",
|
||||
)
|
||||
|
||||
zip_path = out_root / f"{kit_name}.zip"
|
||||
if zip_path.exists():
|
||||
zip_path.unlink()
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||
for f in kit.rglob("*"):
|
||||
if f.is_file():
|
||||
zf.write(f, f.relative_to(out_root).as_posix())
|
||||
|
||||
print(f"KIT {kit}")
|
||||
print(f"ZIP {zip_path}")
|
||||
print(f"ZIP size = {zip_path.stat().st_size} bytes")
|
||||
for p in sorted(kit.iterdir()):
|
||||
print(f" {p.name}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,169 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Flash Boot then APP via cspybat (AT32F403AC), reset, verify Boot strings."""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
BASE = Path(r"C:\Users\qjyu\Documents\SoundWalker\涓€璇哄浗闄呭悏浠朶Code\YNGJ-GT1-M - AT32F403ARCT7")
|
||||
STAGE = Path(r"C:\Temp\k1flash_boot")
|
||||
CSPY = Path(r"C:\Program Files (x86)\IAR Systems\Embedded Workbench 7.3\common\bin\cspybat.exe")
|
||||
JLINK = Path(r"C:\Program Files\SEGGER\JLink_V818\JLink.exe")
|
||||
|
||||
BOOT_OUT = BASE / "AT32F403ARCT7_BOOT" / "project" / "IAR_V7.4" / "AT32F403ARCT7_BOOT" / "Exe" / "AT32F403ARCT7_BOOT.out"
|
||||
APP_OUT = BASE / "project" / "IAR_V7.4" / "YNGJ-GT1-M" / "Exe" / "YNGJ-GT1-M.out"
|
||||
GEN_TMPL = BASE / "project" / "IAR_V7.4" / "settings" / "YNGJ-GT1-M.YNGJ-GT1-M.general.xcl"
|
||||
DRV_TMPL = BASE / "project" / "IAR_V7.4" / "settings" / "YNGJ-GT1-M.YNGJ-GT1-M.driver.xcl"
|
||||
RESET_JLINK = BASE / "tools" / "reset_run.jlink"
|
||||
|
||||
NEW_GBK = bytes([0xC9, 0xD5, 0xC2, 0xBC, 0xC4, 0xA3, 0xCA, 0xBD]) # 鐑у綍妯″紡
|
||||
OLD_GBK = bytes([0xC9, 0xFD, 0xBC, 0xB6, 0xC4, 0xA3, 0xCA, 0xBD]) # 鍗囩骇妯″紡
|
||||
|
||||
|
||||
def kill_debuggers() -> None:
|
||||
for n in (
|
||||
"cspybat.exe",
|
||||
"CSpyBat.exe",
|
||||
"JLink.exe",
|
||||
"IarIdePm.exe",
|
||||
"JLinkGUIServer.exe",
|
||||
"JLinkRTTClient.exe",
|
||||
"JFlash.exe",
|
||||
):
|
||||
subprocess.run(["taskkill", "/F", "/IM", n], capture_output=True)
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
def stage_xcl(out_file: Path) -> tuple[Path, Path]:
|
||||
STAGE.mkdir(parents=True, exist_ok=True)
|
||||
staged_out = STAGE / out_file.name
|
||||
shutil.copy2(out_file, staged_out)
|
||||
|
||||
gen_lines = GEN_TMPL.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
new_gen = []
|
||||
for ln in gen_lines:
|
||||
if ".out" in ln and ("YNGJ" in ln or "BOOT" in ln or "AT32" in ln):
|
||||
new_gen.append(f'"{staged_out}" ')
|
||||
else:
|
||||
new_gen.append(ln)
|
||||
gen_path = STAGE / "general.xcl"
|
||||
gen_path.write_text("\n".join(new_gen) + "\n", encoding="utf-8", newline="\n")
|
||||
|
||||
drv_lines = DRV_TMPL.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
forced = [ln for ln in drv_lines if not ln.strip().startswith("--jlink_device")]
|
||||
forced.append("--jlink_device=AT32F403AC")
|
||||
drv_path = STAGE / "driver.xcl"
|
||||
drv_path.write_text("\n".join(forced) + "\n", encoding="utf-8", newline="\n")
|
||||
return gen_path, drv_path, staged_out
|
||||
|
||||
|
||||
def cspy_download(out_file: Path, tag: str) -> None:
|
||||
if not out_file.is_file():
|
||||
raise SystemExit(f"missing {out_file}")
|
||||
gen_path, drv_path, staged_out = stage_xcl(out_file)
|
||||
logp = STAGE / f"cspy_{tag}.log"
|
||||
cmd = [
|
||||
str(CSPY),
|
||||
"-f",
|
||||
str(gen_path),
|
||||
f"--debug_file={staged_out}",
|
||||
"--download_only",
|
||||
"--backend",
|
||||
"-f",
|
||||
str(drv_path),
|
||||
]
|
||||
print(f"cspybat download {tag}: {staged_out.name}", flush=True)
|
||||
with logp.open("w", encoding="utf-8", errors="replace") as log:
|
||||
r = subprocess.run(cmd, stdout=log, stderr=subprocess.STDOUT, timeout=240)
|
||||
text = logp.read_text(encoding="utf-8", errors="replace")
|
||||
print(text[-1500:], flush=True)
|
||||
if r.returncode != 0:
|
||||
raise SystemExit(f"cspybat {tag} failed rc={r.returncode}")
|
||||
|
||||
|
||||
def jlink_reset() -> None:
|
||||
print("J-Link reset/run...", flush=True)
|
||||
subprocess.run(
|
||||
[
|
||||
str(JLINK),
|
||||
"-Device",
|
||||
"AT32F403AC",
|
||||
"-If",
|
||||
"SWD",
|
||||
"-Speed",
|
||||
"4000",
|
||||
"-AutoConnect",
|
||||
"1",
|
||||
"-CommandFile",
|
||||
str(RESET_JLINK),
|
||||
],
|
||||
capture_output=True,
|
||||
timeout=45,
|
||||
)
|
||||
time.sleep(2.5)
|
||||
|
||||
|
||||
def verify_boot_in_mcu() -> None:
|
||||
import pylink
|
||||
|
||||
j = pylink.JLink()
|
||||
j.open()
|
||||
try:
|
||||
try:
|
||||
j.exec_command("HideDeviceSelection = 1")
|
||||
except Exception:
|
||||
pass
|
||||
j.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
||||
last = None
|
||||
for dev in ("AT32F403AC", "Cortex-M4", "AT32F403A"):
|
||||
try:
|
||||
try:
|
||||
j.exec_command(f"Device = {dev}")
|
||||
except Exception:
|
||||
pass
|
||||
j.connect(dev)
|
||||
print(f"verify connect: {dev}", flush=True)
|
||||
break
|
||||
except Exception as exc:
|
||||
last = exc
|
||||
else:
|
||||
raise SystemExit(f"verify connect failed: {last}")
|
||||
|
||||
j.halt()
|
||||
# Boot image is in first ~32KB; search GBK markers
|
||||
chunk = bytes(j.memory_read8(0x08000000, 0x8000))
|
||||
has_new = NEW_GBK in chunk
|
||||
has_old = OLD_GBK in chunk
|
||||
print(f"MCU@0x08000000 contains 鐑у綍妯″紡={has_new} 鍗囩骇妯″紡={has_old}", flush=True)
|
||||
if not has_new:
|
||||
raise SystemExit("Boot flash verify FAILED: new 鐑у綍妯″紡 string missing")
|
||||
if has_old:
|
||||
print("WARN: old 鍗囩骇妯″紡 string still present somewhere in Boot region", flush=True)
|
||||
else:
|
||||
print("Boot flash verify OK", flush=True)
|
||||
j.reset(halt=False)
|
||||
finally:
|
||||
j.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
boot_bin = BOOT_OUT.with_suffix(".bin")
|
||||
data = boot_bin.read_bytes()
|
||||
print(
|
||||
f"local BOOT.bin: new={NEW_GBK in data} old={OLD_GBK in data} size={len(data)}",
|
||||
flush=True,
|
||||
)
|
||||
kill_debuggers()
|
||||
cspy_download(BOOT_OUT, "boot")
|
||||
kill_debuggers()
|
||||
cspy_download(APP_OUT, "app")
|
||||
jlink_reset()
|
||||
verify_boot_in_mcu()
|
||||
print("Done. Keep KEY for burn mode; screen should use UI0902_FLASH_MODE if ExtFlash has art.", flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
log_decode.py — 将 K1 LOG.BIN(W25Q 日志分区镜像)解码为可读时间线
|
||||
|
||||
布局:
|
||||
[0x0000 .. 0x0FFF] header (magic K1LG ...)
|
||||
[0x1000 .. end] 文本环形区,行以 \\n 结束
|
||||
|
||||
用法:
|
||||
python log_decode.py LOG.BIN -o problem.log
|
||||
python log_decode.py LOG.BIN --write-off 1234 --wrap 1 -o problem.log
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import struct
|
||||
|
||||
MAGIC = 0x4B314C47 # 'K1LG'
|
||||
HDR_SIZE = 0x1000
|
||||
|
||||
|
||||
def unpack_header(blob: bytes) -> dict:
|
||||
if len(blob) < 36:
|
||||
return {"valid": False}
|
||||
magic = struct.unpack_from("<I", blob, 0)[0]
|
||||
ver = struct.unpack_from("<H", blob, 4)[0]
|
||||
boot = struct.unpack_from("<I", blob, 8)[0]
|
||||
write_off = struct.unpack_from("<I", blob, 12)[0]
|
||||
wrap = struct.unpack_from("<I", blob, 16)[0]
|
||||
lines = struct.unpack_from("<I", blob, 20)[0]
|
||||
overflow = struct.unpack_from("<I", blob, 24)[0]
|
||||
seq = struct.unpack_from("<I", blob, 28)[0]
|
||||
return {
|
||||
"valid": magic == MAGIC,
|
||||
"magic": magic,
|
||||
"ver": ver,
|
||||
"boot_count": boot,
|
||||
"write_off": write_off,
|
||||
"wrap_count": wrap,
|
||||
"line_count": lines,
|
||||
"overflow": overflow,
|
||||
"seq": seq,
|
||||
}
|
||||
|
||||
|
||||
def extract_text(data: bytes, write_off: int, wrapped: bool) -> str:
|
||||
"""Linearize ring: if wrapped, [write_off..end) + [0..write_off); else [0..write_off)."""
|
||||
if not data:
|
||||
return ""
|
||||
if write_off > len(data):
|
||||
write_off = len(data)
|
||||
if wrapped and write_off < len(data):
|
||||
raw = data[write_off:] + data[:write_off]
|
||||
else:
|
||||
raw = data[:write_off] if write_off else data
|
||||
text = raw.decode("utf-8", errors="replace")
|
||||
text = text.replace("\xff", "")
|
||||
return text
|
||||
|
||||
|
||||
def decode_file(bin_path: str, out_path: str, write_off=None, wrap_count=None) -> None:
|
||||
with open(bin_path, "rb") as f:
|
||||
blob = f.read()
|
||||
hdr = unpack_header(blob)
|
||||
data = blob[HDR_SIZE:] if len(blob) > HDR_SIZE else b""
|
||||
|
||||
wo = write_off if write_off is not None else hdr.get("write_off", 0)
|
||||
wrap = wrap_count if wrap_count is not None else hdr.get("wrap_count", 0)
|
||||
|
||||
text = extract_text(data, wo, wrap > 0)
|
||||
lines = [ln for ln in text.splitlines() if ln.strip()]
|
||||
|
||||
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(
|
||||
f"# K1 log decode magic_ok={hdr.get('valid')} ver={hdr.get('ver')} "
|
||||
f"boot={hdr.get('boot_count')} write_off={wo} wrap={wrap} "
|
||||
f"line_count={hdr.get('line_count')} decoded_lines={len(lines)}\n"
|
||||
)
|
||||
for ln in lines:
|
||||
f.write(ln.rstrip("\r") + "\n")
|
||||
print(f"decoded {len(lines)} lines -> {out_path}")
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("bin")
|
||||
ap.add_argument("-o", "--out", default="problem.log")
|
||||
ap.add_argument("--write-off", type=int, default=None)
|
||||
ap.add_argument("--wrap", type=int, default=None)
|
||||
args = ap.parse_args()
|
||||
decode_file(args.bin, args.out, args.write_off, args.wrap)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Dump K1 persisted W25Q field log via J-Link RTT (`log flash dump` / `log flash scan`)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
from rtt_log_dump import ( # noqa: E402
|
||||
connect_jlink,
|
||||
find_rtt_control_block,
|
||||
import_deps,
|
||||
send_command,
|
||||
wait_rtt_ready,
|
||||
)
|
||||
|
||||
DUMP_CMD = b"log flash dump\n"
|
||||
SCAN_CMD = b"log flash scan 65536\n"
|
||||
|
||||
|
||||
def dump_flash_log(out_path: str, device: str, timeout_s: float, cmd: bytes = DUMP_CMD) -> None:
|
||||
jlink = connect_jlink(device)
|
||||
lines: list[str] = []
|
||||
try:
|
||||
cb = find_rtt_control_block(jlink)
|
||||
if cb is None:
|
||||
raise SystemExit("SEGGER RTT control block not found in SRAM")
|
||||
print(f"RTT CB @ 0x{cb:08X}")
|
||||
|
||||
jlink.rtt_start(cb)
|
||||
wait_rtt_ready(jlink)
|
||||
|
||||
for _ in range(5):
|
||||
stale = jlink.rtt_read(0, 4096)
|
||||
if stale:
|
||||
text = bytes(stale).decode("utf-8", errors="replace")
|
||||
if text.strip():
|
||||
print("RTT0 stale:", text.strip()[:200])
|
||||
time.sleep(0.05)
|
||||
|
||||
print(f"Sending: {cmd!r}")
|
||||
send_command(jlink, cmd)
|
||||
|
||||
buffer = b""
|
||||
deadline = time.time() + timeout_s
|
||||
while time.time() < deadline:
|
||||
chunk = jlink.rtt_read(0, 8192)
|
||||
if chunk:
|
||||
buffer += bytes(chunk)
|
||||
if len(buffer) >= 4096 and (len(buffer) % 8192) < 300:
|
||||
print(f" received {len(buffer)} bytes...")
|
||||
if b"LOG_FLASH_DUMP_END" in buffer:
|
||||
break
|
||||
else:
|
||||
time.sleep(0.01)
|
||||
|
||||
if b"LOG_FLASH_DUMP_END" not in buffer:
|
||||
preview = buffer[-500:].decode("utf-8", errors="replace") if buffer else ""
|
||||
raise SystemExit(
|
||||
"Timeout waiting for LOG_FLASH_DUMP_END.\n"
|
||||
f"got {len(buffer)} bytes. tail={preview!r}"
|
||||
)
|
||||
|
||||
text = buffer.decode("utf-8", errors="replace")
|
||||
capture = False
|
||||
for line in text.splitlines():
|
||||
if line.strip() == "LOG_FLASH_DUMP_BEGIN":
|
||||
capture = True
|
||||
continue
|
||||
if line.strip() == "LOG_FLASH_DUMP_END":
|
||||
break
|
||||
if capture:
|
||||
lines.append(line)
|
||||
|
||||
header = (
|
||||
f"# K1 flash log dump {datetime.now().isoformat(timespec='seconds')}\n"
|
||||
f"# device={device}\n"
|
||||
f"# cmd={cmd.decode('ascii', errors='replace').strip()}\n"
|
||||
)
|
||||
body = "\n".join(lines) + ("\n" if lines else "")
|
||||
with open(out_path, "w", encoding="utf-8", newline="\n") as f:
|
||||
f.write(header)
|
||||
f.write(body)
|
||||
print(f"Saved {len(lines)} lines to {out_path}")
|
||||
print(f"Full path: {os.path.abspath(out_path)}")
|
||||
finally:
|
||||
try:
|
||||
jlink.rtt_stop()
|
||||
except Exception:
|
||||
pass
|
||||
jlink.close()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Dump K1 Flash field log via RTT")
|
||||
parser.add_argument("--out", default="crash.log")
|
||||
parser.add_argument("--device", default="AT32F403AC")
|
||||
parser.add_argument("--timeout", type=float, default=300.0)
|
||||
parser.add_argument(
|
||||
"--scan",
|
||||
action="store_true",
|
||||
help="Ignore write pointer; scan first 64KB data for recoverable lines",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
import_deps()
|
||||
cmd = SCAN_CMD if args.scan else DUMP_CMD
|
||||
dump_flash_log(args.out, args.device, args.timeout, cmd=cmd)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue