Refactor first-boot documentation and scripts to enhance Chromium startup behavior. Update start-chromium.sh to prefer Wayland for better touch support and adjust fullscreen handling for both Wayland and X11 environments. Clarify CM4 EEPROM configuration in documentation to prevent conflicts with reTerminal DM display backlight. Improve user guidance on touch interactions in Chromium.

This commit is contained in:
nearxos
2026-02-20 16:52:26 +02:00
parent 58d9144752
commit 66ad3b0a39
8 changed files with 481 additions and 32 deletions

View File

@@ -17,7 +17,7 @@ This script runs once on first boot via cloud-init (see `user-data-remote-gnss.e
9. **reTerminal DM drivers** — Seeed repo clone and `reTerminal.sh`.
10. **Re-apply splash** — Set `disable_splash=0`, Plymouth theme to `custom` only, `update-initramfs`.
11. **Dark theme** — Set GTK dark theme for user `pi`: `~/.config/gtk-3.0/settings.ini` with `gtk-application-prefer-dark-theme=1` and `gtk-theme-name=PiXnoir` (Raspberry Pi OS dark theme).
12. **CM4 EEPROM enable** — On CM4, `rpi-eeprom-update` is disabled by default. First-boot enables it by: adding `RPI_EEPROM_USE_FLASHROM=1` and `CM4_ENABLE_RPI_EEPROM_UPDATE=1` to `/etc/default/rpi-eeprom-update`; adding a `[cm4]` block to `config.txt` with `dtparam=spi=on`, `dtoverlay=audremap`, `dtoverlay=spi-gpio40-45`. After reboot, `rpi-eeprom-update -l` works and boot order can be set.
12. **CM4 EEPROM enable** — On CM4, `rpi-eeprom-update` is disabled by default. First-boot enables it by adding `RPI_EEPROM_USE_FLASHROM=1` and `CM4_ENABLE_RPI_EEPROM_UPDATE=1` to `/etc/default/rpi-eeprom-update`. **No config.txt changes are needed** `dtoverlay=audremap`/`dtoverlay=spi-gpio40-45` are for the flashrom method only and **must not be added** as they conflict with the reTerminal DM display backlight (GPIO13 PWM). The bootloader method (`pieeprom.upd`) is used instead.
13. **Boot order** — If `rpi-eeprom-config` is available, set `BOOT_ORDER=0x21` (network first, then eMMC/SD). On CM4 first boot this may be skipped (EEPROM not yet enabled); a one-shot systemd service runs after reboot to set boot order once.
14. **One-shots** — Download `set-rotation-once.sh` + `.desktop` from file server (wlr-randr for labwc). Wallpaper is set once via pcmanfm config during first-boot.
15. **Reboot.**
@@ -70,11 +70,13 @@ Creates `/home/pi/.config/autostart` so that `.desktop` files placed there are s
Downloads from `FILE_SERVER` (no local creation):
- **`FILE_SERVER`** — Base URL for first-boot assets (default: `http://10.130.60.141:5000/files/first-boot`). All first-boot files are served from a **first-boot** subfolder on the file server. Change this if your server or path is different.
- **`start-chromium.sh`** — Downloaded to `/home/pi/start-chromium.sh`, made executable (755), owned by `pi`. This script waits for the desktop, starts Chromium in kiosk mode (e.g. `--app=...`), and uses `wmctrl` to force fullscreen.
- **`start-chromium.sh`** — Downloaded to `/home/pi/start-chromium.sh`, made executable (755), owned by `pi`. Waits for the desktop, then starts Chromium in fullscreen. When the session is Wayland (rpd-labwc), Chromium runs with `--ozone-platform=wayland` so **touch long-press produces right-click (context menu)** like the rest of the desktop; on X11 it falls back to `--ozone-platform=x11` and uses `wmctrl` to force fullscreen.
- **`chromium-kiosk.desktop`** — Downloaded to `/home/pi/.config/autostart/chromium-kiosk.desktop`, mode 644, owned by `pi`. This autostart entry runs `start-chromium.sh` when `pi` logs in.
Ensure the `.desktop` file on the server has `Exec=/home/pi/start-chromium.sh` (or the path you use on the device).
**Touch in Chromium:** Long-press on the touchscreen to open the context menu (right-click). This works when Chromium runs as a Wayland client (default under rpd-labwc). If you ever run under pure X11, long-press may not trigger the context menu; in that case you can use **evdev-right-click-emulation** (see e.g. [evdev-right-click-emulation](https://github.com/PeterCxy/evdev-right-click-emulation)) to inject right-click on long-press at the input layer.
---
## Boot splash and wallpaper (single image from file server)
@@ -134,7 +136,7 @@ First-boot sets a dark GTK theme for user **pi** via **`~/.config/gtk-3.0/settin
## CM4: enable rpi-eeprom-update (for boot order)
On **CM4**, first-boot enables `rpi-eeprom-update` by: (1) **`/etc/default/rpi-eeprom-update`**: **`RPI_EEPROM_USE_FLASHROM=1`**, **`CM4_ENABLE_RPI_EEPROM_UPDATE=1`**; (2) **config.txt** **`[cm4]`** block: **`dtparam=spi=on`**, **`dtoverlay=audremap`**, **`dtoverlay=spi-gpio40-45`**. After reboot, **`rpi-eeprom-update -l`** works. See: [usbboot](https://github.com/raspberrypi/usbboot/blob/master/Readme.md).
On **CM4**, first-boot enables `rpi-eeprom-update` by setting **`RPI_EEPROM_USE_FLASHROM=1`** and **`CM4_ENABLE_RPI_EEPROM_UPDATE=1`** in **`/etc/default/rpi-eeprom-update`**. **No dtparams are added to config.txt.** `dtoverlay=audremap` and `dtoverlay=spi-gpio40-45` are only needed for the *flashrom* (direct SPI) update method — they **must not** be added because `audremap` remaps audio to GPIO12/13, which conflicts with the reTerminal DM display backlight PWM on GPIO13, causing a blank screen. The bootloader file method (`pieeprom.upd`) works without these overlays. See: [usbboot](https://github.com/raspberrypi/usbboot/blob/master/Readme.md).
## Boot order (network first, then eMMC/SD)

View File

@@ -1,35 +1,42 @@
#!/bin/bash
# Disable keyring prompts
export GNOME_KEYRING_CONTROL=""
export DISPLAY=:0
# Force X11 instead of Wayland for better fullscreen support
# Prefer Wayland when available so touch long-press produces right-click (context menu)
# like the rest of the desktop. X11/XWayland does not get that behavior for Chromium.
USE_WAYLAND=0
if [ -n "$WAYLAND_DISPLAY" ] && [ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$WAYLAND_DISPLAY" ]; then
USE_WAYLAND=1
fi
if [ "$USE_WAYLAND" -eq 1 ]; then
# Native Wayland: fullscreen + touch-friendly (long-press = right-click)
export GDK_BACKEND=wayland
# Wait for compositor
for i in {1..60}; do
if [ -S "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/$WAYLAND_DISPLAY" ]; then
pgrep -x labwc >/dev/null 2>&1 && break
fi
sleep 0.5
done
sleep 3
/usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=wayland --enable-features=WaylandWindowDecorations --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 &
else
# Fallback: X11 (e.g. no Wayland session)
export DISPLAY=:0
export GDK_BACKEND=x11
unset WAYLAND_DISPLAY
# Wait for display and desktop environment to be ready
# Check if DISPLAY is accessible (wait up to 30 seconds)
for i in {1..60}; do
if xset q >/dev/null 2>&1 || [ -n "$DISPLAY" ]; then
# Wait for desktop environment to be fully loaded
if pgrep -x pcmanfm >/dev/null 2>&1 || pgrep -x lxsession >/dev/null 2>&1 || pgrep -x xfdesktop >/dev/null 2>&1; then
break
fi
fi
sleep 0.5
done
# Additional delay to ensure window manager is fully ready
sleep 5
# Start Chromium with flags to avoid keyring and ensure proper fullscreen
# Force X11 platform and add fullscreen-related flags
# Fullscreen mode (current active)
/usr/bin/chromium --start-fullscreen --noerrdialogs --disable-infobars --disable-session-crashed-bubble --disable-restore-session-state --no-first-run --password-store=basic --use-mock-keychain --ozone-platform=x11 --disable-features=UseChromeOSDirectVideoDecoder --app=http://127.0.0.1:8080 &
# Wait for Chromium window to appear and then force fullscreen
sleep 3
# Try to find Chromium window and force it to fullscreen
for i in {1..10}; do
WINDOW_ID=$(wmctrl -l 2>/dev/null | grep -i chromium | head -1 | awk '{print $1}')
if [ -n "$WINDOW_ID" ]; then
@@ -38,6 +45,7 @@ for i in {1..10}; do
fi
sleep 0.5
done
fi

View File

@@ -0,0 +1,97 @@
# How network boot deployment works
This describes the full flow from power-on to eMMC deploy/backup when using **network boot** with the provisioning LXC.
---
## Overview
1. **reTerminal** is set to try **network boot first** (EEPROM `BOOT_ORDER=0x21`).
2. It is connected to the **same LAN as the LXCs eth1** (e.g. 10.20.50.0/24).
3. On power-on it gets an IP via **DHCP** and loads **boot files via TFTP** from the LXC.
4. The **netboot environment** (kernel + rootfs) runs **provisioning-client.sh**, which registers with the **dashboard** and polls for an action.
5. In the **dashboard** you see the device under “Device detected (Network)” and choose **Deploy** or **Backup**.
6. The device performs the action (download image → write eMMC, or read eMMC → upload), then you can reboot to run from eMMC.
---
## Step-by-step
### 1. LXC (provisioning server)
- **eth0** = WAN (e.g. 10.130.60.141), internet for the LXC.
- **eth1** = LAN (e.g. 10.20.50.1/24):
- **dnsmasq**: DHCP on eth1 (e.g. 10.20.50.100200) and **TFTP** with next-server = 10.20.50.1, boot file = `start4cd.elf`.
- **TFTP root** `/srv/tftpboot`: Raspberry Pi 4/CM4 boot files (from GitHub: start4cd.elf, fixup4cd.dat, kernel8.img, etc.).
- **NAT**: traffic from 10.20.50.0/24 is masqueraded out eth0 so netbooted devices have internet if needed.
The **dashboard** (Flask) runs in the LXC and is reachable at e.g. `http://10.20.50.1:5000` from the LAN. The **golden image** for Deploy lives at `/var/lib/cm4-provisioning/golden.img` (same LXC or bind-mounted from host).
### 2. reTerminal (device)
- **EEPROM**: `BOOT_ORDER=0x21` (network first, then SD/eMMC). Can be set by cloud-init first-boot on an already-flashed device.
- **Network**: Ethernet connected to the same segment as the LXCs **eth1** (e.g. same switch/VLAN as 10.20.50.0/24).
- On **power-on**:
1. Pi 4/CM4 firmware does **DHCP** on the wired interface.
2. DHCP reply gives: IP (e.g. 10.20.50.100), **next-server (TFTP)** = 10.20.50.1, **boot filename** = start4cd.elf.
3. Device **TFTP**s boot files from the LXC (start4cd.elf, fixup4cd.dat, kernel, DTB, etc.).
4. It boots the **kernel** (and optionally an initramfs or NFS root). That environment must have **network**, **curl**, and **provisioning-client.sh**.
### 3. Netboot root / environment
The **TFTP**-loaded kernel (and optional initramfs/NFS root) must end up in an environment where:
- The device has an IP on the same LAN as the LXC (already from DHCP).
- **provisioning-client.sh** is present and run (e.g. from init, a login script, or a systemd service).
- **PROVISIONING_SERVER** is set to the dashboard URL on the LXCs LAN IP, e.g.
`PROVISIONING_SERVER=http://10.20.50.1:5000`
So the “netboot environment” is either:
- A **custom initramfs** (recommended): build with **network-boot-initramfs/build.sh**, copy **initrd.img** to the TFTP root, and add `initramfs initrd.img followkernel` to **config.txt**. The initramfs brings up the network and runs the provisioning client. See **network-boot-initramfs/README.md**.
- A **minimal rootfs** (e.g. NFS) that runs the client script at boot, or
- Any other setup that gets the client running with network and the right `PROVISIONING_SERVER`.
### 4. Provisioning client (on the device)
- **provisioning-client.sh**:
1. **Registers**: `POST /api/register-device` with MAC and IP.
2. **Polls**: `GET /api/device-action-poll?mac=...` every few seconds.
3. When the dashboard returns **action = deploy** (with **url**):
downloads the image from **url** and runs `dd of=/dev/mmcblk0`.
4. When the dashboard returns **action = backup** (with **upload_url**):
runs `dd if=/dev/mmcblk0` and POSTs the stream to **upload_url**.
5. Then exits (and you can reboot to eMMC after deploy).
### 5. Dashboard (your actions)
- You open the dashboard at `http://10.20.50.1:5000` (or the LXCs WAN IP if youre not on the provisioning LAN).
- Under **“Device detected (Network)”** you see the device (identified by MAC).
- You click **Deploy** or **Backup**.
- The dashboard sets the **action** (and URL/upload_url) for that MAC; the next **device-action-poll** returns it, and the client runs the corresponding dd + curl.
---
## Data flow summary
| Stage | Where | What happens |
|-------------|--------------|--------------|
| Boot | reTerminal | DHCP (get IP + next-server + boot file), then TFTP (load start4cd.elf, kernel, etc.). |
| Boot | reTerminal | Kernel (and netboot root) start; run **provisioning-client.sh** with `PROVISIONING_SERVER=http://10.20.50.1:5000`. |
| Register | Device → LXC | POST /api/register-device (MAC, IP). |
| Poll | Device → LXC | GET /api/device-action-poll?mac=... every 5 s. |
| Your choice | You → LXC | In dashboard: click Deploy or Backup for that device. |
| Deploy | LXC → device | Client GETs image URL, streams to `dd of=/dev/mmcblk0`. |
| Backup | Device → LXC | Client `dd if=/dev/mmcblk0` and POSTs to upload_url. |
| After | reTerminal | Reboot; if you deployed, it can now boot from eMMC. |
---
## What you need in place
- **LXC**: eth1 = 10.20.50.1/24, dnsmasq (DHCP + TFTP on eth1), `/srv/tftpboot` with RPi 4 boot files, NAT for 10.20.50.0/24 via eth0. Dashboard running, `golden.img` present for Deploy.
See **NETWORK-BOOT-LXC.md** and **setup-network-boot-on-lxc.sh**.
- **reTerminal**: EEPROM boot order = network first; Ethernet on 10.20.50.0/24; netboot environment that runs **provisioning-client.sh** with `PROVISIONING_SERVER=http://10.20.50.1:5000`.
- **Netboot root**: Must provide network, curl, and the client script (NFS, initramfs, or custom root).
The **TFTP** setup only gets the Pi to boot a kernel (and optional root). The **provisioning** (Deploy/Backup) is done by that kernels environment running the **network-client** against the dashboard on the LXC.

View File

@@ -0,0 +1,77 @@
# Provisioning initramfs for network boot
Minimal initramfs that runs **provisioning-client.sh** after bringing up the network. Used with Raspberry Pi 4 / CM4 (reTerminal) when booting via TFTP from the provisioning LXC.
## What it does
1. Mounts `/proc`, `/sys`, `/dev`, `/dev/pts`.
2. Ensures an IP (reuses kernel DHCP or runs `udhcpc` on eth0).
3. Runs the provisioning client with `PROVISIONING_SERVER` (default `http://10.20.50.1:5000`, overridable via kernel cmdline).
4. The client registers with the dashboard and polls for **Deploy** or **Backup**; on action it performs the dd + curl and exits.
## Build
**On x86_64 (e.g. your laptop):** the script uses **Podman** or **Docker** with `--platform linux/arm64` to run an arm64 container and copy busybox + curl into the initramfs. Your host must be able to *run* arm64 containers (via QEMU emulation).
- **Fedora:** one-time setup to enable arm64 containers:
```bash
sudo dnf install -y qemu-user-static
```
Then run the build (Podman will use QEMU automatically):
```bash
cd emmc-provisioning/network-boot-initramfs
./build.sh
```
- If you dont install `qemu-user-static`, the script will fail with an error and print the same instructions and an alternative (build on a Pi).
**On a Raspberry Pi 4 or other aarch64 host:** no Docker. Install deps and run:
```bash
sudo apt install -y busybox curl
./build.sh
```
Optional: pass an output path:
```bash
./build.sh /path/to/initrd.img
```
## Deploy to TFTP root
1. Copy **initrd.img** to the LXC TFTP root (e.g. `/srv/tftpboot`):
```bash
scp initrd.img root@10.130.60.141:/srv/tftpboot/
```
2. In the TFTP root, ensure **config.txt** (Raspberry Pi boot config) includes the initramfs line. If you use the stock `config.txt` from the RPi firmware repo, add:
```
initramfs initrd.img followkernel
```
So the firmware loads the kernel and then the initrd that “follows” it. The Pi will boot the kernel and run `/init` from the initrd.
3. If your DHCP already points the Pi to this TFTP server and `start4cd.elf`, the Pi will load kernel + initrd from the same root. No NFS or extra server needed.
## Kernel cmdline (optional)
To override the provisioning server URL (e.g. if the dashboard is on another IP), add to **cmdline.txt** in the TFTP root (or append to the kernel command line):
```
provisioning_server=http://10.20.50.1:5000
```
The init script reads `provisioning_server=` from `/proc/cmdline` and exports `PROVISIONING_SERVER` for the client.
## Flow summary
1. Pi does DHCP → gets IP and TFTP server (e.g. 10.20.50.1).
2. Pi loads via TFTP: start4cd.elf, fixup4cd.dat, config.txt, cmdline.txt, kernel8.img, **initrd.img**.
3. Kernel boots with initrd as root; runs `/init`.
4. Init mounts minimal fs, ensures network, runs `/provisioning-client.sh`.
5. Client registers and polls; you choose Deploy or Backup in the dashboard; client runs dd + curl and exits.
6. After deploy, power cycle the Pi so it boots from eMMC.
See **docs/NETWORK-BOOT-DEPLOYMENT-FLOW.md** for the full deployment flow.

View File

@@ -0,0 +1,163 @@
#!/usr/bin/env bash
# Build provisioning initramfs for Raspberry Pi 4 / CM4 (aarch64).
# Produces initrd.img (gzip cpio) for TFTP boot (config.txt: initramfs initrd.img followkernel).
#
# On x86_64: tries Docker/Podman with --platform linux/arm64; if that fails (no
# arm64 emulation), downloads prebuilt static aarch64 busybox and curl. No sudo needed.
# On aarch64 (e.g. Raspberry Pi): uses local busybox and curl if installed.
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OUTPUT="${1:-$SCRIPT_DIR/initrd.img}"
BUILD_DIR=$(mktemp -d)
trap "rm -rf $BUILD_DIR" EXIT
echo "Build dir: $BUILD_DIR"
# Layout: /init, /provisioning-client.sh, /bin/busybox, /bin/sh, /usr/bin/curl, /lib/*.so
mkdir -p "$BUILD_DIR"/{bin,usr/bin,proc,sys,dev,dev/pts,lib}
cp "$SCRIPT_DIR/init" "$BUILD_DIR/init"
cp "$SCRIPT_DIR/provisioning-client.sh" "$BUILD_DIR/provisioning-client.sh"
chmod +x "$BUILD_DIR/init" "$BUILD_DIR/provisioning-client.sh"
ARCH=$(uname -m 2>/dev/null)
if [ "$ARCH" = "aarch64" ] || [ "$ARCH" = "arm64" ] || [ "$ARCH" = "armv8l" ]; then
if ! command -v busybox >/dev/null 2>&1 || ! command -v curl >/dev/null 2>&1; then
echo "On aarch64: install busybox and curl (apt install busybox curl) then re-run."
exit 1
fi
echo "Copying busybox and curl from host (aarch64)..."
cp "$(command -v busybox)" "$BUILD_DIR/bin/busybox"
cp "$(command -v curl)" "$BUILD_DIR/usr/bin/curl"
chmod +x "$BUILD_DIR/bin/busybox" "$BUILD_DIR/usr/bin/curl"
for f in $(ldd "$(command -v curl)" 2>/dev/null | awk '/=>/{print $3}'); do
[ -f "$f" ] && cp "$f" "$BUILD_DIR/lib/"
done
cp /lib/ld-linux-aarch64.so.1 "$BUILD_DIR/lib/" 2>/dev/null || true
else
# x86_64: try container first; on "Exec format error" (no arm64 emulation) use static downloads
CONTAINER_RUNTIME=""
for cmd in docker podman; do
if command -v "$cmd" >/dev/null 2>&1; then
CONTAINER_RUNTIME="$cmd"
break
fi
done
CONTAINER_OK=0
if [ -n "$CONTAINER_RUNTIME" ]; then
echo "Trying $CONTAINER_RUNTIME (linux/arm64)..."
CNT_NAME="cm4-initramfs-build-$$"
# Use a container-internal dir (no bind mount); copy out with podman cp (works with rootless)
$CONTAINER_RUNTIME run --name "$CNT_NAME" --platform linux/arm64 debian:bookworm-slim bash -c '
apt-get update -qq && apt-get install -y -qq busybox curl
mkdir -p /out/bin /out/usr/bin /out/lib
cp /bin/busybox /out/bin/busybox
cp /usr/bin/curl /out/usr/bin/curl
chmod +x /out/bin/busybox /out/usr/bin/curl
ldd /usr/bin/curl 2>/dev/null | awk "/=>/{print \$3}" | while read f; do [ -n "$f" ] && [ -f "$f" ] && cp "$f" /out/lib/; done
cp /lib/ld-linux-aarch64.so.1 /out/lib/ 2>/dev/null || true
' 2>/dev/null || true
# Always copy from container (avoids rootless bind-mount issues)
$CONTAINER_RUNTIME cp "$CNT_NAME:/out/bin/busybox" "$BUILD_DIR/bin/" 2>/dev/null && \
$CONTAINER_RUNTIME cp "$CNT_NAME:/out/usr/bin/curl" "$BUILD_DIR/usr/bin/" 2>/dev/null && \
$CONTAINER_RUNTIME cp "$CNT_NAME:/out/lib/." "$BUILD_DIR/lib/" 2>/dev/null || true
$CONTAINER_RUNTIME rm -f "$CNT_NAME" 2>/dev/null
if [ -f "$BUILD_DIR/bin/busybox" ] && [ -f "$BUILD_DIR/usr/bin/curl" ]; then
CONTAINER_OK=1
echo "Container build succeeded."
fi
fi
if [ "$CONTAINER_OK" -ne 1 ]; then
echo "Using prebuilt static aarch64 binaries (no container/emulation needed)..."
DOWNLOAD_DIR=$(mktemp -d)
trap "rm -rf $BUILD_DIR $DOWNLOAD_DIR" EXIT
if command -v curl >/dev/null 2>&1; then
GET="curl -sL"
GET_O="curl -sL -o"
else
GET="wget -q -O -"
GET_O="wget -q -O"
fi
# Static busybox aarch64: try busybox.net, then Alpine busybox-static package
BB_OK=0
$GET_O "$DOWNLOAD_DIR/busybox" "https://busybox.net/downloads/binaries/1.35.0-defconfig-multiarch-musl/busybox-armv8l" 2>/dev/null || true
if [ -f "$DOWNLOAD_DIR/busybox" ] && [ -s "$DOWNLOAD_DIR/busybox" ]; then
BB_OK=1
fi
if [ "$BB_OK" -ne 1 ]; then
echo "Trying Alpine busybox-static aarch64..."
$GET_O "$DOWNLOAD_DIR/bb.apk" "https://dl-cdn.alpinelinux.org/alpine/v3.19/main/aarch64/busybox-static-1.36.1-r11.apk" 2>/dev/null || true
if [ -f "$DOWNLOAD_DIR/bb.apk" ] && [ -s "$DOWNLOAD_DIR/bb.apk" ]; then
case "$(head -c 4 "$DOWNLOAD_DIR/bb.apk" | od -An -tx1 2>/dev/null | tr -d ' ')" in
3c21444f|3c68746d) ;; # HTML response, skip extract
*)
(cd "$DOWNLOAD_DIR" && (tar xf bb.apk 2>/dev/null || tar xzf bb.apk 2>/dev/null) && [ -f data.tar.gz ] && tar xzf data.tar.gz 2>/dev/null)
;;
esac
fi
if [ -f "$DOWNLOAD_DIR/bin/busybox" ] && [ -s "$DOWNLOAD_DIR/bin/busybox" ]; then
cp "$DOWNLOAD_DIR/bin/busybox" "$DOWNLOAD_DIR/busybox"
BB_OK=1
fi
fi
if [ "$BB_OK" -ne 1 ]; then
echo "Failed to download busybox (x86 host cannot run arm64 container without emulation)."
echo ""
echo "Option A - Enable arm64 containers (one-time, needs sudo):"
echo " Fedora: sudo dnf install -y qemu-user-static"
echo " Then re-run this script (Podman will use QEMU to run the arm64 build)."
echo ""
echo "Option B - Build on a Raspberry Pi 4 (aarch64):"
echo " scp -r $(dirname "$SCRIPT_DIR") pi@<pi-ip>:~/ && ssh pi@<pi-ip> 'cd ~/emmc-provisioning/network-boot-initramfs && sudo apt install -y busybox curl && ./build.sh'"
echo " Then scp pi@<pi-ip>:~/emmc-provisioning/network-boot-initramfs/initrd.img ."
exit 1
fi
chmod +x "$DOWNLOAD_DIR/busybox"
cp "$DOWNLOAD_DIR/busybox" "$BUILD_DIR/bin/busybox"
# Static curl aarch64 glibc (Raspberry Pi OS uses glibc)
$GET "https://github.com/stunnel/static-curl/releases/download/8.18.0/curl-linux-aarch64-glibc-8.18.0.tar.xz" -o "$DOWNLOAD_DIR/curl.tar.xz" || true
if [ ! -f "$DOWNLOAD_DIR/curl.tar.xz" ] || [ ! -s "$DOWNLOAD_DIR/curl.tar.xz" ]; then
echo "Failed to download static curl."
exit 1
fi
(cd "$DOWNLOAD_DIR" && tar xf curl.tar.xz)
CURL_BIN=$(find "$DOWNLOAD_DIR" -maxdepth 3 -name "curl" -type f 2>/dev/null | head -1)
if [ -n "$CURL_BIN" ] && [ -x "$CURL_BIN" ]; then
cp "$CURL_BIN" "$BUILD_DIR/usr/bin/curl"
chmod +x "$BUILD_DIR/usr/bin/curl"
else
echo "Could not find curl binary in tarball."
exit 1
fi
rm -rf "$DOWNLOAD_DIR"
trap "rm -rf $BUILD_DIR" EXIT
fi
fi
# Verify we have busybox (container or fallback must have left it)
if [ ! -f "$BUILD_DIR/bin/busybox" ] || [ ! -s "$BUILD_DIR/bin/busybox" ]; then
echo "Error: busybox not found in $BUILD_DIR/bin. If the container ran, check Podman volume mount."
exit 1
fi
chmod +x "$BUILD_DIR/bin/busybox" 2>/dev/null || true
# Busybox applets we need (sh, mount, udhcpc, etc.)
cd "$BUILD_DIR/bin"
./busybox --list 2>/dev/null | while read applet; do
case "$applet" in
sh|ash|mount|umount|mkdir|cat|ip|udhcpc|sleep|echo|grep|cut|awk|hostname|dd) ln -sf busybox "$applet"; ;;
esac
done
[ -e sh ] || ln -sf busybox sh
# Build cpio (gzip)
echo "Building cpio..."
( cd "$BUILD_DIR"; find . -print0 | cpio -o -H newc -0 2>/dev/null ) | gzip -9 > "$OUTPUT"
echo "Written: $OUTPUT ($(stat -c%s "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null) bytes)"
echo ""
echo "Next: copy initrd.img to TFTP root (e.g. /srv/tftpboot on LXC) and in config.txt add:"
echo " initramfs initrd.img followkernel"
echo "Then ensure the kernel line loads the initrd (followkernel does that)."
echo "Default provisioning server: http://10.20.50.1:5000 (override with kernel cmdline: provisioning_server=http://...)"

