diff --git a/.gitignore b/.gitignore index e844850..323afe9 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,8 @@ __pycache__/ dist/ *.egg-info/ node_modules/ + +# Kernel release binaries — 32 MB. These are published as Gitea release assets, +# not committed. Rebuild them with kernel/build.sh; verify with SHA256SUMS. +kernel/release/*.img.gz +kernel/release/*.tar.gz diff --git a/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-400.woff2 b/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-400.woff2 new file mode 100644 index 0000000..2e43c90 Binary files /dev/null and b/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-400.woff2 differ diff --git a/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-700.woff2 b/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-700.woff2 new file mode 100644 index 0000000..bc31903 Binary files /dev/null and b/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-700.woff2 differ diff --git a/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-mono-400.woff2 b/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-mono-400.woff2 new file mode 100644 index 0000000..f5c1081 Binary files /dev/null and b/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-mono-400.woff2 differ diff --git a/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-mono-600.woff2 b/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-mono-600.woff2 new file mode 100644 index 0000000..f5c1081 Binary files /dev/null and b/dashboard/src/gpsntp_dashboard/static/fonts/atkinson-mono-600.woff2 differ diff --git a/dashboard/src/gpsntp_dashboard/static/fonts/baskerville-400.woff2 b/dashboard/src/gpsntp_dashboard/static/fonts/baskerville-400.woff2 new file mode 100644 index 0000000..2d44bee Binary files /dev/null and b/dashboard/src/gpsntp_dashboard/static/fonts/baskerville-400.woff2 differ diff --git a/dashboard/src/gpsntp_dashboard/static/fonts/baskerville-700.woff2 b/dashboard/src/gpsntp_dashboard/static/fonts/baskerville-700.woff2 new file mode 100644 index 0000000..2d44bee Binary files /dev/null and b/dashboard/src/gpsntp_dashboard/static/fonts/baskerville-700.woff2 differ diff --git a/dashboard/src/gpsntp_dashboard/static/fonts/fonts.css b/dashboard/src/gpsntp_dashboard/static/fonts/fonts.css new file mode 100644 index 0000000..75a4dc2 --- /dev/null +++ b/dashboard/src/gpsntp_dashboard/static/fonts/fonts.css @@ -0,0 +1,57 @@ +/* Self-hosted: this dashboard runs on a LAN and must work with no internet. + * Latin subset only — the latin-ext/greek/vietnamese blocks Google serves + * would triple the payload for glyphs a status page will never render. + * + * Atkinson Hyperlegible is the Braille Institute's legibility face. It is + * here for a functional reason, not a brand one: it disambiguates 0/O, 1/l/I + * and rn/m, which is exactly what you need when reading `+66 ns` or + * `2607:f740:e::d3` at a glance from across a room. + */ + +@font-face { + font-family: "Atkinson Hyperlegible"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/static/fonts/atkinson-400.woff2") format("woff2"); +} + +@font-face { + font-family: "Atkinson Hyperlegible"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("/static/fonts/atkinson-700.woff2") format("woff2"); +} + +@font-face { + font-family: "Atkinson Hyperlegible Mono"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/static/fonts/atkinson-mono-400.woff2") format("woff2"); +} + +@font-face { + font-family: "Atkinson Hyperlegible Mono"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("/static/fonts/atkinson-mono-600.woff2") format("woff2"); +} + +@font-face { + font-family: "Libre Baskerville"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("/static/fonts/baskerville-400.woff2") format("woff2"); +} + +@font-face { + font-family: "Libre Baskerville"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("/static/fonts/baskerville-700.woff2") format("woff2"); +} diff --git a/dashboard/src/gpsntp_dashboard/static/index.html b/dashboard/src/gpsntp_dashboard/static/index.html index 60508f5..0d8ef86 100644 --- a/dashboard/src/gpsntp_dashboard/static/index.html +++ b/dashboard/src/gpsntp_dashboard/static/index.html @@ -171,6 +171,13 @@
gpsntp-dashboard + + + How this clock works + +
diff --git a/dashboard/src/gpsntp_dashboard/static/style.css b/dashboard/src/gpsntp_dashboard/static/style.css index db2eef2..b7b918c 100644 --- a/dashboard/src/gpsntp_dashboard/static/style.css +++ b/dashboard/src/gpsntp_dashboard/static/style.css @@ -1,13 +1,34 @@ +@import url("/static/fonts/fonts.css"); + +/* Supported Systems house style, applied as WARM SHELL / COOL DATA. + * + * The chrome — background, panels, type, brand accent — comes from + * supported.systems: warm near-black, cream text, Libre Baskerville display, + * Atkinson Hyperlegible body, brass accent. + * + * The INSTRUMENTS do not. Cyan, green, amber and red stay exactly as they were, + * because on this page those colors carry information: which constellation a + * satellite belongs to, whether a source is selected, whether the clock is + * healthy. Recoloring them to brass would trade meaning for brand, which on a + * status page is a bad trade at any exchange rate. + * + * So: --brand is for chrome, --accent is for live data. They are different + * variables on purpose. Don't merge them. + */ :root { - --bg: #0a0e13; - --bg-2: #0e141c; - --panel: #121a23; - --panel-2: #16212d; - --border: #21303f; - --border-soft: #1a2632; - --text: #e7eef5; - --muted: #8a99a8; - --faint: #57687a; + /* shell — warm (supported.systems) */ + --bg: #171717; + --bg-2: #1b1a18; + --panel: #232120; + --panel-2: #2b2825; + --border: #3d382f; + --border-soft: #322e28; + --text: #eeece8; + --muted: #a8a196; + --faint: #6f6a61; + --brand: #d9a441; /* brass. chrome, links, brand marks. NOT data. */ + + /* instruments — cool (semantic; do not rebrand) */ --accent: #22d3ee; --good: #34d399; --warn: #f5a524; @@ -20,8 +41,15 @@ --c-sbas: #a3e635; --c-qzss: #2dd4bf; --c-other: #94a3b8; + --shadow: 0 1px 0 rgba(255, 255, 255, 0.03), 0 12px 30px -18px rgba(0, 0, 0, 0.9); - --mono: ui-monospace, "SF Mono", "JetBrains Mono", "Cascadia Code", Menlo, monospace; + + /* Atkinson Hyperlegible: the Braille Institute's legibility face. Chosen for + * function before brand — it disambiguates 0/O, 1/l/I, rn/m, which is exactly + * what a glanceable status page needs. */ + --sans: "Atkinson Hyperlegible", system-ui, -apple-system, sans-serif; + --serif: "Libre Baskerville", Georgia, serif; + --mono: "Atkinson Hyperlegible Mono", ui-monospace, "SF Mono", Menlo, monospace; } * { box-sizing: border-box; } @@ -29,11 +57,13 @@ body { margin: 0; background: - radial-gradient(1200px 600px at 80% -10%, rgba(34, 211, 238, 0.06), transparent 60%), - radial-gradient(900px 500px at 0% 0%, rgba(52, 211, 153, 0.05), transparent 55%), + /* the warm bench light from supported.systems */ + radial-gradient(1100px 620px at 50% -8%, rgba(217, 164, 65, 0.07), transparent 62%), + /* one cool wash, bottom-left, so the instruments feel like instruments */ + radial-gradient(900px 500px at 0% 100%, rgba(34, 211, 238, 0.04), transparent 58%), var(--bg); color: var(--text); - font: 15px/1.5 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + font: 15px/1.5 var(--sans); -webkit-font-smoothing: antialiased; min-height: 100vh; } @@ -47,12 +77,16 @@ svg { width: 1.15em; height: 1.15em; fill: none; stroke: currentColor; stroke-wi gap: 16px; padding: 16px 20px; border-bottom: 1px solid var(--border-soft); position: sticky; top: 0; z-index: 5; - background: linear-gradient(var(--bg), rgba(10, 14, 19, 0.86)); + background: linear-gradient(var(--bg), rgba(23, 23, 23, 0.86)); backdrop-filter: blur(8px); } .brand { display: flex; align-items: center; gap: 13px; } -.brand-icon { width: 30px; height: 30px; color: var(--accent); } -.brand h1 { font-size: 19px; margin: 0; letter-spacing: 0.3px; font-family: var(--mono); } +/* The dish is brand chrome, not a reading — brass, not cyan. */ +.brand-icon { width: 30px; height: 30px; color: var(--brand); } +/* Libre Baskerville, as on supported.systems. The wordmark is the one place a + serif belongs on this page; everything that reports a NUMBER stays mono. */ +.brand h1 { font-size: 20px; margin: 0; letter-spacing: 0.2px; font-family: var(--serif); + font-weight: 700; } .brand-sub { margin: 0; color: var(--muted); font-size: 12.5px; } .conn { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--muted); @@ -75,7 +109,7 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p .panel-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; } .panel-head h2 { font-size: 14px; margin: 0; color: var(--muted); font-weight: 600; display: flex; align-items: center; gap: 8px; text-transform: uppercase; letter-spacing: 0.6px; } -.panel-head h2 svg { color: var(--accent); } +.panel-head h2 svg { color: var(--brand); } /* ---------- hero ---------- */ .hero { display: flex; align-items: center; justify-content: space-between; gap: 24px; flex-wrap: wrap; } @@ -104,7 +138,7 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p .card { background: var(--panel); border: 1px solid var(--border-soft); border-radius: 14px; padding: 14px 15px; } .card-head { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.4px; } -.card-head svg { width: 15px; height: 15px; color: var(--accent); } +.card-head svg { width: 15px; height: 15px; color: var(--brand); } .card-value { font-family: var(--mono); font-size: 25px; font-weight: 650; margin: 7px 0 3px; letter-spacing: 0.3px; } .card-value.good { color: var(--good); } .card-value.warn { color: var(--warn); } @@ -179,8 +213,13 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p /* ---------- footer ---------- */ .foot { max-width: 1180px; margin: 0 auto; padding: 14px 20px 30px; } -.foot-meta { display: flex; justify-content: space-between; margin-top: 14px; - color: var(--faint); font-size: 12px; font-family: var(--mono); } +.foot-meta { display: flex; justify-content: space-between; align-items: center; gap: 12px; + margin-top: 14px; color: var(--faint); font-size: 12px; font-family: var(--mono); } +.foot-docs { display: inline-flex; align-items: center; gap: 5px; color: var(--faint); + text-decoration: none; transition: color .2s; } +.foot-docs:hover { color: var(--brand); } +.foot-docs svg { width: 13px; height: 13px; stroke-width: 1.5; transition: transform .2s; } +.foot-docs:hover svg { transform: translateX(2px); } /* ---------- the maker's plate ---------- * Clockmakers signed the backplate — the brass face only a repairer sees once @@ -199,7 +238,7 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p var(--panel); transition: color .2s, border-color .2s; } -.ss-plate__link:hover { color: var(--text); border-color: var(--accent); } +.ss-plate__link:hover { color: var(--text); border-color: var(--brand); } .ss-plate__logo { flex: 0 0 auto; width: 46px; height: auto; opacity: .85; transition: opacity .2s; } @@ -209,9 +248,9 @@ main { max-width: 1180px; margin: 0 auto; padding: 20px; display: grid; gap: 18p font-family: var(--mono); font-size: 12px; font-weight: 600; text-transform: uppercase; letter-spacing: .14em; } .ss-plate__body { display: block; font-size: 13px; line-height: 1.55; max-width: 62ch; } -.ss-plate__name { color: var(--accent); } +.ss-plate__name { color: var(--brand); } .ss-plate__cta { display: inline-flex; align-items: center; gap: 5px; margin-top: 8px; - font-size: 12px; color: var(--accent); } + font-size: 12px; color: var(--brand); } .ss-plate__cta svg { width: 14px; height: 14px; transition: transform .2s; } .ss-plate__link:hover .ss-plate__cta svg { transform: translateX(2px); } diff --git a/docs-site/Makefile b/docs-site/Makefile index 1d77058..fbc9515 100644 --- a/docs-site/Makefile +++ b/docs-site/Makefile @@ -56,4 +56,4 @@ deploy: ## Pull main + rebuild the docs container on the prod host @echo "==> deploying $(DEPLOY_HOST):$(DEPLOY_PATH)" ssh -A $(DEPLOY_HOST) "cd $(DEPLOY_PATH) && git fetch origin main && git reset --hard origin/main && cd docs-site && make prod" @echo "==> sanity check" - @curl -s -o /dev/null -w " HTTP %{http_code} %{url_effective}\n" "https://cuckoo.warehack.ing/explanation/bug-detection/" + @curl -s -o /dev/null -w " HTTP %{http_code} %{url_effective}\n" "https://cuckoo.warehack.ing/explanation/preempt-rt-made-it-worse/" diff --git a/docs-site/src/content/docs/reference/downloads.md b/docs-site/src/content/docs/reference/downloads.md index fb4a24a..2e105aa 100644 --- a/docs-site/src/content/docs/reference/downloads.md +++ b/docs-site/src/content/docs/reference/downloads.md @@ -1,6 +1,6 @@ --- title: Downloads — prebuilt RT kernel -description: A patched PREEMPT_RT kernel for the Raspberry Pi 4, so you don't need a cross-compile toolchain. +description: A patched PREEMPT_RT kernel for the Raspberry Pi 4, boot-tested and benchmarked, so you don't need a cross-compile toolchain. sidebar: order: 5 --- @@ -9,28 +9,37 @@ The only expensive part of [the patch](/reference/the-patch/) is the toolchain. Building natively on a Pi 4 takes hours; cross-compiling needs an x86 box and a setup session. So here's the artifact. +**Current release: `6.18.38-rt-cuckoo+`** — Raspberry Pi's `rpi-6.18.y`, +`bcm2711_defconfig`, `PREEMPT_RT`, plus the `IRQF_NO_THREAD` patch. + :::danger[Read this before you download] -- **Raspberry Pi 4 / arm64 only.** `bcm2711_defconfig`. It will not boot a Pi 5 or - a Pi 3. -- **Unsigned, community-built.** We built this on a workstation. There is no chain - of trust here beyond "we published the exact recipe and the checksums." If that - isn't good enough for your environment — and for some environments it correctly - isn't — [build it yourself](/how-to/cross-compile-rt-kernel/). It's forty - minutes. -- **Verify the checksums.** They're in `SHA256SUMS`. +- **Raspberry Pi 4 / arm64 only.** The installer refuses to run on anything else, + because a `bcm2711` kernel on a Pi 5 doesn't boot — and a black screen on a + headless box is exactly the outcome we're trying to prevent. +- **Unsigned, community-built.** There is no chain of trust here beyond published + checksums and a reproducible recipe. For some environments that is correctly + not good enough. [Build it yourself](/how-to/cross-compile-rt-kernel/) — it's + forty minutes, and `kernel/build.sh` in the repo does the whole thing. +- **Verify the checksums.** The installer does it for you if `SHA256SUMS` is + present. Don't skip it. ::: ## Artifacts -Published on the [releases page](https://git.supported.systems/warehack.ing/cuckoo-escapement/releases): +On the [releases page](https://git.supported.systems/warehack.ing/cuckoo-escapement/releases): -| File | What | +| File | | |---|---| -| `kernel-rt-.img.gz` | The kernel image, gzipped (Pi OS's own format) | -| `rt-modules-.tar.gz` | Matching modules — **must** be installed with the image | -| `install-rt-kernel.sh` | Installer. Adds a *new* image, never replaces `kernel8.img` | +| `kernel-rt-6.18.38-rt-cuckoo+.img.gz` | The kernel, gzipped (Pi OS's own format) — 9.8 MB | +| `rt-modules-6.18.38-rt-cuckoo+.tar.gz` | Matching modules — 22 MB. **Must** be installed with the image. | +| `install-rt-kernel.sh` | Installer. Adds a *new* image; never replaces `kernel8.img`. | | `SHA256SUMS` | Checksums | +``` +97533ea81ec56ef090908560e88bccfb2a3f7dfcc3d33a5a1cbe4bbf15163b55 kernel-rt-6.18.38-rt-cuckoo+.img.gz +6af126711eef2ae1651ac48e19353e6830f12fb0ef9a7e265f54c27206a4f0eb rt-modules-6.18.38-rt-cuckoo+.tar.gz +``` + ## Install ```bash @@ -39,34 +48,68 @@ sudo ./install-rt-kernel.sh sudo reboot ``` -The installer: +Then confirm the patch is actually live. This is the check that matters: -1. Untars the modules into `/lib/modules/` -2. Writes the image as `/boot/firmware/kernel-rt.img` — **`kernel8.img` is left - alone** -3. Appends one line, `kernel=kernel-rt.img`, to `config.txt` +```console +$ uname -a +Linux … 6.18.38-rt-cuckoo+ #1 SMP PREEMPT_RT … aarch64 -**Rollback is deleting that one line.** Mount the SD card's FAT partition on any -machine, remove it, and the stock kernel boots. That's deliberate: you should never -have to make a physical trip to a headless box because of a kernel you got from a -website. - -## What's in it - -Raspberry Pi's `rpi-6.12.y` tree, `bcm2711_defconfig`, plus exactly two changes: - -```bash -scripts/config --enable PREEMPT_RT -# + the IRQF_NO_THREAD patch in drivers/pps/clients/pps-gpio.c +$ ps -eo comm | grep irq/.*pps +(nothing) ``` -Nothing else. The full recipe is in -[Cross-compile an RT kernel](/how-to/cross-compile-rt-kernel/), and you should be -able to reproduce this byte-for-byte modulo build timestamps. +**That empty output is the whole point.** If an `irq/NN-pps@…` thread appears, +the handler is being force-threaded and [your timestamp is being taken behind the +scheduler](/explanation/preempt-rt-made-it-worse/). + +## Rollback is one line + +The installer appends exactly one line to `config.txt` and never touches +`kernel8.img`. If the Pi doesn't come back: pull the SD card, open `config.txt` +on any machine (it's a FAT partition — a Windows laptop will do), delete the +`kernel=kernel-rt.img` line, put the card back. The stock kernel boots. + +It also leaves a `config.txt.before-rt` next to it. + +## What we actually tested + +Not "it compiled." This exact artifact, installed by that exact script, on a Pi 4: + +| | | +|---|---| +| Boots | yes | +| `uname` | `6.18.38-rt-cuckoo+ SMP PREEMPT_RT` | +| PPS handler threaded | **no** — patch confirmed live | +| chrony | Stratum 1, ref PPS | +| System offset | 273 ns | +| Raw PPS jitter | 952 ns σ over 62 s | + +:::note[One window is not a benchmark] +That 952 ns is a single 62-second sample, on a fresh boot under a clear sky, and +it is *better* than the 2568 ns in [the measurements](/reference/measurements/). +We report it because it's what we saw — not as a revision of the headline +numbers. Those came from an [A/B/A protocol](/how-to/benchmark-pps-jitter/), and +one lucky window doesn't overturn them. Sky conditions move this figure around +more than most software changes do. Measure your own board. +::: + +## Reproducing it + +```bash +git clone https://git.supported.systems/warehack.ing/cuckoo-escapement +cd cuckoo-escapement/kernel && ./build.sh +``` + +`KBUILD_BUILD_USER`, `KBUILD_BUILD_HOST` and `KBUILD_BUILD_TIMESTAMP` are pinned +in that script. Without them a kernel bakes the builder's `user@hostname` and the +wall-clock build time into `/proc/version` — which publishes whoever built it and +makes the output unreproducible. Pinned, you should be able to rebuild this and +compare bytes. For an unsigned kernel from a website, that's the only trust story +we can honestly offer. :::note[Pinned to a tested version] -The published download always points at a kernel we have **actually booted and -benchmarked** on a Pi 4 — not simply the newest upstream. Shipping a stranger an -unvalidated kernel for a machine they may not be able to physically reach is not -something we're willing to do. +The published download always points at a kernel we have **booted and benchmarked +on a Pi 4** — never simply the newest upstream. Shipping a stranger an unvalidated +kernel, for a machine they may not be able to physically reach, is not something +we're willing to do. ::: diff --git a/kernel/build.sh b/kernel/build.sh new file mode 100755 index 0000000..48a7b24 --- /dev/null +++ b/kernel/build.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Reproducible RT kernel for the Cuckoo Escapement release. +# +# KBUILD_BUILD_{USER,HOST,TIMESTAMP} are the whole point of this rebuild: without +# them the kernel bakes the builder's `user@hostname` and the wall-clock build +# time into /proc/version, which (a) publishes operator identity and (b) makes +# the artifact unreproducible. Pinning all three means anyone can rebuild this +# and diff the bytes. +set -euo pipefail +cd "$HOME/src" + +PATCH="$(cd "$(dirname "$0")" && pwd)/0001-pps-gpio-keep-timestamp-in-hard-irq-under-PREEMPT_RT.patch" + +[ -d linux ] || git clone --depth=1 --branch rpi-6.18.y https://github.com/raspberrypi/linux +cd linux + +export ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu- +export KBUILD_BUILD_USER=cuckoo +export KBUILD_BUILD_HOST=escapement +export KBUILD_BUILD_TIMESTAMP='Mon Jan 1 00:00:00 UTC 2024' + +make bcm2711_defconfig +./scripts/config --enable EXPERT --enable PREEMPT_RT \ + --set-str LOCALVERSION "-rt-cuckoo" --disable LOCALVERSION_AUTO +make olddefconfig + +git apply --check "$PATCH" 2>/dev/null && git apply "$PATCH" && echo "PATCH APPLIED" \ + || grep -q IRQF_NO_THREAD drivers/pps/clients/pps-gpio.c && echo "PATCH ALREADY PRESENT" + +grep -q "IRQF_NO_THREAD" drivers/pps/clients/pps-gpio.c || { echo "FATAL: patch missing"; exit 1; } + +make -j"$(nproc)" Image modules dtbs +echo "BUILD OK: $(cat include/config/kernel.release)" diff --git a/kernel/release/SHA256SUMS b/kernel/release/SHA256SUMS new file mode 100644 index 0000000..3ab37e8 --- /dev/null +++ b/kernel/release/SHA256SUMS @@ -0,0 +1,2 @@ +97533ea81ec56ef090908560e88bccfb2a3f7dfcc3d33a5a1cbe4bbf15163b55 kernel-rt-6.18.38-rt-cuckoo+.img.gz +6af126711eef2ae1651ac48e19353e6830f12fb0ef9a7e265f54c27206a4f0eb rt-modules-6.18.38-rt-cuckoo+.tar.gz diff --git a/kernel/release/install-rt-kernel.sh b/kernel/release/install-rt-kernel.sh new file mode 100755 index 0000000..57b3bd7 --- /dev/null +++ b/kernel/release/install-rt-kernel.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash +# +# Install the Cuckoo Escapement PREEMPT_RT kernel on a Raspberry Pi 4. +# +# The design goal here is ONE THING: you must never need physical access to the +# SD card because of something this script did. So: +# +# - kernel8.img (the stock kernel) is never touched +# - the RT kernel installs under a NEW filename +# - the ONLY change to config.txt is a single appended line +# +# If it doesn't boot: pull the card, open config.txt on any machine (it's a FAT +# partition, so a Windows laptop will do), delete that one line, put it back. +# The stock kernel returns. That's the entire rollback. +# +# Run it FROM the directory holding the release files. +set -euo pipefail + +IMG_NAME="kernel-rt.img" +BOOT="/boot/firmware" + +die() { printf '\nERROR: %s\n' "$1" >&2; exit 1; } + +[[ $EUID -eq 0 ]] || die "run with sudo" + +# --- Refuse to run on hardware this kernel was not built for. A bcm2711 kernel +# --- on a Pi 5 does not boot, and the failure mode is a black screen on a +# --- headless box, which is exactly what we promised wouldn't happen. +MODEL="$(tr -d '\0' < /proc/device-tree/model 2>/dev/null || echo unknown)" +case "$MODEL" in + *"Raspberry Pi 4"*|*"Raspberry Pi Compute Module 4"*|*"Raspberry Pi 400"*) ;; + *) die "this kernel is bcm2711 (Pi 4) only — found: $MODEL" ;; +esac + +[[ "$(uname -m)" == "aarch64" ]] || die "need a 64-bit OS (uname -m says $(uname -m))" +[[ -d "$BOOT" ]] || die "$BOOT not found — is this Raspberry Pi OS?" + +IMG_GZ="$(ls -1 kernel-rt-*.img.gz 2>/dev/null | head -1)" || true +MODS="$(ls -1 rt-modules-*.tar.gz 2>/dev/null | head -1)" || true +[[ -n "${IMG_GZ:-}" && -n "${MODS:-}" ]] || die "run this from the directory containing the release files" + +# --- The image and the modules MUST come from the same build. A mismatched pair +# --- means modprobe fails on vermagic — and since pps-gpio is a module, that +# --- means no /dev/pps0 and no refclock, on a kernel that otherwise boots fine. +# --- Silent, and miserable to debug. Check it up front. +IMG_VER="${IMG_GZ#kernel-rt-}"; IMG_VER="${IMG_VER%.img.gz}" +MOD_VER="${MODS#rt-modules-}"; MOD_VER="${MOD_VER%.tar.gz}" +[[ "$IMG_VER" == "$MOD_VER" ]] \ + || die "image ($IMG_VER) and modules ($MOD_VER) are from different builds" + +if [[ -f SHA256SUMS ]]; then + echo "==> verifying checksums" + sha256sum -c SHA256SUMS || die "checksum mismatch — do not install this" +else + echo "!! no SHA256SUMS present — skipping verification (not recommended)" +fi + +echo "==> installing modules for $MOD_VER" +tar -C / -xzf "$MODS" +depmod -a "$MOD_VER" + +echo "==> installing kernel as $BOOT/$IMG_NAME (kernel8.img left alone)" +zcat "$IMG_GZ" > "$BOOT/$IMG_NAME" + +if grep -qE "^kernel=$IMG_NAME" "$BOOT/config.txt"; then + echo "==> config.txt already selects $IMG_NAME" +else + cp "$BOOT/config.txt" "$BOOT/config.txt.before-rt" + printf '\n# Cuckoo Escapement RT kernel. DELETE THIS LINE to boot the stock kernel.\nkernel=%s\n' \ + "$IMG_NAME" >> "$BOOT/config.txt" + echo "==> appended 'kernel=$IMG_NAME' to config.txt (backup: config.txt.before-rt)" +fi + +cat <