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