Compare commits

..

4 Commits

Author SHA1 Message Date
dafcdfbd7b feat(kernel): Add launch.sh for streamlined module loading and frontend execution
All checks were successful
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 1m13s
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 1m16s
Build kernel module / release (pull_request) Has been skipped
Introduce `launch.sh` as a single execution entry point for the IEC listener module and Python frontend. Automates kernel module selection, virtual environment setup, and cleanup to simplify usage. Ensures consistency by unloading modules on script exit.
2026-06-20 05:31:21 +02:00
b3d63664a0 docs(readme): Add instructions for running frontend on Pi
Document the process for setting up and running the Python frontend on
a Raspberry Pi using release packages. Include details on wiring
self-tests, module selection, and using `launch.sh` for streamlined
execution. Update kernel README to cross-reference top-level changes.
2026-06-20 05:31:11 +02:00
65ff323d50 feat(kernel): Include helper scripts and Python frontend in releases
Add `launch.sh` and `iecpoc` Python frontend to the release package for streamlined execution on the target device. Ensure accompanying files like `pyproject.toml` and `README.md` are included, while cleaning unnecessary caches.
2026-06-20 05:29:32 +02:00
3135a2502b fix(kernel): Handle unreadable module files in selftest.sh
Improve error handling in module discovery by identifying unreadable
`.ko` files and providing detailed error messages. Suggest actions like
unzip or rebuild to resolve issues.
2026-06-20 05:13:48 +02:00
5 changed files with 214 additions and 7 deletions

View File

@ -100,8 +100,10 @@ jobs:
if-no-files-found: error if-no-files-found: error
# Release flow only: merges all built modules into one modules/ folder, wraps it # Release flow only: merges all built modules into one modules/ folder, wraps it
# in a single repo-named top folder alongside selftest.sh, zips it and attaches # in a single repo-named top folder alongside the helper scripts (selftest.sh,
# it to the Gitea release for the pushed tag. Skipped entirely on pull requests. # launch.sh) and the iecpoc Python frontend (so launch.sh can pip-install it on
# the Pi), zips it and attaches it to the Gitea release for the pushed tag.
# Skipped entirely on pull requests.
release: release:
needs: build needs: build
if: startsWith(github.ref, 'refs/tags/') if: startsWith(github.ref, 'refs/tags/')
@ -123,7 +125,14 @@ jobs:
root="package/$repo" root="package/$repo"
mkdir -p "$root" mkdir -p "$root"
cp -r dist/modules "$root"/ # modules/iec_listener_<kernel_version>.ko cp -r dist/modules "$root"/ # modules/iec_listener_<kernel_version>.ko
cp kernel/selftest.sh "$root"/ # selftest.sh only at the package root cp kernel/selftest.sh "$root"/ # one-shot hardware self-test
cp launch.sh "$root"/ # load module -> run frontend -> unload
# iecpoc Python frontend + its packaging metadata. launch.sh pip-installs
# this on the Pi; the build reads pyproject.toml's readme = "README.md",
# so README.md must travel with it. __pycache__ is stripped to stay clean.
cp pyproject.toml README.md "$root"/
cp -r iecpoc "$root"/
find "$root/iecpoc" -name __pycache__ -type d -prune -exec rm -rf {} +
(cd package && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo") (cd package && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo")
- name: Publish to Gitea release - name: Publish to Gitea release

View File

@ -71,6 +71,39 @@ See [`docs/wiring.md`](docs/wiring.md) for the level-shifter circuit and the
pre-C64 bring-up checklist, and [`docs/kernel-notes.md`](docs/kernel-notes.md) pre-C64 bring-up checklist, and [`docs/kernel-notes.md`](docs/kernel-notes.md)
for the resolved kernel real-time decisions. for the resolved kernel real-time decisions.
## Running the frontend on the Pi
A tagged build publishes a release zip that bundles everything needed to run on
a Pi without building anything:
```
comodore-iec-emu/
selftest.sh one-shot wiring self-test (no C64 connected)
launch.sh load module → run iecpoc → unload
pyproject.toml iecpoc packaging metadata
README.md
iecpoc/ the Python frontend
modules/ iec_listener_<kernel_version>.ko, one per supported kernel
```
Unzip it on the Pi and run **`launch.sh`**. It installs the frontend into a local
`.venv`, detects the running kernel and picks the matching `.ko` (by *vermagic*,
not filename), `insmod`s it, runs `iecpoc`, and **always `rmmod`s on exit** — so
the Pi is left exactly as before:
```bash
unzip comodore-iec-emu-*.zip && cd comodore-iec-emu
sudo ./selftest.sh # first: check the wiring (want 0x1F)
sudo ./launch.sh # then: load + run the frontend (address 4)
sudo ./launch.sh --address 8 # listen as device 8
sudo ./launch.sh -- --raw # forward extra args (after --) to iecpoc
```
The module is picked automatically; pass `--ko PATH` only to force a specific
file. `launch.sh` needs `python3` plus `python3-venv` (and network on first run
for the build backend). See [`kernel/README.md`](kernel/README.md) for the
self-test details and the per-kernel module selection.
## Status ## Status
- **Phase 0 (userspace + tests):** complete, runs on any host. - **Phase 0 (userspace + tests):** complete, runs on any host.

