177 lines
6.3 KiB
Bash
Executable file
177 lines
6.3 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# verify-dvx-input.sh - Typed-character and present gate for DVX. Runs KEYS
|
|
# (examples/keys) as the desktop of a scratch DVX in DOSBox Staging under
|
|
# Xvfb and types the RetroNet definition-of-done string with xdotool (real X
|
|
# key events: ':' needs Shift, which DVX's keyboard layout applies before the
|
|
# window's onKey hands the character to jlInputGetChar). Two checks:
|
|
# 1. the present: a raw screenshot of the desktop must show KEYS's
|
|
# verification scanline (row 198: sentinel, count, every received
|
|
# character as two pixel nibbles) somewhere -- the window's content,
|
|
# expanded from the stage through the port's palette tables into DVX's
|
|
# 16-bit desktop -- decoding to exactly the typed bytes;
|
|
# 2. the typed history: after Escape, KEYS logs every received character
|
|
# as hex to its joeylog.txt (in its settings folder), which must equal
|
|
# the typed ASCII.
|
|
#
|
|
# Usage: scripts/verify-dvx-input.sh
|
|
# Requires: DOSBox Staging, Xvfb, xdotool, python3 + PIL, the dvx component;
|
|
# the keys program (make dvx).
|
|
set -uo pipefail
|
|
|
|
repo=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
source "$repo/scripts/lib/joeyEnv.sh"
|
|
source "$repo/scripts/lib/dvxGate.sh"
|
|
joeyNeed DOSBOX dosbox
|
|
joeyNeed DVX_ROOT dvx
|
|
|
|
program=keys
|
|
target="192.168.1.10:6510"
|
|
wall=180 # hard cap on DOSBox, whatever happens to this script
|
|
readySeconds=60 # DVX boot until its shell reports ready
|
|
settleSeconds=3 # then KEYS opening its window and taking the focus
|
|
logSeconds=30 # Escape until KEYS's typed line is in its log
|
|
|
|
dvxNeedBuild "$program"
|
|
|
|
work=$(mktemp -d -t joeylib-dvx-input.XXXXXX)
|
|
guard=""
|
|
dosbox=""
|
|
xvfb=""
|
|
# 2>/dev/null: bash reports the SIGKILLed timeout wrapper as "Killed".
|
|
cleanup() {
|
|
joeyStop "$dosbox" "$guard" "$xvfb" 2> /dev/null
|
|
rm -rf "$work"
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
dvxStage "$work/c" "$(dvxAppDir "$program")"
|
|
dvxDosboxConf "$work/dosbox.conf" "$work/c"
|
|
printf '\n[capture]\ncapture_dir = %s\ndefault_image_capture_formats = raw\n' "$work/shots" >> "$work/dosbox.conf"
|
|
mkdir -p "$work/xdg" "$work/shots"
|
|
|
|
display=":$((RANDOM % 400 + 2500))"
|
|
Xvfb "$display" -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 &
|
|
xvfb=$!
|
|
sleep 1
|
|
|
|
XDG_CONFIG_HOME="$work/xdg" DISPLAY="$display" SDL_AUDIODRIVER=dummy \
|
|
timeout -s KILL "$wall" "$DOSBOX" "${JOEY_DOSBOX_ISOLATE[@]}" \
|
|
--conf "$work/dosbox.conf" > "$work/dosbox.log" 2>&1 &
|
|
guard=$!
|
|
dosbox=$(joeyChild "$guard")
|
|
|
|
ready=""
|
|
for ((waited = 0; waited < readySeconds; waited++)); do
|
|
if dvxDvxLog "$work/c" 200 | grep -q "DVX Shell ready"; then
|
|
ready=1
|
|
break
|
|
fi
|
|
joeyAlive "$guard" || break
|
|
sleep 1
|
|
done
|
|
if [ -z "$ready" ]; then
|
|
echo "verify-dvx-input: FAIL - DVX did not come up within ${readySeconds}s (DVX.LOG follows)" >&2
|
|
dvxDvxLog "$work/c" >&2
|
|
exit 1
|
|
fi
|
|
sleep "$settleSeconds"
|
|
|
|
win=$(joeyFindWindow "$display" "$dosbox")
|
|
if [ -z "$win" ]; then
|
|
echo "verify-dvx-input: FAIL - DOSBox window not found" >&2
|
|
exit 1
|
|
fi
|
|
DISPLAY="$display" xdotool windowactivate --sync "$win" 2> /dev/null || true
|
|
DISPLAY="$display" xdotool type --window "$win" --delay 150 -- "$target"
|
|
sleep 2
|
|
# Ctrl+F5 = DOSBox screenshot hotkey.
|
|
DISPLAY="$display" xdotool key --window "$win" ctrl+F5
|
|
sleep 2
|
|
DISPLAY="$display" xdotool key --window "$win" Escape
|
|
|
|
log=$(dvxAwaitLog "$work/c" "$program" '^typed=' "$logSeconds" "$guard")
|
|
cp "$log" "$work/joeylog.txt" 2> /dev/null
|
|
joeyStop "$dosbox" "$guard" 2> /dev/null
|
|
dosbox=""
|
|
guard=""
|
|
|
|
png=$(ls "$work"/shots/*.png 2> /dev/null | head -1)
|
|
if [ -z "$png" ]; then
|
|
echo "verify-dvx-input: FAIL - no screenshot captured" >&2
|
|
exit 1
|
|
fi
|
|
if [ -z "$log" ]; then
|
|
echo "verify-dvx-input: FAIL - KEYS logged no typed line within ${logSeconds}s of Escape" >&2
|
|
exit 1
|
|
fi
|
|
|
|
python3 - "$png" "$target" "$work/joeylog.txt" <<'PY'
|
|
import sys
|
|
from PIL import Image
|
|
|
|
png, target, logPath = sys.argv[1], sys.argv[2], sys.argv[3]
|
|
fails = []
|
|
|
|
# KEYS palette 0 (buildPalette): a gray ramp with four demo colours and red
|
|
# at 15, 12-bit $0RGB, each channel widened x17 as the port's tables do.
|
|
pal12 = [(i << 8) | (i << 4) | i for i in range(16)]
|
|
pal12[0], pal12[1], pal12[2], pal12[3] = 0x000, 0x333, 0x0F0, 0xFFF
|
|
pal12[15] = 0xF00
|
|
palRgb = [(((v >> 8) & 0xF) * 17, ((v >> 4) & 0xF) * 17, (v & 0xF) * 17) for v in pal12]
|
|
# The desktop is 16-bit (5-6-5): a channel is off by at most 8 after the
|
|
# round trip, while the palette entries sit 17 apart; nearest is exact.
|
|
maxDist = 3 * 9 ** 2
|
|
|
|
img = Image.open(png).convert("RGB")
|
|
w, h = img.size
|
|
px = img.load()
|
|
|
|
nearest = {}
|
|
|
|
def nib(x, y):
|
|
rgb = px[x, y]
|
|
if rgb not in nearest:
|
|
best, bestDist = 0, 1 << 30
|
|
for n, pal in enumerate(palRgb):
|
|
d = sum((c - p) ** 2 for c, p in zip(rgb, pal))
|
|
if d < bestDist:
|
|
best, bestDist = n, d
|
|
nearest[rgb] = best if bestDist <= maxDist else -1
|
|
return nearest[rgb]
|
|
|
|
# Find the verification row: the sentinel's four nibbles A 5 A 5, then the
|
|
# count byte, anywhere on the desktop (the window is wherever DVX put it; at
|
|
# the gate's 640x480 it shows the stage at 1x).
|
|
expect = bytes([0xA5, 0xA5, len(target), 0x00]) + target.encode("ascii")
|
|
found = None
|
|
for y in range(h):
|
|
for x in range(w - 2 * len(expect)):
|
|
if nib(x, y) == 0xA and nib(x + 1, y) == 0x5 and nib(x + 2, y) == 0xA and nib(x + 3, y) == 0x5:
|
|
row = bytes(((nib(x + 2 * i, y) & 0xF) << 4) | (nib(x + 2 * i + 1, y) & 0xF) for i in range(len(expect)))
|
|
if row[:4] == expect[:4]:
|
|
found = (x, y, row)
|
|
break
|
|
if found:
|
|
break
|
|
if not found:
|
|
fails.append("no verification row (A5A5 %02X00) on the screen" % len(target))
|
|
else:
|
|
x, y, row = found
|
|
print("VERIFY-DVX-INPUT row at (%d,%d)=%s" % (x, y, row.hex().upper()))
|
|
if row != expect:
|
|
fails.append("screen row %s, want %s" % (row.hex().upper(), expect.hex().upper()))
|
|
|
|
typed = ""
|
|
for line in open(logPath, errors="replace"):
|
|
if line.startswith("typed="):
|
|
typed = line.strip()[len("typed="):]
|
|
want = target.encode("ascii").hex().upper()
|
|
print("VERIFY-DVX-INPUT log typed=%s" % typed)
|
|
if typed != want:
|
|
fails.append("log typed=%s, want %s" % (typed, want))
|
|
|
|
if fails:
|
|
print("verify-dvx-input: FAIL - " + "; ".join(fails))
|
|
sys.exit(1)
|
|
print('verify-dvx-input: PASS (typed "%s": all %d chars reached jlInputGetChar, and the present shows them)' % (target, len(target)))
|
|
PY
|