232 lines
8.3 KiB
Bash
Executable file
232 lines
8.3 KiB
Bash
Executable file
#!/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 <key>` 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.
|
|
#
|
|
# 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
|
|
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 -- 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 i
|
|
local base
|
|
local addr
|
|
local row
|
|
joeyStop "${hatari_pid:-}"
|
|
rm -f "$work/cmd.fifo"
|
|
(cd "$work" && exec env SDL_VIDEODRIVER=dummy SDL_AUDIODRIVER=dummy "$HATARI" \
|
|
--tos "$1" \
|
|
--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
|
|
python3 - "$work/hatari.log" "$rowAddr" "$1" <<'PY'
|
|
import re
|
|
import sys
|
|
|
|
log, rowAddr, target = sys.argv[1], int(sys.argv[2]), sys.argv[3]
|
|
data = open(log, "rb").read().decode(errors="replace")
|
|
lines = {}
|
|
# Keep only the LAST dump of each address (earlier sentinel probes also match).
|
|
for m in re.finditer(r"^(00[0-9A-F]{6}): ((?:[0-9a-f]{2} ){1,16})", data, re.M):
|
|
lines[int(m.group(1), 16)] = m.group(2)
|
|
raw = b""
|
|
for off in range(0, 160, 16):
|
|
if rowAddr + off not in lines:
|
|
print(f"verify-atarist-input: FAIL - missing dump line at +{off}")
|
|
sys.exit(1)
|
|
raw += bytes.fromhex(lines[rowAddr + off].replace(" ", ""))
|
|
|
|
def group(words):
|
|
p = [int.from_bytes(words[i * 2:i * 2 + 2], "big") for i in range(4)]
|
|
return [((p[0] >> b) & 1) | (((p[1] >> b) & 1) << 1) |
|
|
(((p[2] >> b) & 1) << 2) | (((p[3] >> b) & 1) << 3)
|
|
for b in range(15, -1, -1)]
|
|
|
|
pix = []
|
|
for g in range(len(raw) // 8):
|
|
pix += group(raw[g * 8:g * 8 + 8])
|
|
|
|
if pix[0:4] != [0xA, 0x5, 0xA, 0x5]:
|
|
print(f"verify-atarist-input: FAIL - sentinel wrong: {pix[0:4]}")
|
|
sys.exit(1)
|
|
count = (pix[4] << 4) | pix[5]
|
|
chars = bytes((pix[8 + 2 * i] << 4) | pix[8 + 2 * i + 1] for i in range(count))
|
|
print(f"VERIFY-ST-INPUT count={count} chars={chars!r}")
|
|
if chars.decode("ascii", errors="replace") != target:
|
|
print(f'verify-atarist-input: FAIL - expected "{target}"')
|
|
sys.exit(1)
|
|
PY
|
|
}
|
|
|
|
|
|
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
|
|
python3 - "$work/hatari.log" "$cellAddr" <<'PY'
|
|
import re
|
|
import sys
|
|
|
|
log, addr = sys.argv[1], int(sys.argv[2])
|
|
GROUPS = (5, 7, 9, 11, 13, 15, 17)
|
|
data = open(log, "rb").read().decode(errors="replace")
|
|
lines = {}
|
|
for m in re.finditer(r"^(00[0-9A-F]{6}): ((?:[0-9a-f]{2} ){1,16})", data, re.M):
|
|
lines[int(m.group(1), 16)] = m.group(2)
|
|
raw = b""
|
|
for off in range(0, 160, 16):
|
|
raw += bytes.fromhex(lines.get(addr + off, "00 " * 16).replace(" ", ""))
|
|
digits = ""
|
|
for g in GROUPS:
|
|
colour = 0
|
|
for plane in range(4):
|
|
word = int.from_bytes(raw[g * 8 + plane * 2:g * 8 + plane * 2 + 2], "big")
|
|
colour |= ((word >> 15) & 1) << plane
|
|
digits += "1" if colour == 2 else ("0" if colour == 1 else "?")
|
|
print(digits)
|
|
PY
|
|
}
|
|
KEYS_SETTLE_SECONDS=1
|
|
keysRunSteps kp8 kp2 kp4 kp6 kpenter
|
|
if [ -n "$KEYS_FAILS" ]; then
|
|
echo "$label: FAIL - $KEYS_FAILS" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# German EmuTOS's Alt layer.
|
|
hatariBoot "$EMUTOS_DE_IMG"
|
|
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"
|
|
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\")"
|