diff --git a/tools/bench/README.md b/tools/bench/README.md index b3734be8..c2e9785d 100644 --- a/tools/bench/README.md +++ b/tools/bench/README.md @@ -51,6 +51,7 @@ whatever node appeared beside it. | `grants.py` | Service bit 20 end to end: packages `grants/module.cpp` and `grants/client.a` as `/VMS/GRANTS`, then launches one content file per slice size. The client grants a slice from a CIA2 timer NMI every 200 cycles, when it will end before the next tick, and counts the NMIs it loses; the module writes 4 KiB to `$4000` for 1.5 s and exits with its job count and that loss, which the main image prints on the way back. Each row also checks a sum the client takes over what it holds after the last job, and a slice that never fits, like a client that never grants (the first row), ends the job `VM_C64_NO_GRANT`. `grants.py 120 140` runs only those rows. Needs `acme` on `PATH` and a host serving bit 20 installed. | | `screen.py` | The C64 text screen. | | `keypress.py [code]` | Put a key in the keyboard buffer (default `Y`; F1/F3/F5/F7 are `0x85`..`0x88`). | +| `type.py [--c128] ''` | Type a line into the keyboard buffer, ten keys at a time; `\n` is RETURN. `--c128` for a C128 in 128 mode. | | `colors.py` | Top row, its colour RAM and the VIC colour registers. | | `listen.py [secs] [hex]` | Echo serial output, optionally sending bytes first. | | `probe.py` | Which image is running -- main, minimal or silent -- and its build banner. | diff --git a/tools/bench/c64.py b/tools/bench/c64.py index 2ca423ed..1db07c2e 100644 --- a/tools/bench/c64.py +++ b/tools/bench/c64.py @@ -13,6 +13,8 @@ VIC_DEN = 0x10 KEYBUF, KEYCOUNT = 0x0277, 0x00C6 # KERNAL keyboard buffer, and the count GETIN reads +KEYBUF_128, KEYCOUNT_128 = 0x034A, 0x00D0 # the same pair in C128 mode +KEYBUF_SIZE = 10 PETSCII_U, PETSCII_Y = 0x55, 0x59 F1, F3, F5, F7, F8 = 0x85, 0x86, 0x87, 0x88, 0x8C diff --git a/tools/bench/test_type.py b/tools/bench/test_type.py new file mode 100644 index 00000000..c515a27a --- /dev/null +++ b/tools/bench/test_type.py @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: MIT +"""Tests for type.py's text-to-key-codes step. python3 -m unittest discover -s tools/bench""" +import importlib +import unittest + +keys = importlib.import_module('type').keys + + +class Keys(unittest.TestCase): + def test_letters_go_in_unshifted(self): + self.assertEqual(keys('list'), b'LIST') + + def test_newline_is_return(self): + self.assertEqual(keys('run\\n'), b'RUN\r') + + def test_hex_escape_is_sent_as_written(self): + self.assertEqual(keys('\\x61\\x0a\\x85'), b'\x61\x0a\x85') + + def test_backslash_escapes_itself(self): + self.assertEqual(keys('a\\\\b'), b'A\\B') + + def test_malformed_escape_is_refused(self): + for text in ('ab\\', '\\xZZ', '\\x6', '\\t', '\\u0041'): + with self.subTest(text=text), self.assertRaises(ValueError): + keys(text) + + def test_non_ascii_is_refused(self): + with self.assertRaises(ValueError): + keys('café') + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/bench/type.py b/tools/bench/type.py new file mode 100755 index 00000000..3542841d --- /dev/null +++ b/tools/bench/type.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Type a line into the KERNAL keyboard buffer by DMA, a buffer-full at a time. + + type.py [--c128] '' + +`\\n` is RETURN, `\\\\` a backslash and `\\xNN` any key code. Letters go in unshifted (uppercase on the +power-on character set). +--c128 uses the C128-mode buffer; C64 mode, including a C128 in 64 mode, is the default. +""" +import re +import sys +import time +from c64 import KEYBUF, KEYBUF_128, KEYBUF_SIZE, KEYCOUNT, KEYCOUNT_128 +from trlink import Link + +DRAIN_TIMEOUT = 30 +RETURN = 0x0D +TOKEN = re.compile(r'\\x([0-9a-fA-F]{2})|\\(.?)|(.)', re.DOTALL) + + +def main(): + args = sys.argv[1:] + c128 = '--c128' in args + args = [a for a in args if a != '--c128'] + if len(args) != 1: + sys.exit(__doc__.strip()) + keybuf, keycount = (KEYBUF_128, KEYCOUNT_128) if c128 else (KEYBUF, KEYCOUNT) + try: + data = keys(args[0]) + except ValueError as e: + sys.exit(str(e)) + with Link() as tr: + for i in range(0, len(data), KEYBUF_SIZE): + wait_until_read(tr, keycount, i) + chunk = data[i:i + KEYBUF_SIZE] + if not (tr.poke(keybuf, chunk) and tr.poke(keycount, [len(chunk)])): + sys.exit(f'poke refused at character {i}') + wait_until_read(tr, keycount, len(data)) + + +def keys(text): + if not text.isascii(): + raise ValueError('ASCII only: write any other key code as \\xNN') + out = bytearray() + for code, escaped, plain in TOKEN.findall(text): + if code: + out.append(int(code, 16)) + elif plain: + out += plain.upper().encode() + elif escaped == 'n': + out.append(RETURN) + elif escaped == '\\': + out += b'\\' + else: + raise ValueError(f'unknown escape \\{escaped}: use \\n, \\\\ or \\xNN') + return bytes(out) + + +def wait_until_read(tr, keycount, at): + start = time.time() + while tr.peek(keycount, 1)[0]: + if time.time() - start > DRAIN_TIMEOUT: + sys.exit(f'keyboard buffer not read for {DRAIN_TIMEOUT} s, at character {at}') + time.sleep(0.1) + + +if __name__ == '__main__': + main() diff --git a/tools/c128/README.md b/tools/c128/README.md new file mode 100644 index 00000000..4e6b3825 --- /dev/null +++ b/tools/c128/README.md @@ -0,0 +1,15 @@ +# C128 tools + +Scripts for working on C128 code for the TR in VICE. Python 3, standard library only. + +| Script | Does | +|--------|------| +| `vicemon.py "" ""` | Run VICE remote-monitor commands, resume, and run more once a breakpoint stops it, on one connection. | + +## Headless VICE + +The headless VICE build (`--enable-headlessui`) runs without a window. It cannot save +screenshots, so read results from memory through the monitor instead (`bsave`, or `m`). + + x128 -default -remotemonitor -monlog -monlogname mon.log -limitcycles 2000000000 & + python3 tools/c128/vicemon.py "break 2f3c" "m 2f3c 2f4f" diff --git a/tools/c128/vicemon.py b/tools/c128/vicemon.py new file mode 100755 index 00000000..ae718a00 --- /dev/null +++ b/tools/c128/vicemon.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +"""Run VICE remote-monitor commands across a breakpoint, on one connection. + + vicemon.py "" "" + +Commands are separated by ';'. The first set runs, then `x` resumes the emulator; once the +monitor prompt comes back (a breakpoint or watchpoint hit), the second set runs. Start +VICE with -remotemonitor (127.0.0.1:6510, or $VICE_MONITOR as host or host:port). +""" +import os +import re +import socket +import sys + +PROMPT = re.compile(rb'\(C:\$[0-9a-fA-F]{4}\) $') +COMMAND_TIMEOUT = 30 +GREETING_WAIT = 0.5 +DEFAULT_HOST, DEFAULT_PORT = '127.0.0.1', 6510 + + +def main(): + if len(sys.argv) != 3: + sys.exit(__doc__.strip()) + host, _, port = os.environ.get('VICE_MONITOR', DEFAULT_HOST).partition(':') + s = socket.create_connection((host or DEFAULT_HOST, int(port) if port else DEFAULT_PORT)) + + def send(cmd): + s.sendall((cmd + '\n').encode()) + + def until_prompt(timeout, what): + s.settimeout(timeout) + out = b'' + try: + while not PROMPT.search(out): + data = s.recv(65536) + if not data: + sys.exit(f'VICE closed the monitor connection {what}') + out += data + except socket.timeout: + sys.exit(f'no monitor prompt within {timeout} s {what}') + return out.decode('latin1') + + def run(cmds, show): + for cmd in commands(cmds): + send(cmd) + out = until_prompt(COMMAND_TIMEOUT, f'after {cmd!r}') + if show: + print(out) + + s.settimeout(GREETING_WAIT) + try: + s.recv(65536) + except socket.timeout: + pass + run(sys.argv[1], show=False) + send('x') + print(until_prompt(None, 'before stopping')) + run(sys.argv[2], show=True) + + +def commands(cmds): + return [c.strip() for c in cmds.split(';') if c.strip()] + + +main()