#!/usr/bin/env bash # # selftest.sh - non-persistent self-test for the IEC listener kernel module. # # This is NOT a full install: nothing is copied into /lib/modules and no # autoload is configured. It only loads the module, tests it, and unloads it. # # Entry point: the built iec_listener.ko is already on the Pi. This script: # 1. checks the module is compatible with the running kernel (vermagic), # 2. loads it (insmod) - non-persistent, no reboot needed, # 3. runs the self-test ioctl on /dev/iec0, # 4. unloads it again (rmmod) so the Pi is left exactly as before. # # The module is never left loaded by this script; it always unloads, even if # the self-test fails or the script is interrupted. # # Usage: # sudo ./selftest.sh [path/to/iec_listener.ko] [--address N] # # Defaults: ./iec_listener.ko (or ~/iec_listener.ko), address=4 (printer). set -euo pipefail MODULE="iec_listener" ADDRESS=4 KO="" LOADED=0 # ---- pretty output (color only on a tty) -------------------------------- if [ -t 1 ]; then C_OK=$'\033[32m'; C_ERR=$'\033[31m'; C_INFO=$'\033[36m'; C_RST=$'\033[0m' else C_OK=""; C_ERR=""; C_INFO=""; C_RST="" fi ok() { printf '%s OK %s %s\n' "$C_OK" "$C_RST" "$*"; } info() { printf '%s ==>%s %s\n' "$C_INFO" "$C_RST" "$*"; } die() { printf '%sFAIL%s %s\n' "$C_ERR" "$C_RST" "$*" >&2; exit 1; } # ---- always unload on the way out --------------------------------------- cleanup() { if [ "$LOADED" = 1 ] && lsmod | grep -q "^${MODULE}\b"; then info "rmmod ${MODULE} (releases DATA, removes /dev/iec0)" rmmod "$MODULE" 2>/dev/null && ok "unloaded - Pi left as before" \ || info "rmmod failed; unload manually: sudo rmmod ${MODULE}" fi } trap cleanup EXIT # ---- args --------------------------------------------------------------- while [ $# -gt 0 ]; do case "$1" in --address) ADDRESS="${2:-}"; shift 2 ;; --address=*) ADDRESS="${1#*=}"; shift ;; -h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; -*) die "unknown option: $1" ;; *) KO="$1"; shift ;; esac done # ---- preconditions ------------------------------------------------------ [ "$(id -u)" -eq 0 ] || die "must run as root (use: sudo $0 ...)" case "$ADDRESS" in ''|*[!0-9]*) die "address must be 0-30 (got '$ADDRESS')" ;; esac { [ "$ADDRESS" -ge 0 ] && [ "$ADDRESS" -le 30 ]; } || die "address out of range 0-30: $ADDRESS" command -v modinfo >/dev/null || die "modinfo not found (install kmod)" # ---- locate the module matching the running kernel ---------------------- # A released package ships one module per supported kernel under modules/. # The build workflow names each file iec_listener_.ko, with # ':' and '/' in the version replaced by '-' (1:6.12.93-1+rpt1 -> # iec_listener_1-6.12.93-1+rpt1.ko); local builds produce a bare # iec_listener.ko. The glob below ($MODULE*.ko) matches both. # # We select by vermagic, not by filename: the package name carries a Debian # epoch/revision (1:6.12.93-1+rpt1) that `uname -r` (6.12.93+rpt-rpi-v8) does # not, so the .ko whose vermagic matches the running kernel is the one to load. RUNNING="$(uname -r)" # Where to look, de-duplicated ($(dirname "$0") is often "." when run locally). SEARCH_DIRS=() for d in "$(dirname "$0")/modules" "$(dirname "$0")" "./modules" "." "$HOME"; do skip=0 for s in "${SEARCH_DIRS[@]:-}"; do [ "$s" = "$d" ] && skip=1; done [ "$skip" = 0 ] && SEARCH_DIRS+=("$d") done if [ -z "$KO" ]; then info "running kernel: $RUNNING" for dir in "${SEARCH_DIRS[@]}"; do [ -d "$dir" ] || continue for cand in "$dir"/${MODULE}*.ko; do [ -f "$cand" ] || continue vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" || continue if [ "${vm%% *}" = "$RUNNING" ]; then KO="$cand"; break 2 fi done done fi if [ -z "$KO" ]; then msg="no module matching the running kernel ($RUNNING) found. Pass a path explicitly (sudo $0 path/to/${MODULE}.ko), or rebuild for this kernel. Modules seen:" any=0 for dir in "${SEARCH_DIRS[@]}"; do [ -d "$dir" ] || continue for cand in "$dir"/${MODULE}*.ko; do [ -f "$cand" ] || continue if vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" && [ -n "$vm" ]; then note="built for ${vm%% *}" else # modinfo couldn't read it - usually not an ELF .ko at all # (e.g. a still-zipped release file). Show what it really is. note="UNREADABLE: $(file -b "$cand" 2>/dev/null || echo 'not a kernel module') - unzip/rebuild?" fi msg="$msg"$'\n'" $cand ($note)" any=1 done done [ "$any" = 1 ] || msg="$msg"$'\n'" (none)" die "$msg" fi [ -f "$KO" ] || die "module not found: $KO" info "module: $KO" info "address: $ADDRESS" # ---- 1. compatibility check (vermagic vs running kernel) ---------------- RUNNING="$(uname -r)" ARCH="$(uname -m)" VERMAGIC="$(modinfo -F vermagic "$KO")" || die "cannot read vermagic from $KO" MOD_KVER="${VERMAGIC%% *}" # first token of vermagic = kernel version info "running kernel: $RUNNING ($ARCH)" info "module vermagic: $VERMAGIC" [ "$MOD_KVER" = "$RUNNING" ] || die \ "kernel mismatch: module built for '$MOD_KVER' but running '$RUNNING'. Rebuild against the current headers (see docs/kernel-notes.md): pkg=\"linux-headers-\$(uname -r | sed 's/.*+rpt-//')\" echo \"HEADERS_PKG=\$pkg KERNEL_VERSION=\$(dpkg-query -W -f='\${Version}' \"\$pkg\")\"" case "$VERMAGIC" in *"$ARCH"*) ;; *) die "arch mismatch: module vermagic has no '$ARCH' (wrong flavour built?)" ;; esac ok "compatible with running kernel" # ---- 2. load (reload if a stale copy is already in) --------------------- if lsmod | grep -q "^${MODULE}\b"; then info "a copy is already loaded; removing it first" rmmod "$MODULE" || die "rmmod failed (is /dev/iec0 in use?)" fi info "insmod ${MODULE}.ko address=${ADDRESS}" insmod "$KO" address="$ADDRESS" || die "insmod failed; check: dmesg | tail" LOADED=1 [ -c /dev/iec0 ] || die "/dev/iec0 was not created" ok "loaded; /dev/iec0 present" # ---- 3. self-test ioctl ------------------------------------------------- info "running self-test (IEC_IOC_SELFTEST) - run with the C64 disconnected" set +e python3 - <<'PY' import array, fcntl, sys IEC_IOC_SELFTEST = 0x80044903 # _IOR('I', 3, __u32) BITS = [ ("DATA_ASSERT_OK", 0x01), # DATA read low while driven low ("DATA_FLOAT_OK", 0x02), # DATA read high after release (Hi-Z) ("ATN_RELEASED", 0x04), # ATN high (idle) ("CLK_RELEASED", 0x08), # CLK high (idle) ("RESET_RELEASED", 0x10), # RESET high (idle) ] buf = array.array("I", [0]) with open("/dev/iec0", "rb", buffering=0) as f: fcntl.ioctl(f, IEC_IOC_SELFTEST, buf, True) r = buf[0] print(" selftest = 0x%02x (%s)" % (r, "PASS" if r == 0x1F else "FAIL")) for name, bit in BITS: print(" %-15s %s" % (name, "ok" if r & bit else "MISSING")) sys.exit(0 if r == 0x1F else 1) PY ST=$? set -e # ---- 4. report (rmmod happens in the EXIT trap) ------------------------- echo if [ "$ST" -eq 0 ]; then ok "self-test PASSED (0x1F) - all four lines wired correctly" else info "self-test did not fully pass." info "On a BARE board 0x15 is expected and fine: only DATA_ASSERT_OK is" info "meaningful; ATN/RESET pass via the SoC's fixed GPIO2/3 pull-ups." info "Re-run once the level shifter is wired + powered; you want 0x1F." fi exit "$ST"