K1Guitar/tools/ble_log_pull.py

344 lines
11 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 -*-
"""
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 探活,失败时不立刻 unpairWindows 上 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="默认 midiuart 裸写在本模组上容易 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()