View File

@@ -0,0 +1,9 @@
# Add this line to config.txt in the TFTP root (/srv/tftpboot) so the Pi loads
# the provisioning initramfs after the kernel.
#
# initramfs initrd.img followkernel
#
# Ensure initrd.img is in the same directory (TFTP root). The kernel will use
# it as the initial root and run /init, which starts the provisioning client.
initramfs initrd.img followkernel

View File

@@ -0,0 +1,35 @@
#!/bin/sh
# Init for provisioning initramfs: bring up minimal env and run provisioning-client.sh.
# PROVISIONING_SERVER can be set via kernel cmdline: provisioning_server=http://10.20.50.1:5000
set -e
export PATH=/bin:/usr/bin
export LD_LIBRARY_PATH=/lib
echo "=== CM4 provisioning initramfs ==="
# Minimal filesystem
mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs none /dev
mkdir -p /dev/pts
mount -t devpts none /dev/pts
# Kernel might have brought up eth0 via ip=dhcp; ensure we have an IP
if ! ip addr show | grep -q 'inet .* scope global'; then
echo "Getting DHCP lease..."
udhcpc -f -q -i eth0 -n 2>/dev/null || true
fi
# Allow kernel cmdline to override: provisioning_server=http://10.20.50.1:5000
for arg in $(cat /proc/cmdline); do
case "$arg" in
provisioning_server=*) export PROVISIONING_SERVER="${arg#*=}"; ;;
esac
done
PROVISIONING_SERVER="${PROVISIONING_SERVER:-http://10.20.50.1:5000}"
export PROVISIONING_SERVER
echo "Provisioning server: $PROVISIONING_SERVER"
echo "Running provisioning client..."
exec /bin/sh /provisioning-client.sh

