164 lines
7.1 KiB
Python
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()
|