#!/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 [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()