#!/usr/bin/env bash # Part of JoeyLib, Copyright (c) 2026 Scott Duensing, under the MIT License. # See LICENSE.md for license information. # SPDX-License-Identifier: MIT # # verify-atarist-input.sh - Headless typed-character acceptance gate for # the Atari ST port. Autostarts KEYS.PRG in Hatari (EmuTOS, GEMDOS # drive), injects the RetroNet definition-of-done string through # Hatari's command FIFO as IKBD key events, and reads the example's # verification scanline back out of ST RAM with the built-in debugger. # The row is planar (4 interleaved bitplane words per 16 pixels); the # decode below reassembles the nibble pixels, which must equal the # typed ASCII exactly. # # Injection encoding (Hatari 2.4 `hatari-event keypress ` parses a # single-character argument as an ASCII character and a multi-character # argument as an ST scancode): digits go as ASCII, '.' as scancode 52, # and ':' as shift make (42) + ';' scancode 39 + shift break -- which # also exercises the ISR's shift handling through the TOS keymap. # # Then the named keys the ST gives a second key for: the keypad's 8, 2, 4, # and 6 as the arrows and its Enter as Return, each held alone (Hatari's # keydown/keyup by scancode), and Up and the keypad's 8 held together with # either let go first, which must leave Up down -- read off KEYS's row 4 # cells in ST RAM (scripts/lib/keysGate.sh; the cells' middles fall on a # 16-pixel group's first pixel). # # Then KEYS again on German EmuTOS, whose Alt layer types @ [ ] on Alt with # the keys U-umlaut, O-umlaut, and A-umlaut, and \ { } with Shift too; Alt # with a letter types nothing, and a plain key goes on typing. And once more # with the OS header made to say TOS 1.04 instead of EmuTOS (Hatari's # debugger writes it before the boot): then the layer must come from # src/atarist/stAltLayer.c's German table, found by the header's country, # as on a German ST's own TOS, which keeps its layer out of Keytbl. # # Usage: scripts/verify-atarist-input.sh # Requires: hatari, the EmuTOS images (US and German), # build/atarist/bin/KEYS.PRG. set -uo pipefail repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd) source "$repo/scripts/lib/joeyEnv.sh" source "$repo/scripts/lib/keysGate.sh" joeyNeed HATARI hatari joeyNeed EMUTOS_IMG emutos joeyNeed EMUTOS_DE_IMG emutos stScreenDump=$(joeyTool stScreenDump) || exit 1 bin_dir=$repo/build/atarist/bin label=verify-atarist-input target="192.168.1.10:6510" # Alt with U-, O-, and A-umlaut (scancodes 0x1A, 0x27, 0x28), the same with # Shift, Alt with A (nothing), then a plain 2 (an ASCII keypress). deTarget='@[]\{}2' [ -f "$bin_dir/KEYS.PRG" ] || { echo "$label: build KEYS.PRG first (make atarist)" >&2; exit 2; } work=$(mktemp -d -t joeylib-st-input.XXXXXX) cleanup() { joeyStop "${hatari_pid:-}" rm -rf "$work" } trap cleanup EXIT send() { timeout 5 bash -c "echo \"\$1\" > \"$work/cmd.fifo\"" _ "$1" || return 1 sleep 0.2 } # hatariBoot TOS [HATARI-ARG...] -- KEYS in Hatari on that TOS image; sets # hatari_pid and rowAddr, KEYS's verification row in ST RAM, once its A5A5 # sentinel is up. # The video base is read from the shifter regs each attempt (planar # sentinel = plane words 5000 A000 5000 A000). hatariBoot() { local tos="$1" local i local base local addr local row shift joeyStop "${hatari_pid:-}" rm -f "$work/cmd.fifo" (cd "$work" && exec env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \ --tos "$tos" \ --harddrive "$bin_dir" \ --gemdos-drive C \ --auto 'C:\KEYS.PRG' \ --cmd-fifo "$work/cmd.fifo" \ --fast-forward on \ "$@" \ > "$work/hatari.log" 2>&1) & hatari_pid=$! for i in $(seq 1 30); do [ -p "$work/cmd.fifo" ] && break sleep 1 done [ -p "$work/cmd.fifo" ] || { echo "$label: FAIL - hatari never created the FIFO" >&2; exit 1; } rowAddr="" for i in $(seq 1 45); do sleep 1 send 'hatari-debug m $ff8201 4' || continue base=$(grep -E '^00FF8201:' "$work/hatari.log" | tail -1 | awk '{print $2 $4}') [ -n "$base" ] || continue addr=$(( (16#$base << 8) + 198 * 160 )) send "hatari-debug m \$$(printf '%x' "$addr") 8" || continue row=$(grep -E "^$(printf '%08X' "$addr"):" "$work/hatari.log" | tail -1) if echo "$row" | grep -qi "50 00 a0 00 50 00 a0 00"; then rowAddr=$addr return 0 fi done echo "$label: FAIL - KEYS sentinel never appeared" >&2 tail -10 "$work/hatari.log" >&2 exit 1 } # rowCheck TARGET -- KEYS's verification row must hold exactly TARGET. rowCheck() { sleep 3 send "hatari-debug m \$$(printf '%x' "$rowAddr") 160" sleep 2 "$stScreenDump" keys "$work/hatari.log" "$rowAddr" "$1" } hatariBoot "$EMUTOS_IMG" # Type the string: digits as ASCII characters, '.' as scancode 52, # ':' as shift + scancode 39. for c in 1 9 2; do send "hatari-event keypress $c"; done send "hatari-event keypress 52" for c in 1 6 8; do send "hatari-event keypress $c"; done send "hatari-event keypress 52" send "hatari-event keypress 1" send "hatari-event keypress 52" for c in 1 0; do send "hatari-event keypress $c"; done send "hatari-event keydown 42" send "hatari-event keypress 39" send "hatari-event keyup 42" for c in 6 5 1 0; do send "hatari-event keypress $c"; done rowCheck "$target" || exit 1 # KEYS's row 4 cells (scripts/lib/keysGate.sh's steps): the middle row's # address, and each cell's 16-pixel group (examples/keys: 28x26 cells 32 # and 30 apart from (2, 2)), whose first pixel is the cell's middle. The # keys by their ST scancodes. cellAddr=$((rowAddr - (198 - 135) * 160)) declare -A stScan=([up]=72 [kp8]=104 [kp2]=110 [kp4]=106 [kp6]=108 [kpenter]=114) KEYS_LABEL=$label keysPress() { send "hatari-event keydown ${stScan[$1]}" } keysRelease() { send "hatari-event keyup ${stScan[$1]}" } keysRead() { send "hatari-debug m \$$(printf '%x' "$cellAddr") 160" sleep 1 "$stScreenDump" cells "$work/hatari.log" "$cellAddr" } KEYS_SETTLE_SECONDS=1 keysRunSteps kp8 kp2 kp4 kp6 kpenter if [ -n "$KEYS_FAILS" ]; then echo "$label: FAIL - $KEYS_FAILS" >&2 exit 1 fi # altLayerType -- the Alt layer's keys, as deTarget says. altLayerType() { send "hatari-event keydown 56" for c in 26 39 40; do send "hatari-event keypress $c"; done send "hatari-event keydown 42" for c in 26 39 40; do send "hatari-event keypress $c"; done send "hatari-event keyup 42" send "hatari-event keypress 30" send "hatari-event keyup 56" send "hatari-event keypress 2" } # German EmuTOS's Alt layer, from its Keytbl tables. hatariBoot "$EMUTOS_DE_IMG" altLayerType rowCheck "$deTarget" || exit 1 # The same as TOS 1.04 would show it: "XXXX" over the header's "ETOS" # (offset 0x2C) and version 0x0104 (offset 2), in the 512 KB ROM at # 0xE00000. printf 'w $e0002c $58 $58 $58 $58\nw $e00002 $01 $04\n' > "$work/tos104.dbg" hatariBoot "$EMUTOS_DE_IMG" --parse "$work/tos104.dbg" send 'hatari-debug m $e0002c 4' sleep 1 if ! grep -qi "^00E0002C: 58 58 58 58" "$work/hatari.log"; then echo "$label: FAIL - the OS header still says EmuTOS: $(grep -i '^00E0002C:' "$work/hatari.log" | tail -1)" >&2 exit 1 fi altLayerType rowCheck "$deTarget" || exit 1 echo "$label: PASS (typed \"$target\", all ${#target} chars arrived via jlInputGetChar; $KEYS_STEPS named-key steps: the keypad's arrows and Enter, Up held with its second key; German EmuTOS's Alt layer typed \"$deTarget\", from its tables and as TOS 1.04 from the built-in German one)"