View File

@ -23,16 +23,26 @@ before connecting the real bus (see PLAN.md §10).
autoload): it detects the running kernel, picks the matching module, loads it, autoload): it detects the running kernel, picks the matching module, loads it,
runs the self-test, and always unloads it again. runs the self-test, and always unloads it again.
A released package ships several kernel builds side by side: A released package ships several kernel builds side by side, plus the launcher
and the Python frontend:
``` ```
comodore-iec-emu/ comodore-iec-emu/
├── selftest.sh ├── selftest.sh # one-shot wiring self-test (this document)
├── launch.sh # load module → run the iecpoc frontend → unload
├── pyproject.toml # iecpoc packaging metadata (launch.sh pip-installs it)
├── README.md
├── iecpoc/ # the Python userspace decoder/trace
└── modules/ └── modules/
├── iec_listener_1-6.12.93-1+rpt1.ko # built for 6.12.x (bookworm) ├── iec_listener_1-6.12.93-1+rpt1.ko # built for 6.12.x (bookworm)
└── iec_listener_1-6.18.34-1+rpt1.ko # built for 6.18.x (trixie) └── iec_listener_1-6.18.34-1+rpt1.ko # built for 6.18.x (trixie)
``` ```
`selftest.sh` only verifies the wiring (it loads and immediately unloads). To
actually capture and decode C64 traffic, use **`launch.sh`**, which installs the
frontend, loads the matching module, runs `iecpoc`, and unloads on exit — see the
[top-level README](../README.md#running-the-frontend-on-the-pi).
You don't pick the file yourself: the script reads `uname -r`, then scans You don't pick the file yourself: the script reads `uname -r`, then scans
`modules/` (and a few fallback locations) and selects the `.ko` whose **vermagic** `modules/` (and a few fallback locations) and selects the `.ko` whose **vermagic**
matches the running kernel. Matching is by vermagic rather than filename because matches the running kernel. Matching is by vermagic rather than filename because

View File

@ -107,8 +107,14 @@ if [ -z "$KO" ]; then
[ -d "$dir" ] || continue [ -d "$dir" ] || continue
for cand in "$dir"/${MODULE}*.ko; do for cand in "$dir"/${MODULE}*.ko; do
[ -f "$cand" ] || continue [ -f "$cand" ] || continue
vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" if vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" && [ -n "$vm" ]; then
msg="$msg"$'\n'" $cand (built for ${vm%% *})" 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 any=1
done done
done done

149
launch.sh Executable file
View File

@ -0,0 +1,149 @@
#!/usr/bin/env bash
#
# launch.sh - load the IEC listener module, run the userspace frontend, unload.
#
# This is the "run the whole thing" entry point for a released package. Unlike
# selftest.sh (a one-shot wiring check that loads and immediately unloads), this
# keeps the module loaded for as long as the frontend runs. In order it:
# 1. installs the iecpoc Python frontend into a local virtualenv (.venv),
# 2. detects the running kernel and picks the matching iec_listener_*.ko,
# 3. insmods it (creating /dev/iec0),
# 4. runs `iecpoc`, streaming the decoded C64 traffic to the terminal,
# 5. on exit (Ctrl-C / EOF / error) ALWAYS rmmods again - the Pi is left
# exactly as before.
#
# The module is never left loaded by this script; the EXIT trap unloads it even
# if the frontend crashes or you interrupt it.
#
# Usage:
# sudo ./launch.sh [--address N] [--device PATH] [--ko FILE] [-- <iecpoc args>]
#
# Examples:
# sudo ./launch.sh # address 4, /dev/iec0, annotated trace
# sudo ./launch.sh --address 8 # listen as device 8
# sudo ./launch.sh -- --raw # forward --raw to iecpoc
# sudo ./launch.sh --ko modules/iec_listener_1-6.12.93-1+rpt1.ko
#
# Defaults: address=4 (printer), device=/dev/iec0.
set -euo pipefail
MODULE="iec_listener"
ADDRESS=4
DEVICE="/dev/iec0"
KO=""
VENV=".venv"
LOADED=0
declare -a PASS=()
# Run relative to the script's own directory so modules/, iecpoc/, pyproject.toml
# and .venv resolve the same way whether invoked as ./launch.sh or by full path.
HERE="$(cd "$(dirname "$0")" && pwd)"
cd "$HERE"
# ---- 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 the bus, removes ${DEVICE})"
rmmod "$MODULE" 2>/dev/null && ok "unloaded - Pi left as before" \
|| info "rmmod failed; unload manually: sudo rmmod ${MODULE}"
fi
}
trap cleanup EXIT
# ---- args ---------------------------------------------------------------
# Anything unrecognised (and everything after a literal --) is forwarded to
# iecpoc, so e.g. --raw / --logfile FILE just work.
while [ $# -gt 0 ]; do
case "$1" in
-a|--address) ADDRESS="${2:-}"; shift 2 ;;
--address=*) ADDRESS="${1#*=}"; shift ;;
-d|--device) DEVICE="${2:-}"; shift 2 ;;
--device=*) DEVICE="${1#*=}"; shift ;;
--ko) KO="${2:-}"; shift 2 ;;
--ko=*) KO="${1#*=}"; shift ;;
--) shift; PASS+=("$@"); break ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) PASS+=("$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)"
command -v python3 >/dev/null || die "python3 not found (sudo apt install python3)"
# ---- 1. python frontend: venv + install (once) --------------------------
# iecpoc declares no third-party runtime deps, so this is offline-safe except
# for the build backend (setuptools), which pip fetches the first time. The
# install is skipped on later runs once the entry point exists.
[ -f "$HERE/pyproject.toml" ] || die "pyproject.toml not found next to $0; package incomplete?"
if [ ! -x "$VENV/bin/iecpoc" ]; then
if [ ! -x "$VENV/bin/python" ]; then
info "creating virtualenv: $VENV"
python3 -m venv "$VENV" || die "could not create venv (sudo apt install python3-venv)"
fi
info "installing iecpoc frontend into $VENV"
"$VENV/bin/pip" install --quiet --disable-pip-version-check . \
|| die "pip install failed (needs network for the build backend, or: sudo apt install python3-pip)"
ok "frontend installed"
fi
# ---- 2. pick the module matching the running kernel ----------------------
# Same vermagic-based selection as selftest.sh: match by what the .ko was built
# against (uname -r), not by filename. See selftest.sh for the full rationale.
RUNNING="$(uname -r)"
if [ -z "$KO" ]; then
info "running kernel: $RUNNING"
for dir in "$HERE/modules" "$HERE" "$HERE/kernel"; do
[ -d "$dir" ] || continue
for cand in "$dir"/${MODULE}*.ko; do
[ -f "$cand" ] || continue
vm="$(modinfo -F vermagic "$cand" 2>/dev/null)" && [ -n "$vm" ] || continue
if [ "${vm%% *}" = "$RUNNING" ]; then KO="$cand"; break 2; fi
done
done
fi
[ -n "$KO" ] && [ -f "$KO" ] || die \
"no module matching the running kernel ($RUNNING) found.
Pass one with --ko PATH, or run ./selftest.sh to list what's available."
VERMAGIC="$(modinfo -F vermagic "$KO" 2>/dev/null)" || die "cannot read vermagic from $KO"
[ "${VERMAGIC%% *}" = "$RUNNING" ] || die \
"kernel mismatch: module built for '${VERMAGIC%% *}' but running '$RUNNING'."
info "module: $KO"
# ---- 3. 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 ${DEVICE} in use?)"
fi
info "insmod ${MODULE} address=${ADDRESS}"
insmod "$KO" address="$ADDRESS" || die "insmod failed; check: dmesg | tail"
LOADED=1
[ -c "$DEVICE" ] || die "${DEVICE} was not created"
ok "loaded; ${DEVICE} present"
# ---- 4. run the frontend (foreground; rmmod runs in the EXIT trap) -------
info "running iecpoc (Ctrl-C to stop) - address ${ADDRESS}, device ${DEVICE}"
echo
set +e
"$VENV/bin/iecpoc" --address "$ADDRESS" --device "$DEVICE" "${PASS[@]}"
ST=$?
set -e
echo
info "frontend exited (status $ST); unloading module"
exit "$ST"