abse/gameboy-advance
A tscircuit package created by abse.
- Version
- 0.0.45
- License
- unset
- Stars
- 0
firmware/game-console/tools/console.py
#!/usr/bin/env python3
"""USB maintenance for the native GBA console. Requires pyserial; Pillow for shots."""
import argparse
import pathlib
import struct
import time
import zlib
import serial
import serial.tools.list_ports
def connect(port):
if not port:
devices = [p.device for p in serial.tools.list_ports.comports()
if p.vid == 0x2E8A and p.serial_number == "725C4F2D480112C0"]
if len(devices) != 1:
raise SystemExit("Expected one GBA USB device; specify --port")
port = devices[0]
return serial.Serial(port, 115200, timeout=5, write_timeout=10)
def line_until(s, prefixes):
deadline = time.monotonic() + 30
while time.monotonic() < deadline:
line = s.readline().decode(errors="replace").strip()
if line.startswith("ERROR"):
raise RuntimeError(line)
if line.startswith(prefixes):
return line
if line:
print(line, flush=True)
raise TimeoutError("No response from board")
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--port")
sub = p.add_subparsers(dest="command", required=True)
for name in ("status", "launch", "menu", "retry", "colors", "resume", "assets", "lcdregs"):
sub.add_parser(name)
rate = sub.add_parser("rate")
rate.add_argument("hz", type=int)
ls = sub.add_parser("ls")
ls.add_argument("path", default="/games", nargs="?")
put = sub.add_parser("put")
put.add_argument("local", type=pathlib.Path)
put.add_argument("remote")
key = sub.add_parser("key")
key.add_argument("mask", type=lambda x: int(x, 0))
key.add_argument("ms", type=int, default=200, nargs="?")
shot = sub.add_parser("shot")
shot.add_argument("file", type=pathlib.Path)
args = p.parse_args()
with connect(args.port) as s:
s.reset_input_buffer()
if args.command == "put":
data = args.local.read_bytes()
if not args.remote.startswith("/games/") or " " in args.remote:
raise SystemExit("Remote path must be under /games and have no spaces")
crc = zlib.crc32(data)
s.write(f"put {args.remote} {len(data)} {crc:08x}\n".encode())
print(line_until(s, ("READY ",)), flush=True)
for off in range(0, len(data), 4096):
chunk = data[off:off+4096]
s.write(chunk)
reply = line_until(s, ("ACK ",))
if reply != f"ACK {off + len(chunk)}":
raise RuntimeError(reply)
if off % (4096 * 64) == 0:
print(f"Uploaded {off + len(chunk)} / {len(data)}", flush=True)
reply = line_until(s, ("DONE ",))
if reply != f"DONE 0 {len(data)} {crc:08x}":
raise RuntimeError(reply)
print(reply)
elif args.command == "shot":
from PIL import Image
s.write(b"shot\n")
header = line_until(s, ("FRAME ",))
_, width, height, stride = header.split()
width, height, stride = int(width), int(height), int(stride)
expected = width * height * stride
raw = bytearray()
while len(raw) < expected:
part = s.read(expected - len(raw))
if not part:
raise TimeoutError("Incomplete frame")
raw.extend(part)
Image.frombytes("RGBA", (width, height), bytes(raw), "raw", "BGRA").convert("RGB").save(args.file)
print(f"Saved {args.file}")
else:
command = args.command
if command == "resume": command = "r"
if command == "rate": command += f" {args.hz}"
if command == "ls": command += f" {args.path}"
if command == "key": command += f" {args.mask:x} {args.ms}"
s.write((command + "\n").encode())
for _ in range(40 if args.command == "ls" else 5):
reply = s.readline().decode(errors="replace").strip()
if reply: print(reply, flush=True)
if reply == "END" or (args.command == "key" and reply.startswith("KEY ")): break
if __name__ == "__main__":
main()