modemwars/swiftlink/testStrapPair.py
2026-08-23 02:09:40 -05:00

164 lines
7.1 KiB
Python

#!/usr/bin/env python3
# testStrapPair.py - two machines whose SwiftLinks are both strapped to $DF00, cross connected through
# the socket null modem, with a tripwire on the register mirrors for the whole session.
#
# python3 testStrapPair.py <disk.d64> [seconds]
#
# testRealtime.py answers "does the link still come up and carry ARQ frames at $DF00" at true C64
# speed. This script answers the other half of the strap question: over a long session that includes
# the module load, the Hayes dialogue, the byte sync, the packet phase and the setup-overlay disk
# loads that happen while the link is live, does anything in the game reach into the cartridge page
# other than the driver's own six accessors?
#
# A watchpoint on $DE04-$DEFF and $DF04-$DFFF is the test. A real SwiftLink decodes only A0 and A1
# inside its page, so those ranges are all mirrors of the same four registers; VICE maps only the
# first four bytes, which makes a stop anywhere in them harmless in the emulator and fatal on the
# hardware. The stock driver's 'sta $DF59,y' - whose dummy read lands on $DF58, the data register -
# is exactly the instruction this is looking for.
import os
import sys
import time
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from viceHarness import ViceSession, aciaArgs, readByte, ACIA_BASE, SCRATCH
from testTwoMachines import NullModemRelay, pickModemOpponent
from testRealtime import analyseWire
from testStrap import describe, readAccessors, showAccessors
SHOTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shots")
LOGS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testLogs")
def armMirrorWatch(session, tag):
session.enterMonitor()
for kind in ("store", "load"):
session.mon(f"watch {kind} $de04 $deff")
session.mon(f"watch {kind} $df04 $dfff")
session.sock.sendall(b"x\n")
session.recv(2)
print(f"[{tag}] mirror tripwire armed on $DE04-$DEFF and $DF04-$DFFF", flush=True)
def answerPrompts(session, tag, answerKey, tries=24):
# testTwoMachines.answerModemPrompts calls the link open as soon as $E5BE reads non-zero, and
# $E5BE is ordinary RAM until the module has been loaded over it - on one run it held $AC from
# whatever was there before and the script stopped pressing keys at a machine that was still on
# the options menu. Two independent conditions instead: the module's own jump table has to be
# resident at $E000, and the ACIA's control register has to have been programmed.
for attempt in range(tries):
session.hold(answerKey, 250)
session.hold("space", 250)
time.sleep(1.5)
session.enterMonitor()
jump = (readByte(session, 0xE000), readByte(session, 0xE001))
control = readByte(session, ACIA_BASE + 3)
session.sock.sendall(b"x\n")
session.recv(2)
if jump == (0x4C, 0x11) and control:
print(f"[{tag}] link open after {attempt + 1} attempt(s): module resident at $E000, "
f"control register ${control:02X}", flush=True)
return True
print(f"[{tag}] link never opened (jump table {jump}, control {control})", flush=True)
return False
def collect(sessions, seconds):
# Drain both monitor sockets, resuming any emulator a watchpoint stopped. With nothing tripping
# the watchpoints this reads nothing and costs the emulators nothing.
text = {tag: "" for _, tag in sessions}
deadline = time.time() + seconds
while time.time() < deadline:
for session, tag in sessions:
chunk = session.recv(0.5)
if chunk.strip():
text[tag] += chunk
session.sock.sendall(b"x\n")
return text
def state(session, tag):
session.enterMonitor()
values = {"connectionPhase": readByte(session, 0xE040), "isLinkActive": readByte(session, 0xE03B),
"linkErrorCount": readByte(session, 0xE047), "baudIndex": readByte(session, 0xE055),
"txCharActive": readByte(session, 0xE5BB),
"aciaCommandShadow": readByte(session, 0xE5BE),
"control": readByte(session, ACIA_BASE + 3), "command": readByte(session, ACIA_BASE + 2)}
session.sock.sendall(b"x\n")
session.recv(2)
print(f"[{tag}] " + " ".join(f"{k}={'??' if v is None else '$%02X' % v}"
for k, v in values.items()), flush=True)
return values
def main():
disk = os.path.abspath(sys.argv[1])
seconds = float(sys.argv[2]) if len(sys.argv) > 2 else 120.0
os.makedirs(SHOTS, exist_ok=True)
os.makedirs(LOGS, exist_ok=True)
relay = NullModemRelay()
print(f"relay on 127.0.0.1:{relay.port}", flush=True)
print(f"acia base for this run: ${ACIA_BASE:04X}", flush=True)
args = aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", baud=2400)
a = ViceSession(disk, f"{SCRATCH}/pair.a.vice.log", args, label="A")
b = ViceSession(disk, f"{SCRATCH}/pair.b.vice.log", args, label="B")
sessions = [(a, "A"), (b, "B")]
try:
a.connect()
b.connect()
a.bootPastLoader()
b.bootPastLoader()
a.findWindow()
b.findWindow()
a.focus()
b.focus()
time.sleep(8)
for session, tag in sessions:
armMirrorWatch(session, tag)
a.shot(f"{SHOTS}/pairDf0001aMenu.png")
b.shot(f"{SHOTS}/pairDf0001bMenu.png")
pickModemOpponent(a, "A", "a")
pickModemOpponent(b, "B", "o")
answerPrompts(a, "A", "a")
answerPrompts(b, "B", "o")
for session, tag in sessions:
state(session, tag)
a.shot(f"{SHOTS}/pairDf0002aLinkOpen.png")
b.shot(f"{SHOTS}/pairDf0002bLinkOpen.png")
marks = [len(relay.log[0]), len(relay.log[1])]
text = collect(sessions, seconds)
for tag in text:
events = describe(text[tag])
print(f"[{tag}] mirror-range stops during {seconds:.0f} s of play: {len(events)}",
flush=True)
for event in events[:20]:
print(f" [{tag}] MIRROR {event}", flush=True)
if text[tag].strip():
open(f"{LOGS}/pairDf00.{tag}.mirror.txt", "w").write(text[tag])
for i in (0, 1):
chunk = bytes(relay.log[i][marks[i]:])
print(f" conn{i} carried {len(chunk)} bytes during the window; wire "
f"{analyseWire(chunk)}", flush=True)
for session, tag in sessions:
state(session, tag)
session.enterMonitor()
showAccessors(readAccessors(session), f"machine {tag}, end of run")
session.mon("m e540 e56f")
session.sock.sendall(b"x\n")
session.recv(2)
a.shot(f"{SHOTS}/pairDf0003aLater.png")
b.shot(f"{SHOTS}/pairDf0003bLater.png")
finally:
a.close()
b.close()
time.sleep(1)
relay.stop()
for i in (0, 1):
print(f"[relay] conn{i} total {len(relay.log[i])} bytes, "
f"wire {analyseWire(bytes(relay.log[i]))}", flush=True)
print(f"[relay] conn{i} tail {bytes(relay.log[i][-60:])!r}", flush=True)
if __name__ == "__main__":
main()