Compare commits

...

11 Commits

Author SHA1 Message Date
a79a469aac Merge pull request 'fix-build' (#4) from fix-build into master
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (push) Successful in 1m9s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (push) Successful in 1m8s
Build kernel module / package (push) Successful in 33s
Build kernel module / release (push) Successful in 20s
Reviewed-on: #4
2026-06-20 05:51:52 +02:00
27ff299f43 feat(kernel): Upload package tree instead of pre-zipped artifact
All checks were successful
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 49s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 54s
Build kernel module / package (pull_request) Successful in 23s
Build kernel module / release (pull_request) Has been skipped
Switch to uploading the assembled package directory as the artifact, allowing the platform to encapsulate it in a single-layer zip. This avoids nested zips and ensures the release process reuses the same archive structure for consistency.
2026-06-20 05:42:55 +02:00
a5406f503c feat(kernel): Add PR packaging and artifact upload for testing
All checks were successful
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 49s
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 1m9s
Build kernel module / package (pull_request) Successful in 18s
Build kernel module / release (pull_request) Has been skipped
Enable packaging and artifact upload for pull requests to facilitate
testing. Package job generates a zip archive identical to release
artifacts, allowing smoke tests before final releases. Adjusted naming
for zip files to include PR reference or SHA for traceability.
2026-06-20 05:36:06 +02:00
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
2881fae5bf fix(kernel): Correct Docker tag sanitization for kernel versions
All checks were successful
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Successful in 52s
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Successful in 1m23s
Build kernel module / release (pull_request) Has been skipped
Ensure Docker image tags map unsupported characters `:`, `/`, and `+`
to `-` for compatibility. This prevents invalid tags in Docker builds
when kernel version strings include these characters.
2026-06-20 04:57:26 +02:00
f91b06da9d fix(kernel): Use per-version tags in Docker build to isolate jobs
Some checks failed
Build kernel module / build (1:6.12.93-1+rpt1, bookworm) (pull_request) Failing after 41s
Build kernel module / build (1:6.18.34-1+rpt1, trixie) (pull_request) Failing after 42s
Build kernel module / release (pull_request) Has been skipped
Prevent race conditions in concurrent job builds by introducing unique
Docker image tags per kernel version. This ensures isolated builds with
correct verm
2026-06-20 04:53:45 +02:00
2aa5104812 feat(kernel): Improve selftest.sh module discovery and error handling
Enhance module selection process by validating vermagic against the running kernel. Add detailed error messages with suggestions for manual path overrides or rebuilding for the current kernel. Ensure robust fallback behavior and improve user guidance.
2026-06-20 04:53:40 +02:00
6e5ec9b84f docs(kernel): Update README with module selection details
Expand documentation on selftest.sh behavior, module matching, and kernel compatibility. Clarify the auto-selection process based on `vermagic` and provide examples to guide users through fallback handling and manual overrides.
2026-06-20 04:53:31 +02:00
5 changed files with 349 additions and 20 deletions

View File

@ -51,23 +51,33 @@ jobs:
if [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then if [ ! -e /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then
docker run --privileged --rm tonistiigi/binfmt --install arm64 docker run --privileged --rm tonistiigi/binfmt --install arm64
fi fi
# Per-version image tag. The matrix entries can run concurrently on a
# single runner sharing one Docker daemon; a fixed tag (e.g. iec-kbuild)
# is then a shared mutable name and the builds race — whichever `docker
# build` finishes last wins the tag, so both `docker create` calls
# resolve to the same image and every job emits the same vermagic.
# A unique tag per kernel version isolates them. Docker tags allow only
# [a-zA-Z0-9._-], so map ':' '/' '+' (all present in 1:6.12.93-1+rpt1)
# to '-'; the tag just needs to be unique and valid, not reversible.
img="iec-kbuild:${KERNEL_VERSION//[:\/+]/-}"
# Builder image: toolchain + matching raspberrypi kernel headers. # Builder image: toolchain + matching raspberrypi kernel headers.
docker build --platform linux/arm64 \ docker build --platform linux/arm64 \
--build-arg DEBIAN_SUITE="$DEBIAN_SUITE" \ --build-arg DEBIAN_SUITE="$DEBIAN_SUITE" \
--build-arg HEADERS_PKG="$HEADERS_PKG" \ --build-arg HEADERS_PKG="$HEADERS_PKG" \
--build-arg KERNEL_VERSION="$KERNEL_VERSION" \ --build-arg KERNEL_VERSION="$KERNEL_VERSION" \
-t iec-kbuild . -t "$img" .
# Compile inside the container. We use `docker cp` instead of the bind # Compile inside the container. We use `docker cp` instead of the bind
# mount that build-in-docker.sh uses for local builds: under the runner's # mount that build-in-docker.sh uses for local builds: under the runner's
# docker-in-docker, /workspace is a volume the host daemon can't see, so # docker-in-docker, /workspace is a volume the host daemon can't see, so
# `-v "$PWD:/build"` mounts an empty dir and make finds no Makefile. # `-v "$PWD:/build"` mounts an empty dir and make finds no Makefile.
cid=$(docker create --platform linux/arm64 --entrypoint sleep iec-kbuild infinity) cid=$(docker create --platform linux/arm64 --entrypoint sleep "$img" infinity)
docker start "$cid" docker start "$cid"
docker cp ./. "$cid:/build" docker cp ./. "$cid:/build"
docker exec "$cid" /usr/local/bin/docker-entrypoint.sh clean docker exec "$cid" /usr/local/bin/docker-entrypoint.sh clean
docker exec "$cid" /usr/local/bin/docker-entrypoint.sh docker exec "$cid" /usr/local/bin/docker-entrypoint.sh
docker cp "$cid:/build/iec_listener.ko" ./iec_listener.ko docker cp "$cid:/build/iec_listener.ko" ./iec_listener.ko
docker rm -f "$cid" docker rm -f "$cid"
docker rmi "$img" || true
- name: Stage build output (modules/iec_listener_<kernel_version>.ko) - name: Stage build output (modules/iec_listener_<kernel_version>.ko)
working-directory: kernel working-directory: kernel
@ -89,12 +99,19 @@ jobs:
path: kernel/out path: kernel/out
if-no-files-found: error if-no-files-found: error
# Release flow only: merges all built modules into one modules/ folder, wraps it # Merge all built modules into one modules/ folder, wrap it in a single
# in a single repo-named top folder alongside selftest.sh, zips it and attaches # repo-named top folder alongside the helper scripts (selftest.sh, launch.sh)
# it to the Gitea release for the pushed tag. Skipped entirely on pull requests. # and the iecpoc Python frontend (so launch.sh can pip-install it on the Pi).
release: # Runs on BOTH triggers and uploads the assembled FOLDER as an artifact: on a PR
# this lets you download and smoke-test the exact tree a release would ship; on a
# tag the release job below zips this same tree and attaches it.
#
# We intentionally upload the folder, not a pre-made .zip: artifacts are always
# transported as a zip by the platform, so uploading a .zip would nest one zip
# inside another. Uploading the tree makes the platform's wrapper zip *be* the
# package archive (a single, clean layer).
package:
needs: build needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
@ -104,21 +121,67 @@ jobs:
uses: https://github.com/christopherHX/gitea-download-artifact@v4 uses: https://github.com/christopherHX/gitea-download-artifact@v4
with: with:
path: dist path: dist
# Merge every artifact back into the shared modules/ folder. # Merge every per-kernel artifact back into the shared modules/ folder.
merge-multiple: true merge-multiple: true
- name: Assemble release package - name: Determine version label
id: ver
run: |
# Tags ship under the tag name; PR builds get a traceable pr<n>-<sha>.
case "${{ github.ref }}" in
refs/tags/*) v="${{ github.ref_name }}" ;;
*) v="pr${{ github.event.pull_request.number }}-$(echo "${{ github.sha }}" | cut -c1-7)" ;;
esac
echo "ver=$v" >> "$GITHUB_OUTPUT"
- name: Assemble package tree
run: | run: |
repo="${{ github.event.repository.name }}" repo="${{ github.event.repository.name }}"
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
(cd package && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo") 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 {} +
- name: Upload package
uses: https://github.com/christopherHX/gitea-upload-artifact@v4
with:
name: ${{ github.event.repository.name }}-${{ steps.ver.outputs.ver }}
# Upload the contents of package/, so the artifact zip contains the
# single top folder package/<repo>/ -> <repo>/...
path: package
if-no-files-found: error
# Release flow only: download the assembled tree the package job uploaded, zip it
# into the named release asset and attach it to the Gitea release for the pushed
# tag. No re-assembly here (the tree is taken as-is). Skipped on PRs.
release:
needs: package
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
steps:
- name: Download package
uses: https://github.com/christopherHX/gitea-download-artifact@v4
with:
# The single artifact the package job produced for this tag; extracts to
# dist/<repo>/...
name: ${{ github.event.repository.name }}-${{ github.ref_name }}
path: dist
- name: Zip release asset
run: |
repo="${{ github.event.repository.name }}"
(cd dist && zip -r "../${repo}-${{ github.ref_name }}.zip" "$repo")
- name: Publish to Gitea release - name: Publish to Gitea release
uses: https://gitea.com/actions/gitea-release-action@v1.3.6 uses: https://gitea.com/actions/gitea-release-action@v1.3.6
with: with:
api_key: ${{ secrets.GITEA_TOKEN }} api_key: ${{ secrets.GITEA_TOKEN }}
files: |- files: |-
*.zip ${{ github.event.repository.name }}-${{ github.ref_name }}.zip

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

@ -20,14 +20,45 @@ before connecting the real bus (see PLAN.md §10).
### Running it ### Running it
`selftest.sh` is non-persistent (nothing is installed into `/lib/modules`, no `selftest.sh` is non-persistent (nothing is installed into `/lib/modules`, no
autoload): it checks vermagic, loads the module, runs the self-test, and always autoload): it detects the running kernel, picks the matching module, loads it,
unloads it again. runs the self-test, and always unloads it again.
A released package ships several kernel builds side by side, plus the launcher
and the Python frontend:
```
comodore-iec-emu/
├── 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/
├── 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)
```
`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
`modules/` (and a few fallback locations) and selects the `.ko` whose **vermagic**
matches the running kernel. Matching is by vermagic rather than filename because
the packaged name carries a Debian epoch/revision (`1:6.12.93-1+rpt1`) that
`uname -r` (`6.12.93+rpt-rpi-v8`) does not.
```bash ```bash
sudo ./selftest.sh # auto-finds ./ or ~/iec_listener.ko, address 4 sudo ./selftest.sh # auto-selects the matching module, address 4
sudo ./selftest.sh ~/iec_listener.ko --address 5 # runs with ~/iec_listener.ko, address 5 sudo ./selftest.sh --address 5 # same, address 5
sudo ./selftest.sh ~/iec_listener.ko --address 5 # force a specific module file
``` ```
If no module matches the running kernel, the script lists the modules it found
(with the kernel each was built for) and exits — rebuild for the current kernel,
or pass a path explicitly.
It exits non-zero unless the result is a full pass (`0x1F`), so it is usable in It exits non-zero unless the result is a full pass (`0x1F`), so it is usable in
scripts/CI. scripts/CI.

View File

@ -62,13 +62,66 @@ done
case "$ADDRESS" in ''|*[!0-9]*) die "address must be 0-30 (got '$ADDRESS')" ;; esac 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" { [ "$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_<kernel_version>.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 if [ -z "$KO" ]; then
for cand in "./${MODULE}.ko" "$HOME/${MODULE}.ko" "$(dirname "$0")/${MODULE}.ko"; do info "running kernel: $RUNNING"
[ -f "$cand" ] && { KO="$cand"; break; } 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 done
fi fi
[ -n "$KO" ] && [ -f "$KO" ] || die "module not found; pass the path, e.g. sudo $0 ~/${MODULE}.ko"
command -v modinfo >/dev/null || die "modinfo not found (install kmod)" 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 "module: $KO"
info "address: $ADDRESS" info "address: $ADDRESS"

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"