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

249 lines
11 KiB
Python

#!/usr/bin/env python3
# testAdopt.py - does the driver really use the line settings the user's hardware is already
# configured for, and does the 38400 hot key work again?
#
# python3 testAdopt.py <disk.d64> [tag]
#
# Two questions, one boot, one machine (the far end is a TCP sink that never answers, which is enough
# for everything measured here - none of it needs a peer).
#
# 1. ADOPT MODE. baudEntryTable's last entry, index 21, has no control byte of its own: aciaProbeAdopted
# reads the 6551's control register before the page probe overwrites it and parks the result there,
# corrected only where it cannot work. So the test writes a control byte into $DE03 with the
# monitor, exactly as a C64 Ultimate or a previously-configured cartridge would leave it, and then
# makes the driver open the link and reads back what it chose.
# * the first case is the real cold-start path: the byte goes in before the A/O prompt is
# answered, i.e. before anything in the module has touched the chip;
# * the rest re-run the same code by clearing aciaPageLatch $E5BF - which is the only thing that
# stops aciaDetect probing twice - and pressing C=+U, so one boot covers the whole table of
# cases. Clearing that latch is a test manipulation and is called out as one.
#
# 2. THE 38400 HOT KEY. C=+8 must put baudIndex $E055 = 18 and the control register = $1F again, and
# C=+U must take the driver back out of the fixed rate and into adopt mode.
import os
import re
import subprocess
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 testModemSelect import SerialSink
from testTwoMachines import pickModemOpponent, answerModemPrompts
from testHighRates import moduleResident
LOGS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "testLogs")
SHOTS = os.path.join(os.path.dirname(os.path.abspath(__file__)), "shots")
CONTROL = ACIA_BASE + 3
BAUD_INDEX = 0xE055 # the three-byte index into baudEntryTable
LIVE_CONTROL = 0xE056 # bitPeriodLo: the control byte loadBaudParameters last wrote
ADOPT_SLOT = 0xE726 # baudEntryTable ($E710) + 22, the adopt entry's control byte
PAGE_LATCH = 0xE5BF
# what the port is set to what the driver must end up using, and why
CASES = [
(0x18, 0x18, "2400 8N1 - usable as it stands, so it is copied through untouched"),
(0x9F, 0x9F, "38400 with two stop bits - stop bits are free, so they survive"),
(0x5A, 0x1A, "4800 but six data bits - the word length is corrected, the rate is not"),
(0x3C, 0x1C, "9600 but seven data bits - same correction"),
(0x00, 0x15, "a cold, unconfigured chip - $00 selects external clocks, so the 300-baud fallback"),
(0x0C, 0x15, "9600 but bit 4 clear = external receive clock - unusable, so the fallback again"),
(0x10, 0x15, "internal clock but no rate at all - unusable, so the fallback again"),
]
def pause(session):
session.enterMonitor()
def resume(session):
session.sock.sendall(b"x\n")
session.recv(2)
def keyDown(session, keysym):
subprocess.run(["xdotool", "keydown", keysym], env=session.env, check=False)
def keyUp(session, keysym):
subprocess.run(["xdotool", "keyup", keysym], env=session.env, check=False)
def holdCombo(session, key, ms=900):
keyDown(session, "Tab") # the Commodore key under VICE's default symbolic keymap
time.sleep(0.2)
keyDown(session, key)
time.sleep(ms / 1000.0)
keyUp(session, key)
time.sleep(0.1)
keyUp(session, "Tab")
time.sleep(0.4)
def pressUntil(session, key, addr, want, tries=6):
# The hot-key layer ignores a repeat of the code it saw last ($E312 cmp lastHotkeyCode), so the
# same combination pressed twice running does nothing the second time. Every press here is
# therefore checked against the byte it is supposed to move, and repeated until it lands.
for attempt in range(tries):
# Re-assert the input focus first: there is no window manager on the private display, and a
# monitor stop between presses is enough to leave the keys going nowhere.
session.focus()
holdCombo(session, key)
time.sleep(0.4)
pause(session)
got = readByte(session, addr)
last = readByte(session, 0xE046)
resume(session)
if got == want:
return attempt + 1
print(f"[press] C=+{key} did not land (want ${want:02X}, got "
f"{'None' if got is None else '$%02X' % got}, lastHotkeyCode=${last:02X}) - again",
flush=True)
time.sleep(0.6)
return 0
def state(session):
pause(session)
out = {
"control": readByte(session, CONTROL),
"baudIndex": readByte(session, BAUD_INDEX),
"liveControl": readByte(session, LIVE_CONTROL),
"adoptSlot": readByte(session, ADOPT_SLOT),
"pageLatch": readByte(session, PAGE_LATCH),
"commandShadow": readByte(session, 0xE5BE),
"nmiState": readByte(session, 0xE5BD),
}
resume(session)
return out
def show(tag, s):
print(f"[{tag}] $DE03={s['control']:02X} baudIndex={s['baudIndex']:02X} "
f"live={s['liveControl']:02X} adoptSlot={s['adoptSlot']:02X} "
f"latch={s['pageLatch']:02X} cmd={s['commandShadow']:02X} nmiState={s['nmiState']:02X}",
flush=True)
def main():
disk = os.path.abspath(sys.argv[1])
tag = sys.argv[2] if len(sys.argv) > 2 else "adopt"
os.makedirs(LOGS, exist_ok=True)
os.makedirs(SHOTS, exist_ok=True)
sink = SerialSink()
session = ViceSession(disk, os.path.join(LOGS, f"vice.{tag}.log"),
aciaArgs(rsDevAddress=f"127.0.0.1:{sink.port}"), label=tag, warp=True)
results = []
try:
session.connect()
session.mon("warp on") # -warp on the command line does not survive the autostart
session.bootPastLoader()
session.findWindow()
session.focus()
# Out of warp before any key is pressed: a joystick press held for 250 ms of wall clock is a
# different number of emulated frames at warp speed than it is at C64 speed, and the options
# menu scrolls one row per frame, so menu keys under warp land wherever they like.
pause(session)
session.mon("warp off")
resume(session)
pickModemOpponent(session, tag, "a")
# Nothing below means anything until the opponent module is actually resident: before that,
# $E055 and $E5BD are whatever the trainer or the loader left in those bytes.
for attempt in range(30):
time.sleep(2)
if moduleResident(session, tag):
break
else:
raise SystemExit(f"{tag}: the opponent module never loaded")
# --- the module is resident and has not touched the chip yet ---
pause(session)
session.mon("warp off")
before = {
"baudIndex": readByte(session, BAUD_INDEX),
"adoptSlot": readByte(session, ADOPT_SLOT),
"latch": readByte(session, PAGE_LATCH),
"control": readByte(session, CONTROL),
}
print(f"[{tag}] at the A/O prompt: baudIndex=${before['baudIndex']:02X} "
f"adoptSlot=${before['adoptSlot']:02X} latch=${before['latch']:02X} "
f"$DE03=${before['control']:02X}", flush=True)
# set the port up the way a user's hardware would already be set up
session.mon(f"> {CONTROL:04x} 18")
resume(session)
results.append(("disk default baudIndex", before["baudIndex"], 0x15))
results.append(("adopt slot before the probe", before["adoptSlot"], 0x00))
results.append(("page latch before the probe", before["latch"], 0x00))
# --- the real cold-start path: open the link with $DE03 already set to 2400 ---
opened = answerModemPrompts(session, tag, "a")
cold = state(session)
show(tag + "/cold", cold)
results.append(("link opened", 1 if opened else 0, 1))
results.append(("cold start adopted $18 into the slot", cold["adoptSlot"], 0x18))
results.append(("cold start left the chip at $18", cold["control"], 0x18))
results.append(("cold start used index 21", cold["baudIndex"], 0x15))
session.shot(os.path.join(SHOTS, f"{tag}Cold.png"))
# --- the table of cases, each one a fresh probe ---
# Each case is two hot keys, and the pair is the point: C=+3 takes the driver out of adopt
# mode and on to a fixed 300 baud (and, with the latch cleared, re-runs the probe, which is
# what re-reads the preset), then C=+U puts it back on the port's own settings. Pressing two
# different combinations also keeps the hot-key layer's repeat filter out of the way.
for preset, expect, why in CASES:
pause(session)
session.mon(f"> {CONTROL:04x} {preset:02x}")
session.mon(f"> {PAGE_LATCH:04x} 00") # test manipulation: let aciaDetect probe again
resume(session)
landed3 = pressUntil(session, "3", BAUD_INDEX, 0x00)
afterFixed = state(session)
landedU = pressUntil(session, "u", BAUD_INDEX, 0x15)
s = state(session)
print(f"[{tag}] preset ${preset:02X} -> slot=${s['adoptSlot']:02X} "
f"$DE03=${s['control']:02X} ({why})", flush=True)
results.append((f"preset ${preset:02X}: C=+3 left adopt mode", afterFixed["baudIndex"], 0x00))
results.append((f"preset ${preset:02X}: C=+3 imposed 300 baud", afterFixed["control"], 0x15))
results.append((f"preset ${preset:02X}: the probe adopted ${expect:02X}",
afterFixed["adoptSlot"], expect))
results.append((f"preset ${preset:02X}: C=+U selected adopt", s["baudIndex"], 0x15))
results.append((f"preset ${preset:02X}: C=+U programmed ${expect:02X}", s["control"], expect))
# --- the 38400 hot key, and coming back out of it ---
pressUntil(session, "8", BAUD_INDEX, 0x12)
s = state(session)
show(tag + "/C=+8", s)
results.append(("C=+8 selects index 18", s["baudIndex"], 0x12))
results.append(("C=+8 writes control $1F", s["control"], 0x1F))
session.shot(os.path.join(SHOTS, f"{tag}Rate38400.png"))
pause(session)
session.mon(f"> {CONTROL:04x} 17") # pretend the port is set to 1200
session.mon(f"> {PAGE_LATCH:04x} 00")
resume(session)
pressUntil(session, "u", BAUD_INDEX, 0x15)
s = state(session)
show(tag + "/C=+U", s)
results.append(("C=+U leaves the fixed rate", s["baudIndex"], 0x15))
results.append(("C=+U adopts $17", s["control"], 0x17))
session.shot(os.path.join(SHOTS, f"{tag}Adopt.png"))
finally:
try:
session.shot(os.path.join(SHOTS, f"{tag}End.png"))
except Exception:
pass
session.close()
sink.stop()
print("\n=== results ===", flush=True)
bad = 0
for name, got, want in results:
ok = got == want
bad += 0 if ok else 1
gotText = "None" if got is None else f"${got:02X}"
print(f" {'PASS' if ok else 'FAIL'} {name}: got {gotText}, want ${want:02X}", flush=True)
print(f"\n{len(results) - bad} of {len(results)} checks passed", flush=True)
return 1 if bad else 0
sys.exit(main())