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

172 lines
8.8 KiB
Python

#!/usr/bin/env python3
# testAdoptPair.py - two machines, true C64 speed: does the driver adopt the line settings the
# hardware was already configured for, and does the link still work afterwards?
#
# python3 testAdoptPair.py <disk.d64> <presetControlHex> [presetCommandHex|-] [seconds] [tag]
#
# testAdopt.py answers the same question on one machine against a serial sink, which is enough to
# read a register but not enough to say the link works. This script pre-programmes BOTH 6551s
# through the monitor at the one moment the value still means anything - after the opponent module is
# resident and before the "PRESS A OR O" prompt is answered, i.e. before anything in the module has
# touched the chip - and then opens a link between them and measures it.
#
# * the control register is read on both machines before the preset is written, after it is
# written, and again after the link is open, so "before and after" is a measurement rather than
# an inference;
# * the command register can be preset too (that is where a 6551 keeps parity), which is how the
# question "is parity adopted as well?" gets an answer instead of an assumption;
# * baudIndex $E055, the live control byte bitPeriodLo $E056 and the adopt entry's own byte
# $E726 are read alongside, because those three say WHY the chip ends up holding what it holds.
#
# Nothing here writes any driver byte. The only monitor writes are to the ACIA's own registers,
# which is what a user's hardware configuration would have left there.
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, answerModemPrompts
from testRealtime import (BITS_PER_BYTE, LOGS, NTSC_HZ, SHOTS, command, decodeControl,
measureWindowOnce, pauseAll, resumeAll, showState, snapshot, waitForPhase)
from testHighRates import moduleResident, perSecond, sampleRings
CONTROL = ACIA_BASE + 3
COMMAND = ACIA_BASE + 2
ADOPT_SLOT = 0xE726 # baudEntryTable $E710 + 22: the adopt entry's control byte
PAGE_LATCH = 0xE5BF
LIVE_CONTROL = 0xE056 # bitPeriodLo: whatever loadBaudParameters last wrote
def registers(session, tag, note):
pauseAll([(session, tag)])
row = {"control": readByte(session, CONTROL), "command": readByte(session, COMMAND),
"baudIndex": readByte(session, 0xE055), "live": readByte(session, LIVE_CONTROL),
"adoptSlot": readByte(session, ADOPT_SLOT), "latch": readByte(session, PAGE_LATCH),
"shadow": readByte(session, 0xE5BE), "nmiState": readByte(session, 0xE5BD)}
resumeAll([(session, tag)])
text = " ".join(f"{name}=" + ("None" if value is None else f"${value:02X}")
for name, value in row.items())
print(f"[{tag}] {note}: {text}", flush=True)
if row["control"] is not None:
print(f"[{tag}] control {decodeControl(row['control'])}", flush=True)
return row
def main():
disk = os.path.abspath(sys.argv[1])
preset = int(sys.argv[2], 16)
presetCommand = None if len(sys.argv) < 4 or sys.argv[3] == "-" else int(sys.argv[3], 16)
seconds = float(sys.argv[4]) if len(sys.argv) > 4 else 75.0
tag = sys.argv[5] if len(sys.argv) > 5 else "ap"
os.makedirs(SHOTS, exist_ok=True)
os.makedirs(LOGS, exist_ok=True)
relay = NullModemRelay()
print(f"relay on 127.0.0.1:{relay.port}; both ports pre-set to control ${preset:02X}"
+ (f", command ${presetCommand:02X}" if presetCommand is not None else ""), flush=True)
args = aciaArgs(rsDevAddress=f"127.0.0.1:{relay.port}", baud=38400)
a = ViceSession(disk, f"{SCRATCH}/ap.{tag}.a.vice.log", args, label="A", warp=False)
b = ViceSession(disk, f"{SCRATCH}/ap.{tag}.b.vice.log", args, label="B", warp=False)
sessions = [(a, "A"), (b, "B")]
rows = {}
try:
a.connect()
b.connect()
for session, sessionTag in sessions:
command(session, "warp on")
a.bootPastLoader(waitSecs=900)
b.bootPastLoader(waitSecs=900)
pauseAll(sessions)
for session, sessionTag in sessions:
command(session, "warp off")
print(f"[{sessionTag}] {command(session, 'warp').strip()}", flush=True)
resumeAll(sessions)
a.findWindow()
b.findWindow()
a.focus()
b.focus()
time.sleep(25)
pickModemOpponent(a, "A", "a")
pickModemOpponent(b, "B", "o")
for session, sessionTag in sessions:
if not moduleResident(session, sessionTag):
raise SystemExit(f"{sessionTag}: the opponent module never loaded")
# ---- the one moment the user's own setting still exists: the module is resident and has
# ---- not touched the chip yet.
for session, sessionTag in sessions:
rows[(sessionTag, "atPrompt")] = registers(session, sessionTag,
"at the A/O prompt, before the preset")
pauseAll([(session, sessionTag)])
command(session, f"> {CONTROL:04x} {preset:02x}")
if presetCommand is not None:
command(session, f"> {COMMAND:04x} {presetCommand:02x}")
resumeAll([(session, sessionTag)])
rows[(sessionTag, "preset")] = registers(session, sessionTag,
"after the preset was written")
a.shot(f"{SHOTS}/ap{tag}01preset.png")
answerModemPrompts(a, "A", "a")
answerModemPrompts(b, "B", "o")
for session, sessionTag in sessions:
rows[(sessionTag, "opened")] = registers(session, sessionTag,
"after the link was opened")
ok, phases = waitForPhase(sessions, 3, 180)
print(f"both in the packet phase: {ok} ({phases})", flush=True)
for session, sessionTag in sessions:
rows[(sessionTag, "phase3")] = registers(session, sessionTag, "in the packet phase")
a.shot(f"{SHOTS}/ap{tag}02linked.png")
b.shot(f"{SHOTS}/ap{tag}02linkedB.png")
run = measureWindowOnce(relay, sessions, seconds, f"adopt ${preset:02X}")
buckets = perSecond(run["samples"])
zeros = {}
for index, name in ((1, "conn0"), (2, "conn1")):
values = [row[index] for row in buckets]
zeros[name] = sum(1 for value in values if value < 0.5)
print(f" {name} per second: min {min(values):.1f}, max {max(values):.1f}, "
f"seconds carrying nothing: {zeros[name]} of {len(values)}", flush=True)
open(f"{LOGS}/adoptPair.{tag}.samples.txt", "w").write(
"\n".join(f"{t}\t{c0}\t{c1}" for t, c0, c1 in run["samples"]) + "\n")
for session, sessionTag in sessions:
rows[(sessionTag, "afterWindow")] = registers(session, sessionTag,
"at the end of the window")
print("\n---- uartRxCount $E42D sampled repeatedly ----", flush=True)
sampleRings(sessions, 12, 0.5, f"adoptPair.{tag}")
for state in [snapshot(session, sessionTag) for session, sessionTag in sessions]:
showState(state)
a.shot(f"{SHOTS}/ap{tag}03end.png")
b.shot(f"{SHOTS}/ap{tag}03endB.png")
print("\n================ SUMMARY ================", flush=True)
print(f"preset control ${preset:02X}"
+ (f", preset command ${presetCommand:02X}" if presetCommand is not None else ""),
flush=True)
for sessionTag in ("A", "B"):
for when in ("atPrompt", "preset", "opened", "phase3", "afterWindow"):
row = rows[(sessionTag, when)]
text = " ".join(f"{name}=" + ("None" if row[name] is None
else f"${row[name]:02X}")
for name in ("control", "command", "baudIndex", "live",
"adoptSlot", "latch"))
print(f" [{sessionTag}] {when:12s} {text}", flush=True)
print(f" window {run['wall']:.2f} s wall; speeds "
f"{ {tag2: round(100.0 * value, 1) for tag2, value in run['speed'].items()} }",
flush=True)
for index in (0, 1):
count = run["counts"][index]
print(f" conn{index} {count} bytes = {count / run['wall']:.2f} B/s; "
f"wire {run['wire'][index]}", flush=True)
print(f" seconds carrying nothing: {zeros}", flush=True)
print(f" both reached phase 3: {ok} ({phases})", flush=True)
finally:
a.close()
b.close()
time.sleep(1)
relay.stop()
for index in (0, 1):
print(f"[relay] conn{index} total {len(relay.log[index])} bytes", flush=True)
if __name__ == "__main__":
main()