207 lines
6.4 KiB
Python
207 lines
6.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Capture K1 LCD screen dump from J-Link RTT channel 1 and save as PNG."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import struct
|
|
import sys
|
|
import time
|
|
|
|
MAGIC = b"SCRN"
|
|
HEADER_FMT = "<4sHHHH"
|
|
HEADER_SIZE = struct.calcsize(HEADER_FMT)
|
|
DEFAULT_DEVICES = ("AT32F403AC", "AT32F403A", "Cortex-M4")
|
|
|
|
|
|
def rgb565_bytes_to_rgb(hi: int, lo: int) -> tuple[int, int, int]:
|
|
value = (hi << 8) | lo
|
|
red = ((value >> 11) & 0x1F) << 3
|
|
green = ((value >> 5) & 0x3F) << 2
|
|
blue = (value & 0x1F) << 3
|
|
return red, green, blue
|
|
|
|
|
|
def import_deps():
|
|
try:
|
|
import pylink # noqa: F401
|
|
except ImportError as exc:
|
|
raise SystemExit("Missing dependency: pip install pylink-square") from exc
|
|
try:
|
|
from PIL import Image # noqa: F401
|
|
except ImportError as exc:
|
|
raise SystemExit("Missing dependency: pip install pillow") from exc
|
|
|
|
|
|
def connect_jlink(device: str):
|
|
import pylink
|
|
|
|
jlink = pylink.JLink()
|
|
jlink.open()
|
|
jlink.set_tif(pylink.enums.JLinkInterfaces.SWD)
|
|
|
|
last_error = None
|
|
for candidate in (device, *DEFAULT_DEVICES):
|
|
try:
|
|
jlink.connect(candidate)
|
|
print(f"Connected as {candidate}")
|
|
return jlink
|
|
except pylink.errors.JLinkException as exc:
|
|
last_error = exc
|
|
jlink.close()
|
|
raise SystemExit(f"Failed to connect J-Link: {last_error}")
|
|
|
|
|
|
def find_rtt_control_block(jlink, ram_base: int = 0x20000000, ram_size: int = 0x18000) -> int | None:
|
|
# AT32F403ARCT7 physical SRAM is 96KB (0x18000).
|
|
"""Locate SEGGER RTT CB in SRAM (needed when auto-scan misses high BSS)."""
|
|
needle = b"SEGGER RTT"
|
|
chunk = 0x1000
|
|
was_halted = jlink.halted()
|
|
if not was_halted:
|
|
jlink.halt()
|
|
try:
|
|
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
|
|
finally:
|
|
if not was_halted:
|
|
# Resume only — never reset (would wipe the on-screen UI).
|
|
if hasattr(jlink, "restart"):
|
|
jlink.restart()
|
|
else:
|
|
jlink.go()
|
|
time.sleep(0.1)
|
|
return None
|
|
|
|
|
|
def wait_rtt_ready(jlink, timeout_s: float = 15.0) -> None:
|
|
deadline = time.time() + timeout_s
|
|
while time.time() < deadline:
|
|
try:
|
|
if jlink.rtt_get_num_up_buffers() > 0:
|
|
return
|
|
except Exception:
|
|
pass
|
|
time.sleep(0.2)
|
|
raise SystemExit("RTT control block found but buffers not ready")
|
|
|
|
|
|
def capture_rtt_png(out_path: str, device: str, timeout_s: float, do_reset: bool = False) -> None:
|
|
import pylink
|
|
from PIL import Image
|
|
|
|
jlink = connect_jlink(device)
|
|
try:
|
|
if do_reset:
|
|
jlink.reset(halt=False)
|
|
time.sleep(2.0)
|
|
|
|
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)
|
|
|
|
# Drain terminal (ch0) and show firmware ACK lines if any.
|
|
for _ in range(10):
|
|
term = jlink.rtt_read(0, 1024)
|
|
if term:
|
|
print("RTT0:", bytes(term).decode("utf-8", errors="replace").strip())
|
|
time.sleep(0.05)
|
|
|
|
print("RTT ready. Sending DUMP command...")
|
|
|
|
wrote = 0
|
|
for _ in range(30):
|
|
wrote = jlink.rtt_write(0, list(b"DUMP\n"))
|
|
if wrote > 0:
|
|
break
|
|
time.sleep(0.2)
|
|
if wrote <= 0:
|
|
raise SystemExit("Failed to send DUMP command on RTT down channel 0")
|
|
print(f"Sent DUMP ({wrote} bytes). Capturing screen...")
|
|
|
|
buffer = b""
|
|
deadline = time.time() + timeout_s
|
|
|
|
while time.time() < deadline:
|
|
term = jlink.rtt_read(0, 1024)
|
|
if term:
|
|
print("RTT0:", bytes(term).decode("utf-8", errors="replace").strip())
|
|
chunk = jlink.rtt_read(1, 8192)
|
|
if chunk:
|
|
buffer += bytes(chunk)
|
|
if len(buffer) >= HEADER_SIZE and buffer[:4] == MAGIC:
|
|
break
|
|
time.sleep(0.01)
|
|
|
|
if len(buffer) < HEADER_SIZE or buffer[:4] != MAGIC:
|
|
raise SystemExit("Timeout waiting for SCRN header on RTT channel 1")
|
|
|
|
_, width, height, fmt, _ = struct.unpack_from(HEADER_FMT, buffer, 0)
|
|
if fmt != 1:
|
|
raise SystemExit(f"Unsupported pixel format: {fmt}")
|
|
|
|
pixel_bytes = width * height * 2
|
|
buffer = buffer[HEADER_SIZE:]
|
|
print(f"Receiving {width}x{height} RGB565 ({pixel_bytes} bytes)...")
|
|
|
|
while len(buffer) < pixel_bytes and time.time() < deadline:
|
|
chunk = jlink.rtt_read(1, 16384)
|
|
if chunk:
|
|
buffer += bytes(chunk)
|
|
else:
|
|
time.sleep(0.001)
|
|
|
|
if len(buffer) < pixel_bytes:
|
|
raise SystemExit(f"Incomplete dump: got {len(buffer)} / {pixel_bytes} bytes")
|
|
|
|
image = Image.new("RGB", (width, height))
|
|
pixels = image.load()
|
|
offset = 0
|
|
for y in range(height):
|
|
for x in range(width):
|
|
hi = buffer[offset]
|
|
lo = buffer[offset + 1]
|
|
offset += 2
|
|
pixels[x, y] = rgb565_bytes_to_rgb(hi, lo)
|
|
|
|
image.save(out_path)
|
|
print(f"Saved {out_path}")
|
|
finally:
|
|
try:
|
|
jlink.rtt_stop()
|
|
except pylink.errors.JLinkException:
|
|
pass
|
|
jlink.close()
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Capture K1 LCD via J-Link RTT")
|
|
parser.add_argument("--out", default="screen.png", help="Output PNG path")
|
|
parser.add_argument("--device", default="AT32F403AC", help="J-Link device name")
|
|
parser.add_argument("--timeout", type=float, default=60.0, help="Seconds to wait")
|
|
parser.add_argument("--open", action="store_true", help="Open PNG after capture")
|
|
parser.add_argument(
|
|
"--reset",
|
|
action="store_true",
|
|
help="Reset MCU before capture (default: keep current UI)",
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
import_deps()
|
|
capture_rtt_png(args.out, args.device, args.timeout, do_reset=args.reset)
|
|
|
|
if args.open and os.name == "nt":
|
|
os.startfile(os.path.abspath(args.out))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|