View File

@@ -0,0 +1,58 @@
#!/bin/sh
# POSIX sh version for initramfs. Registers with dashboard and runs Deploy or Backup.
# PROVISIONING_SERVER must be set (e.g. http://10.20.50.1:5000).
BASE_URL="${PROVISIONING_SERVER:-http://10.20.50.1:5000}"
BASE_URL="${BASE_URL%/}"
EMMC_DEV="${EMMC_DEV:-/dev/mmcblk0}"
get_mac() {
cat /sys/class/net/eth0/address 2>/dev/null || \
cat /sys/class/net/enp0s1f0/address 2>/dev/null || \
echo "unknown"
}
get_ip() {
hostname -I 2>/dev/null | awk '{print $1}' || echo ""
}
MAC=$(get_mac)
IP=$(get_ip)
echo "Registering with $BASE_URL (MAC=$MAC IP=$IP)..."
curl -s -X POST -H "Content-Type: application/json" \
-d "{\"mac\":\"$MAC\",\"ip\":\"$IP\"}" "$BASE_URL/api/register-device" || true
echo "Polling for action (Backup or Deploy)..."
while true; do
resp=$(curl -s "$BASE_URL/api/device-action-poll?mac=$MAC" 2>/dev/null || echo '{"action":"wait"}')
action=$(echo "$resp" | grep -o '"action":"[^"]*"' | cut -d'"' -f4)
url=$(echo "$resp" | grep -o '"url":"[^"]*"' | cut -d'"' -f4)
upload_url=$(echo "$resp" | grep -o '"upload_url":"[^"]*"' | cut -d'"' -f4)
if [ "$action" = "deploy" ] && [ -n "$url" ]; then
echo "Deploy: downloading image and writing to $EMMC_DEV..."
if [ ! -b "$EMMC_DEV" ]; then
echo "Error: $EMMC_DEV not found"
sleep 10
continue
fi
curl -sL "$url" | dd of="$EMMC_DEV" bs=4M status=progress conv=fsync
echo "Deploy done. Reboot to run from eMMC."
exit 0
fi
if [ "$action" = "backup" ] && [ -n "$upload_url" ]; then
echo "Backup: reading $EMMC_DEV and uploading..."
if [ ! -b "$EMMC_DEV" ]; then
echo "Error: $EMMC_DEV not found"
sleep 10
continue
fi
dd if="$EMMC_DEV" bs=4M status=progress 2>/dev/null | curl -s -X POST -T - "$upload_url"
echo "Backup done."
exit 0
fi
sleep 5
done