gs2debug — Python API for agents¶
How to drive a running GSSquared from Python. Prefer this package over hand-rolled sockets.
Wire format (if needed): DebugProtocol.md. Design notes: DebugClient.md.
Setup¶
# Terminal A — emulator (IIe example: -p 2)
./build/GSSquared --debug /tmp/gs2.sock -p 2
# Terminal B — always a separate terminal (Ctrl-C on the script must not SIGINT the emu)
PYTHONPATH=clients/python/src python3 your_script.py /tmp/gs2.sock
On Windows+, pass a filesystem socket path the same way (--debug C:\Temp\gs2.sock). AF_UNIX is supported; do not use a TCP port.
-p N |
Platform |
|---|---|
| 0 | Apple II |
| 1 | Apple II Plus |
| 2 | Apple IIe |
| 3 | Apple IIe Enhanced |
| 4 | Apple IIe 65816 |
| 5 | Apple IIgs |
Same integers as get_status().platform_id / PLATFORM_* constants. Use a distinct socket path per instance if several are running.
from gs2debug import Client, MEM_MAIN, PLATFORM_APPLE_IIE, ProtocolError
with Client() as c:
c.connect("/tmp/gs2.sock")
c.hello() # required before anything else
status = c.get_status()
assert status.platform_id == PLATFORM_APPLE_IIE
# ... use API ...
Import path: clients/python/src on PYTHONPATH, or pip install -e clients/python.
API (what to call)¶
| Method | Purpose |
|---|---|
connect(path) / close() |
AF_UNIX connect; also context-manager |
hello() → HelloInfo |
Handshake; sets version, flags, max_payload |
ping() |
Liveness; empty reply |
quit() |
Force-quit emu (skips QuitModal); prefer over kill/SIGTERM |
get_status() → StatusInfo |
.execution_mode (0=NORMAL, 1=STEP_INTO, 2=PAUSED), .platform_id (PlatformId_t / -p N) |
reset(cold_start=False) |
computer_t::reset(cold_start) on main thread |
pause() / continue_() |
Run-control; emits EVT_STOPPED / EVT_RUN_STATE |
step_into(count=1) |
Arm N-instruction step; empty reply; EVT_STOPPED (STOP_STEP) + trace when done |
get_trace(ago=0, count=100) → TraceWindow |
Instruction ring window; .available, .entries (40-byte blobs, oldest→newest) |
get_regs() → bytes |
Live 40-byte CPU snapshot (same layout as stop/trace) |
set_regs(mask, *, pc=…, a=…, …) |
Masked register write (REG_PC, REG_A, …) |
find_mem(domain, address, length, pattern, *, mask=None, max_hits=16) → list[int] |
Pattern search; optional wildcard mask |
video_text(page=CURRENT, mode=CURRENT) → VideoText |
Linearized text page; .page/.mode resolved; .as_lines() |
mount(slot, unit, path) → int |
Mount media (0-based unit); prefer absolute path; returns MEDIA_* |
unmount(slot, unit) → int |
Unmount discard; returns MEDIA_* |
state_get(device_id) → bytes |
Device snapshot blob (DEVICE_ID_ENSONIQ, …) |
bp_set(...) → id |
Create EXEC/DATA/IO breakpoint (see DebugProtocol.md) |
bp_clear(id) / bp_clear_all() / bp_enable(id, enabled) / bp_list() |
Breakpoint table |
wait_event() / wait_stopped() |
Block for unsolicited EVENT / parse EVT_STOPPED |
read_mem(domain, address, length) → bytes |
Peek (MEM_MAIN / MEM_MAIN_RAW; MEM_MEGAII / MEM_MEGAII_RAW on IIgs) |
write_mem(domain, address, data) |
Poke (same domains); data non-empty |
find_mem(...) |
See above; same domains as read_mem |
key_event(down, scancode, mod=0) |
One SDL key down/up |
key_down / key_up |
Same, for modifiers |
tap_key(scancode, mod=0, hold_s=0.02) |
Down, optional hold, then up |
type_text(text, delay_s=0.05, hold_s=0.02) |
ASCII US layout KEYEVENT taps; \n → Return; hold_s between down/up; delay_s after each char (3× after Return) |
paste_text(text) |
Fill IIe/IIgs paste buffer (PASTE_TEXT); one round-trip; guest paces drain. Prefer for long strings |
request(type, payload) |
Raw framed call; raises ProtocolError on ERROR |
on_event(handler) |
Optional handler(event_id, seq, data) for EVENT frames |
Rules: call hello() first; one outstanding request at a time; never reimplement framing in agent scripts.
Constants¶
From gs2debug / gs2debug.keys / gs2debug.types:
| Name | Value / meaning |
|---|---|
MEM_MAIN |
0 — CPU MMU view (cpu->mmu->read / write) |
MEM_MEGAII |
1 — Mega II / IIe-view MMU (computer->mmu); IIgs only |
MEM_ENSONIQ |
2 — DOC RAM (IIgs only); address = DOC offset |
MEM_ADBMICRO |
Reserved (ERROR unsupported domain) |
DEVICE_ID_DISK_II |
9 — for state_get (60-byte v1 blob; see DebugProtocol.md) |
DEVICE_ID_ENSONIQ |
22 — for state_get (784-byte v1 blob; see DebugProtocol.md) |
STATE_GET |
0x00000601 |
MEM_MAIN_RAW |
4 — physical cpu->mmu->get_memory_base()[addr] (no MMU/bus) |
MEM_MEGAII_RAW |
5 — physical Mega II buffer; IIgs only |
PLATFORM_APPLE_II … PLATFORM_APPLE_IIGS |
0…5 — same as -p / StatusInfo.platform_id |
SCANCODE_LCTRL, SCANCODE_F12, SCANCODE_RETURN, … |
SDL3 scancodes (see keys.py) |
KMOD_LCTRL, KMOD_CTRL, KMOD_LSHIFT, … |
SDL keymod bits for the event’s mod field |
ProtocolError.code / .message |
E_* from protocol (E_BAD_LENGTH=2, E_INTERNAL=6, …) |
Addresses are linear uint32. IIe / Mega II text page 1 row 0: 0x0400 (MEM_MAIN on II/IIe, or MEM_MEGAII on IIgs). IIgs bank $E0 text via FPI: MEM_MAIN at 0xE00400. MEM_*_RAW uses buffer offsets (IIgs FPI: banks 0–127 in 8 MB; Mega II: 128 KB including aux at +0x10000) — not CPU $E0xxxx addresses.
Recipes¶
Check platform¶
status = c.get_status()
print(status.execution_mode, status.platform_id)
Peek / poke text row¶
row = c.read_mem(MEM_MAIN, 0x0400, 0x28) # II / IIe
c.write_mem(MEM_MAIN, 0x0400, os.urandom(0x28))
assert c.read_mem(MEM_MAIN, 0x0400, 0x28) == data # verify
# IIgs FPI (MAIN): use 0xE00400
# IIgs Mega II: c.read_mem(MEM_MEGAII, 0x0400, 0x28)
# Physical buffers: c.read_mem(MEM_MAIN_RAW, 0x0400, 0x28)
# c.read_mem(MEM_MEGAII_RAW, 0x0400, 0x28) # IIgs
Live registers / pattern search¶
from gs2debug import REG_A
import struct
c.pause()
regs = c.get_regs() # 40-byte system_trace_entry_t
a = struct.unpack_from("<H", regs, 18)[0]
c.set_regs(REG_A, a=0x1234)
assert struct.unpack_from("<H", c.get_regs(), 18)[0] == 0x1234
sig = b"GS2FIND"
c.write_mem(MEM_MAIN, 0x300, sig)
hits = c.find_mem(MEM_MAIN, 0x0000, 0x1000, sig)
assert 0x300 in hits
# Wildcard: match GS2???? with mask clearing last 4 bytes
hits = c.find_mem(MEM_MAIN, 0x0000, 0x1000, b"GS2XXXX", mask=b"\xff\xff\xff\x00\x00\x00\x00")
Text screen snapshot¶
from gs2debug import VIDEO_MODE_TEXT40, VIDEO_MODE_TEXT80
vt = c.video_text() # CURRENT page + mode
print(vt.mode, vt.page, vt.flags) # resolved TEXT40/TEXT80, never CURRENT
print("\n".join(vt.as_lines())) # ASCII-ish rows (bit7 cleared)
vt40 = c.video_text(1, VIDEO_MODE_TEXT40) # force page 1 / 40-col
assert vt40.cols == 40 and len(vt40.chars) == 960
Mount / unmount media¶
Protocol unit is 0-based (CLI -ds6d1= → mount(6, 0, path)).
from pathlib import Path
from gs2debug import MEDIA_OK
path = str(Path("disk_images/SPFBase.dsk").resolve()) # absolute
assert c.mount(6, 0, path) == MEDIA_OK
assert c.unmount(6, 0) == MEDIA_OK
Machine reset¶
Prefer the protocol command (not keyboard):
c.reset(cold_start=False) # warm
c.reset(cold_start=True) # cold (clears $3F2–$3F4)
Keyboard Control+Reset is still useful for testing KEYEVENT. Handlers check Ctrl on the F12 event (macOS/Windows Reset key), not only a prior Ctrl-down:
from gs2debug import SCANCODE_LCTRL, SCANCODE_F12, KMOD_LCTRL, KMOD_CTRL
import time
c.key_down(SCANCODE_LCTRL, KMOD_LCTRL)
c.key_down(SCANCODE_F12, KMOD_CTRL) # mod must include CTRL
time.sleep(1)
c.key_up(SCANCODE_F12, KMOD_CTRL)
c.key_up(SCANCODE_LCTRL, 0)
Type BASIC / arbitrary text¶
Prefer paste_text for programs and long strings (one round-trip; the guest meters the buffer). Use type_text for Control-Reset, Open-Apple, and other keys that are not pasteable ASCII.
c.paste_text('10 PRINT "HI"\nRUN\n')
# c.type_text('10 PRINT "HI"\nRUN\n', delay_s=0.1) # KEYEVENT taps; prefer ≥0.1 so line numbers are not dropped
Or shell helper (stdin / --text / --file; does not embed a program). --paste uses PASTE_TEXT; default is KEYEVENT:
PYTHONPATH=clients/python/src python3 clients/python/examples/type_to_emu.py /tmp/gs2.sock \
--paste --reset --wait 2 <<'EOF'
NEW
10 GR
20 END
RUN
EOF
--reset calls protocol reset(cold_start=False).
Applesoft graphics (don’t confuse modes)¶
| Mode | Command | Lines |
|---|---|---|
Lo-res GR |
COLOR= 0–15 |
HLIN / VLIN (coords ~40×48) |
Hi-res HGR |
HCOLOR= 0–7 |
HPLOT x1,y TO x2,y (280×192) — not HLIN |
IIe-only soft-switch status reads ($C01A TEXT, $C01D HIRES, …) are invalid on II / II Plus — use -p 2 (or Enhanced) if you need those.
Gotchas¶
- Separate terminal for the emulator — Ctrl-C in the same shell/process group can kill GSSquared. Prefer
c.quit()(or start with--no-quit-confirmif the harness must SIGTERM). - Typing too fast drops keys (e.g. line
60becomes line0). Preferpaste_textfor long strings. If you must usetype_text, usedelay_s≥0.1and rely onhold_s; Return already gets a longer pause. - Shifted glyphs (
", uppercase, …): library holds Shift for that tap; do not put Control/Alt intype_text— usekey_down/key_up. - MAIN / MAIN_RAW / MEGAII / MEGAII_RAW are implemented; ENSONIQ / ADBMICRO are not.
- IIgs MAIN is the FPI/banked CPU MMU; MEGAII is
computer->mmu.*_RAWindexesget_memory_base()(buffer offsets, not$E0xxxx). Non-GS + MEGAII/_RAW → ERRORMEGAII only on Apple IIgs. - Disconnecting the client does not quit the emulator; it accepts a new client.
Example scripts¶
| Script | Role |
|---|---|
examples/hello_ping.py |
HELLO / GET_STATUS (mode + platform) / PING |
examples/read_text40.py |
Loop-read II/IIe $0400 |
examples/read_text40_iigs.py |
Loop-read IIgs $E0/0400 via MAIN |
examples/read_text40_megaii.py |
Loop-read IIgs $0400 via MEGAII |
examples/write_text40.py |
Random write + readback +/- (II/IIe) |
examples/write_text40_iigs.py |
Same for $E0/0400 |
examples/type_basic_iie.py |
Boot wait, protocol RESET, demo Applesoft |
examples/type_to_emu.py |
Generic typer tool for agents (--paste for PASTE_TEXT) |
examples/test_breakpoints.py |
PAUSE/CONTINUE + EXEC/IO breakpoints (IIe Enhanced -p 3 and IIgs -p 5) |
examples/test_regs_findmem.py |
GET_REGS / SET_REGS / FINDMEM smoke (IIe Enhanced -p 3) |
examples/test_video_text.py |
VIDEO_TEXT CURRENT + TEXT40/TEXT80 (IIe Enhanced -p 3) |
examples/test_media_mount.py |
MOUNT / UNMOUNT Disk II slot 6 unit 0 (IIe Enhanced -p 3) |
All under clients/python/. Each file’s header has concrete Usage lines.
Breakpoints (IIe Enhanced and IIgs)¶
# Terminal A — IIe Enhanced
./build/GSSquared --debug /tmp/gs2-iie.sock -p 3
# Terminal B
PYTHONPATH=clients/python/src python3 clients/python/examples/test_breakpoints.py /tmp/gs2-iie.sock 3
# Terminal A — IIgs
./build/GSSquared --debug /tmp/gs2-gs.sock -p 5
# Terminal B
PYTHONPATH=clients/python/src python3 clients/python/examples/test_breakpoints.py /tmp/gs2-gs.sock 5
from gs2debug import Client, BP_KIND_EXEC, BP_FLAG_ENABLED
with Client() as c:
c.connect("/tmp/gs2.sock")
c.hello()
bp = c.bp_set(kind=BP_KIND_EXEC, address=0xFA62, flags=BP_FLAG_ENABLED)
c.continue_()
hit = c.wait_stopped()
assert hit.bp_id == bp
c.step_into(1)
stepped = c.wait_stopped()
assert stepped.reason == 4 # STOP_STEP
assert len(stepped.trace) == 40
c.continue_() # Policy A steps off if still on EXEC bp