| 961b3c61 Andy McCall |
1 |
|
| 2 |
|
| 3 |
|
| 4 |
|
| 5 |
|
| 6 |
|
| 7 |
|
| 8 |
|
| 9 |
|
| 10 |
|
| 11 |
|
| 12 |
|
| 13 |
|
| 14 |
|
| 15 |
|
| 16 |
|
| 17 |
from __future__ import annotations |
| 18 |
|
| 19 |
import io |
| 20 |
import json |
| 21 |
import time |
| 22 |
import urllib.error |
| 23 |
import urllib.request |
| 24 |
|
| 25 |
CONTRACT_MAJOR = 0 |
| 26 |
|
| 27 |
|
| 28 |
class EmuControlError(RuntimeError): |
| 29 |
pass |
| 30 |
|
| 31 |
|
| 32 |
class EmuControl: |
| 33 |
def __init__(self, port: int, host: str = "127.0.0.1", timeout: float = 5.0): |
| 34 |
self.base = f"http://{host}:{port}" |
| 35 |
self.timeout = timeout |
| 36 |
|
| 37 |
|
| 38 |
def _get(self, path: str) -> tuple[bytes, str]: |
| 39 |
try: |
| 40 |
with urllib.request.urlopen(self.base + path, timeout=self.timeout) as r: |
| 41 |
return r.read(), r.headers.get_content_type() |
| 42 |
except urllib.error.HTTPError as e: |
| 43 |
raise EmuControlError(f"GET {path} -> {e.code}: {e.read()!r}") from e |
| 44 |
|
| 45 |
def _post(self, path: str) -> bytes: |
| 46 |
req = urllib.request.Request(self.base + path, method="POST", data=b"") |
| 47 |
try: |
| 48 |
with urllib.request.urlopen(req, timeout=self.timeout) as r: |
| 49 |
return r.read() |
| 50 |
except urllib.error.HTTPError as e: |
| 51 |
raise EmuControlError(f"POST {path} -> {e.code}: {e.read()!r}") from e |
| 52 |
|
| 53 |
|
| 54 |
def status(self) -> dict: |
| 55 |
body, _ = self._get("/status") |
| 56 |
st = json.loads(body) |
| 57 |
major = int(str(st.get("contract", "0")).split(".")[0]) |
| 58 |
if major != CONTRACT_MAJOR: |
| 59 |
raise EmuControlError( |
| 60 |
f"contract major mismatch: emulator {st.get('contract')} " |
| 61 |
f"vs client {CONTRACT_MAJOR}.x" |
| 62 |
) |
| 63 |
return st |
| 64 |
|
| 65 |
def frame(self) -> int: |
| 66 |
return int(self.status()["frame"]) |
| 67 |
|
| 68 |
def regs(self) -> dict: |
| 69 |
body, _ = self._get("/regs") |
| 70 |
return json.loads(body) |
| 71 |
|
| 72 |
def mem(self, addr: int, length: int, bank: int | None = None) -> bytes: |
| 73 |
path = f"/mem?addr={addr}&len={length}" |
| 74 |
if bank is not None: |
| 75 |
path += f"&bank={bank}" |
| 76 |
body, _ = self._get(path) |
| 77 |
return body |
| 78 |
|
| 79 |
def step(self, frames: int = 1) -> int: |
| 80 |
body = self._post(f"/step?frames={frames}") |
| 81 |
return int(json.loads(body)["frame"]) |
| 82 |
|
| 83 |
def pause(self) -> None: |
| 84 |
self._post("/pause") |
| 85 |
|
| 86 |
def resume(self) -> None: |
| 87 |
self._post("/resume") |
| 88 |
|
| 89 |
def screenshot(self): |
| 90 |
|
| 91 |
body, ctype = self._get("/screenshot") |
| 92 |
try: |
| 93 |
from PIL import Image |
| 94 |
except ImportError as e: |
| 95 |
raise EmuControlError("screenshot() needs Pillow (pip install pillow)") from e |
| 96 |
return Image.open(io.BytesIO(body)) |
| 97 |
|
| 98 |
|
| 99 |
def wait_ready(self, tries: int = 100, delay: float = 0.05) -> dict: |
| 100 |
|
| 101 |
last = None |
| 102 |
for _ in range(tries): |
| 103 |
try: |
| 104 |
return self.status() |
| 105 |
except (EmuControlError, urllib.error.URLError, ConnectionError) as e: |
| 106 |
last = e |
| 107 |
time.sleep(delay) |
| 108 |
raise EmuControlError(f"emulator control port never came up: {last}") |
| 109 |
|
| 110 |
def run_to_frame(self, target: int) -> int: |
| 111 |
|
| 112 |
cur = self.frame() |
| 113 |
if target > cur: |
| 114 |
self.step(frames=target - cur) |
| 115 |
return self.frame() |