Remove obsolete audio and buzzer control documentation files, including detailed guides and HTML interfaces, to streamline the repository and eliminate redundancy. This cleanup enhances maintainability and focuses on essential resources for the reTerminal DM4 audio and buzzer functionalities.

This commit is contained in:
nearxos
2026-02-20 15:39:39 +02:00
parent 9656771d5a
commit 58d9144752
101 changed files with 80 additions and 193 deletions

View File

@@ -0,0 +1,4 @@
# Ensure CM4 boot-mode USB device is accessible to rpiboot (libusb).
# Load before 90-cm4-boot-mode.rules so permissions are set before the trigger runs.
SUBSYSTEM=="usb", ATTR{idVendor}=="2b8e", MODE="0666"
SUBSYSTEM=="usb", ATTR{idVendor}=="0a5c", ATTR{idProduct}=="2711", MODE="0666"

View File

@@ -0,0 +1,10 @@
# When reTerminal (CM4) is connected in USB boot mode (eMMC disable jumper),
# the device appears as either Raspberry Pi Foundation (2b8e) or Broadcom BCM2711 Boot (0a5c:2711).
# Trigger provisioning: run rpiboot, then wait for user to choose Backup or Deploy in the portal.
# Install: sudo cp 90-cm4-boot-mode.rules /etc/udev/rules.d/
# sudo udevadm control --reload-rules
SUBSYSTEM=="usb", ATTR{idVendor}=="2b8e", ACTION=="add", \
RUN+="/usr/local/bin/cm4-flash-trigger.sh"
SUBSYSTEM=="usb", ATTR{idVendor}=="0a5c", ATTR{idProduct}=="2711", ACTION=="add", \
RUN+="/usr/local/bin/cm4-flash-trigger.sh"

View File

@@ -0,0 +1,11 @@
# Host-side provisioning scripts
These files run on the **provisioning host** (e.g. the Proxmox host where the reTerminal is connected via USB).
| File | Purpose |
|------|--------|
| **flash-emmc-on-connect.sh** | Runs `rpiboot`, detects eMMC, waits for dashboard choice (Backup/Deploy), then runs `dd`. Install to `/opt/cm4-provisioning/`. |
| **cm4-flash-trigger.sh** | Started by udev when USB device 2b8e is added. Launches the flash script via `systemd-run`. Install to `/usr/local/bin/`. |
| **90-cm4-boot-mode.rules** | udev rule: on USB add (vendor 2b8e), run the trigger. Install to `/etc/udev/rules.d/`. |
See [../docs/EMMC-PROVISIONING-GUIDE.md](../docs/EMMC-PROVISIONING-GUIDE.md) for full setup. The [deploy script](../scripts/deploy-to-proxmox.sh) copies these into place on the Proxmox host and LXC.

View File

@@ -0,0 +1,174 @@
#!/usr/bin/env bash
# Run on the Proxmox host when build_cloudinit_request.json appears in the provisioning dir.
# Downloads Raspberry Pi OS image from URL, injects cloud-init NoCloud files, saves to BACKUPS_DIR.
# Uses loop devices and mount (available on host, not in unprivileged LXC).
# Triggered by systemd path unit cm4-build-cloudinit.path.
set -e
PROV_DIR="${CM4_PROVISIONING_DIR:-/var/lib/cm4-provisioning}"
REQUEST_FILE="$PROV_DIR/build_cloudinit_request.json"
STATUS_FILE="$PROV_DIR/build_cloudinit_status.json"
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
BACKUPS_DIR="${BACKUPS_DIR:-$PROV_DIR/backups}"
CLOUDINIT_IMAGES_DIR="${CLOUDINIT_IMAGES_DIR:-$PROV_DIR/cloudinit-images}"
DOWNLOAD_CACHE_DIR="${CM4_DOWNLOAD_CACHE_DIR:-$PROV_DIR/download-cache}"
# Write built cloud-init images to CLOUDINIT_IMAGES_DIR (separate from backups)
OUTPUT_DIR="${CLOUDINIT_IMAGES_DIR}"
GOLDEN_IMAGE="${GOLDEN_IMAGE:-$PROV_DIR/golden.img}"
write_status() {
local phase="$1" message="$2" output_name="$3" error="$4"
printf '{"phase":"%s","message":"%s","output_name":%s,"error":%s,"updated":%s}\n' \
"$phase" "$message" \
"$([ -n "$output_name" ] && echo "\"$output_name\"" || echo "null")" \
"$([ -n "$error" ] && echo "\"${error//\"/\\\"}\"" || echo "null")" \
"$(date +%s)" > "$STATUS_FILE"
}
[[ -f "$REQUEST_FILE" ]] || { echo "No request file"; exit 0; }
# Use temp dir on provisioning dir (not /tmp) so we have enough space for decompress (~3GB+)
mkdir -p "$PROV_DIR"
TEMP_DIR=$(mktemp -d -p "$PROV_DIR" build.XXXXXX 2>/dev/null) || TEMP_DIR="$PROV_DIR/build.$$" && mkdir -p "$TEMP_DIR"
trap 'rm -rf "$TEMP_DIR"; rm -f "$REQUEST_FILE"' EXIT
# Extract fields from JSON into temp files (handles multi-line content)
python3 - "$REQUEST_FILE" "$TEMP_DIR" << 'PY'
import json, sys
with open(sys.argv[1]) as f:
d = json.load(f)
out = sys.argv[2]
for name, key in [("user-data", "user_data"), ("meta-data", "meta_data"), ("network-config", "network_config")]:
with open(f"{out}/{name}", "w") as f:
f.write(d.get(key, ""))
with open(f"{out}/variant", "w") as f:
f.write(d.get("variant", "lite"))
with open(f"{out}/url", "w") as f:
f.write(d.get("url", ""))
with open(f"{out}/set_golden", "w") as f:
f.write("1" if d.get("set_as_golden_after") else "0")
PY
URL=$(cat "$TEMP_DIR/url")
VARIANT=$(cat "$TEMP_DIR/variant")
SET_AS_GOLDEN=$(cat "$TEMP_DIR/set_golden")
[[ -n "$URL" ]] || { write_status "error" "" "" "Missing url in request"; exit 1; }
OUT_NAME="raspios-${VARIANT}-cloudinit-$(date +%Y%m%d-%H%M%S).img"
OUT_PATH="$OUTPUT_DIR/$OUT_NAME"
BASENAME=$(basename "$URL")
SHA256_URL="${URL}.sha256"
CACHED="$DOWNLOAD_CACHE_DIR/$BASENAME"
XZ_FILE="$TEMP_DIR/image.img.xz"
CURL_ERR="$TEMP_DIR/curl_err.txt"
SHA256_FILE="$TEMP_DIR/expected.sha256"
# Fetch official SHA256 checksum if available (Raspberry Pi OS publishes .img.xz.sha256 next to each image)
write_status "downloading" "Checking for existing image and checksum…" "" ""
EXPECTED_HASH=""
if curl -sfL -o "$SHA256_FILE" "$SHA256_URL" 2>/dev/null && [[ -s "$SHA256_FILE" ]]; then
# Format is typically "hash filename" or just "hash"; take first 64-char hex token
EXPECTED_HASH=$(head -1 "$SHA256_FILE" | awk '{print $1}')
[[ ${#EXPECTED_HASH} -eq 64 && "$EXPECTED_HASH" =~ ^[0-9a-fA-F]+$ ]] || EXPECTED_HASH=""
fi
USE_CACHED=0
mkdir -p "$DOWNLOAD_CACHE_DIR"
if [[ -f "$CACHED" && -s "$CACHED" ]]; then
if [[ -n "$EXPECTED_HASH" ]]; then
ACTUAL_HASH=$(sha256sum -b "$CACHED" 2>/dev/null | awk '{print $1}')
if [[ "$ACTUAL_HASH" == "$EXPECTED_HASH" ]]; then
USE_CACHED=1
else
rm -f "$CACHED"
fi
else
USE_CACHED=1
fi
fi
if [[ $USE_CACHED -eq 1 ]]; then
write_status "downloading" "Using cached image (checksum OK): $BASENAME" "" ""
cp "$CACHED" "$XZ_FILE"
else
write_status "downloading" "Downloading $(basename "$URL")" "" ""
CURL_OPTS=(-f -L -o "$XZ_FILE" --connect-timeout 30 --max-time 7200)
[[ "${CURL_INSECURE:-}" == "1" ]] && CURL_OPTS+=(-k)
if ! curl "${CURL_OPTS[@]}" "$URL" 2>"$CURL_ERR"; then
err_detail=$(head -c 200 "$CURL_ERR" | tr '\n' ' ' | sed 's/"/\\"/g')
write_status "error" "" "" "Download failed: ${err_detail:-curl exited with error}"
exit 1
fi
# Verify with official checksum if we have it
if [[ -n "$EXPECTED_HASH" ]]; then
ACTUAL_HASH=$(sha256sum -b "$XZ_FILE" 2>/dev/null | awk '{print $1}')
if [[ "$ACTUAL_HASH" != "$EXPECTED_HASH" ]]; then
write_status "error" "" "" "Checksum mismatch: download may be corrupted (expected ${EXPECTED_HASH:0:16}..., got ${ACTUAL_HASH:0:16}...). Try again."
exit 1
fi
write_status "downloading" "Checksum OK, caching image…" "" ""
fi
cp "$XZ_FILE" "$CACHED"
fi
write_status "decompressing" "Decompressing image…" "" ""
# Check we have a real xz file (not HTML error page)
if ! command -v xz >/dev/null 2>&1; then
write_status "error" "" "" "Decompress failed: xz not installed. Install xz-utils (apt install xz-utils)"
exit 1
fi
if [[ ! -s "$XZ_FILE" ]]; then
write_status "error" "" "" "Decompress failed: downloaded file is empty"
exit 1
fi
FILE_TYPE=$(file -b "$XZ_FILE" 2>/dev/null || true)
if [[ "$FILE_TYPE" == *"HTML"* ]] || [[ "$FILE_TYPE" == *"text"* ]] && [[ "$FILE_TYPE" != *"XZ"* ]]; then
write_status "error" "" "" "Decompress failed: download is not an image (got: ${FILE_TYPE:0:80})"
exit 1
fi
# Need ~3GB+ free for decompressing typical Raspios .img.xz; use same filesystem as TEMP_DIR
AVAIL_KB=$(df -P "$TEMP_DIR" 2>/dev/null | awk 'NR==2 {print $4}')
AVAIL_GB=0
[[ -n "$AVAIL_KB" ]] && AVAIL_GB=$((AVAIL_KB / 1024 / 1024))
if [[ -n "$AVAIL_KB" && "$AVAIL_KB" -lt 3145728 ]]; then
write_status "error" "" "" "Decompress failed: not enough disk space (${AVAIL_GB}GB free on $(df -P "$TEMP_DIR" 2>/dev/null | awk 'NR==2 {print $6}'); need ~3GB). Free space or use a larger volume."
exit 1
fi
IMG_FILE="$TEMP_DIR/image.img"
XZ_ERR="$TEMP_DIR/xz_err.txt"
# -T 1: single-threaded to reduce memory use; -d -k -f: decompress, keep source, force
if ! xz -T 1 -d -k -f "$XZ_FILE" 2>"$XZ_ERR"; then
err_detail=$(cat "$XZ_ERR" 2>/dev/null | tr '\n' ' ' | sed 's/"/\\"/g' | head -c 400)
[[ -z "$err_detail" ]] && err_detail="out of disk space or memory?"
write_status "error" "" "" "Decompress failed: ${err_detail} (free space: ${AVAIL_GB}GB)"
exit 1
fi
[[ -f "$IMG_FILE" ]] || { write_status "error" "" "" "image.img not found after decompress"; exit 1; }
write_status "injecting" "Mounting boot partition and injecting cloud-init…" "" ""
LOOP=$(losetup -f --show -P "$IMG_FILE")
boot_part="${LOOP}p1"
[[ -b "$boot_part" ]] || boot_part="${LOOP}p2"
[[ -b "$boot_part" ]] || { write_status "error" "" "" "Boot partition not found"; losetup -d "$LOOP"; exit 1; }
MNT="$TEMP_DIR/mnt"
mkdir -p "$MNT"
mount "$boot_part" "$MNT"
cp "$TEMP_DIR/user-data" "$MNT/user-data"
cp "$TEMP_DIR/meta-data" "$MNT/meta-data"
cp "$TEMP_DIR/network-config" "$MNT/network-config"
umount "$MNT"
losetup -d "$LOOP"
write_status "finalizing" "Copying image to cloud-init images…" "" ""
mkdir -p "$OUTPUT_DIR"
cp "$IMG_FILE" "$OUT_PATH"
if [[ "$SET_AS_GOLDEN" == "1" ]]; then
rm -f "$GOLDEN_IMAGE"
ln -sf "$OUT_PATH" "$GOLDEN_IMAGE"
write_status "done" "Built $OUT_NAME and set as golden image." "$OUT_NAME" ""
else
write_status "done" "Built $OUT_NAME" "$OUT_NAME" ""
fi

View File

@@ -0,0 +1,9 @@
[Unit]
Description=CM4 build cloud-init image (when request file appears)
After=local-fs.target
[Path]
PathExists=/var/lib/cm4-provisioning/build_cloudinit_request.json
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,14 @@
[Unit]
Description=CM4 build cloud-init image (download Raspios, inject NoCloud)
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
Environment=CM4_PROVISIONING_DIR=/var/lib/cm4-provisioning
ExecStart=/opt/cm4-provisioning/build-cloudinit-image.sh
# Run as root; script uses losetup, mount
User=root
StandardOutput=journal
StandardError=journal
TimeoutStartSec=7200

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
# Called by udev when CM4 in boot mode (0a5c:2711 or 2b8e) is connected.
# Start the flash service via systemd so it runs under systemd, not udev —
# otherwise udev kills the process when the device re-enumerates (first→second stage).
# --no-block: return immediately so udev doesn't wait; the service runs in the background.
systemctl --no-block start cm4-flash.service

View File

@@ -0,0 +1,21 @@
[Unit]
Description=CM4 eMMC provisioning (rpiboot + backup/deploy)
# Run after udev has settled; do not block boot
After=systemd-udevd.service
DefaultDependencies=yes
[Service]
Type=oneshot
# Delay so USB device is enumerated and udev permissions applied before we run
ExecStartPre=/bin/sleep 5
ExecStart=/opt/cm4-provisioning/flash-emmc-on-connect.sh
# Run as root; flash script logs to /var/lib/cm4-provisioning/flash.log
User=root
StandardOutput=journal
StandardError=journal
# Allow long run: rpiboot + wait for choice + dd 32GB (~2035 min) + optional PiShrink (~1530 min)
TimeoutStartSec=7200
[Install]
# Only started by udev trigger, not at boot
WantedBy=multi-user.target

View File

@@ -0,0 +1,9 @@
[Unit]
Description=CM4 shrink/compress backup (when request file appears)
After=local-fs.target
[Path]
PathExists=/var/lib/cm4-provisioning/shrink_request.json
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,12 @@
[Unit]
Description=CM4 run PiShrink on requested backup image
After=local-fs.target
[Service]
Type=oneshot
Environment=CM4_PROVISIONING_DIR=/var/lib/cm4-provisioning
ExecStart=/opt/cm4-provisioning/run-shrink-on-host.sh
User=root
StandardOutput=journal
StandardError=journal
TimeoutStartSec=3600

View File

@@ -0,0 +1,233 @@
#!/usr/bin/env bash
# Provision CM4 eMMC when reTerminal is connected in boot mode (eMMC disable jumper).
# On USB boot device (2b8e) detection: run rpiboot to expose eMMC, then wait for the user
# to choose Backup or Deploy in the portal — no auto-flash; action runs only after portal choice.
# Run this from udev or a systemd service. Requires: usbboot (rpiboot) built, golden image for deploy.
# Do NOT use set -e: rpiboot and intermediate commands may return non-zero without being fatal
# Redirect all output to log file (stdout + stderr) so nothing is lost to nohup buffering
LOG_FILE="${LOG_FILE:-/var/lib/cm4-provisioning/flash.log}"
exec >> "$LOG_FILE" 2>&1
# Load overrides from env file if present
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
# Configuration - adjust paths and size for your setup
RPIBOOT_DIR="${RPIBOOT_DIR:-/opt/usbboot}"
GOLDEN_IMAGE="${GOLDEN_IMAGE:-/var/lib/cm4-provisioning/golden.img}"
# Expected eMMC size in bytes. reTerminal DM (CM4) has 32 GB eMMC (~31268536320 bytes).
EMMC_SIZE_BYTES="${EMMC_SIZE_BYTES:-$(( 32 * 1024 * 1024 * 1024 ))}"
LOG_TAG="cm4-flash"
STATUS_FILE="${STATUS_FILE:-/var/lib/cm4-provisioning/status.json}"
LOG_FILE="${LOG_FILE:-/var/lib/cm4-provisioning/flash.log}"
LOCK_FILE="${LOCK_FILE:-/var/lib/cm4-provisioning/flash.lock}"
log() { echo "[$(date -Iseconds)] [$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
write_status() {
local phase="$1" message="$2" progress="${3:-null}" error="${4:-}"
local ts; ts=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
message="${message//\"/\\\"}"; error="${error//\"/\\\"}"
if [[ -n "$error" ]]; then
printf '{"phase":"%s","message":"%s","progress":%s,"error":"%s","updated":"%s"}\n' \
"$phase" "$message" "$progress" "$error" "$ts" > "$STATUS_FILE" 2>/dev/null || true
else
printf '{"phase":"%s","message":"%s","progress":%s,"updated":"%s"}\n' \
"$phase" "$message" "$progress" "$ts" > "$STATUS_FILE" 2>/dev/null || true
fi
}
# Prevent concurrent runs (udev fires multiple times during rpiboot re-enumeration)
exec 9>"$LOCK_FILE"
if ! flock -n 9; then
echo "[$(date -Iseconds)] Another flash-emmc instance is already running; exiting."
exit 0
fi
trap 'rm -f "$LOCK_FILE" "$CURRENT_DEVICE_FILE" "$DEVICE_SOURCE_FILE" 2>/dev/null; exec 9>&-' EXIT
# Optional: only run if this file exists (safety)
ENABLE_FILE="${ENABLE_FILE:-/etc/cm4-provisioning/enabled}"
if [[ -n "$ENABLE_FILE" && ! -f "$ENABLE_FILE" ]]; then
log "Skipping: $ENABLE_FILE not present"
write_status "idle" "Provisioning disabled (remove /etc/cm4-provisioning/enabled to enable)" "null" 2>/dev/null || true
exit 0
fi
# When a device is detected we ask the user (dashboard): Backup or Deploy?
# These files are used to wait for the user's choice (written by dashboard, read by this script).
BACKUPS_DIR="${BACKUPS_DIR:-/var/lib/cm4-provisioning/backups}"
# Optional: shrink backup with PiShrink (requires pishrink + parted, e2fsprogs). SHRINK_BACKUP=1 to enable.
SHRINK_BACKUP="${SHRINK_BACKUP:-0}"
# If SHRINK_BACKUP=1, optionally compress: PISHRINK_COMPRESS=gz or xz (uses parallel when available). Uncompressed .img can be dd'd directly for deploy.
PISHRINK_COMPRESS="${PISHRINK_COMPRESS:-}"
ACTION_REQUEST_FILE="${ACTION_REQUEST_FILE:-/var/lib/cm4-provisioning/action_request}"
CURRENT_DEVICE_FILE="${CURRENT_DEVICE_FILE:-/var/lib/cm4-provisioning/current_device}"
DEVICE_SOURCE_FILE="${DEVICE_SOURCE_FILE:-/var/lib/cm4-provisioning/device_source}"
WAIT_TIMEOUT="${WAIT_TIMEOUT:-600}"
# Golden image required only for Deploy; allow Backup without it
if [[ ! -f "$GOLDEN_IMAGE" ]]; then
log "Golden image not found (Deploy will be unavailable): $GOLDEN_IMAGE"
fi
RPIBOOT_BIN="$RPIBOOT_DIR/rpiboot"
# Gadget dir for CM4 (64-bit); fallback to mass-storage-gadget
RPIBOOT_GADGET=""
for d in "$RPIBOOT_DIR/mass-storage-gadget64" "$RPIBOOT_DIR/mass-storage-gadget"; do
if [[ -d "$d" ]]; then
RPIBOOT_GADGET="$d"
break
fi
done
if [[ ! -x "$RPIBOOT_BIN" ]]; then
log "rpiboot not found: $RPIBOOT_BIN (build usbboot and set RPIBOOT_DIR)"
write_status "error" "rpiboot not installed" "null" "rpiboot not found. Run install-usbboot-on-host.sh on the host or build-and-deploy-usbboot-to-host.sh from your machine."
exit 1
fi
if [[ -z "$RPIBOOT_GADGET" ]]; then
log "rpiboot gadget dir not found under $RPIBOOT_DIR (need mass-storage-gadget64 or mass-storage-gadget)"
write_status "error" "rpiboot gadget missing" "null" "Copy mass-storage-gadget(64) to $RPIBOOT_DIR"
exit 1
fi
# rpiboot requires bootfiles.bin or one of bootcode*.bin in the gadget dir; empty dir causes "No 'bootcode' files found"
if [[ ! -f "$RPIBOOT_GADGET/bootfiles.bin" && ! -f "$RPIBOOT_GADGET/bootcode.bin" && ! -f "$RPIBOOT_GADGET/bootcode4.bin" && ! -f "$RPIBOOT_GADGET/bootcode5.bin" ]]; then
log "rpiboot gadget dir has no boot files: $RPIBOOT_GADGET (reinstall usbboot)"
write_status "error" "rpiboot gadget empty" "null" "No boot files in $RPIBOOT_GADGET. On the host run: fix-gadget-bootcode-on-host.sh (or from your machine: ssh root@HOST 'bash -s' < scripts/fix-gadget-bootcode-on-host.sh). See docs troubleshooting."
exit 1
fi
# Ensure status dir exists and start with running state
mkdir -p "$(dirname "$STATUS_FILE")" "$BACKUPS_DIR" 2>/dev/null || true
write_status "rpiboot" "Connecting to CM4 in boot mode…" "0"
# Block devices before rpiboot (so we can detect new one after)
before_devs=$(lsblk -nd -o NAME 2>/dev/null | sort)
log "Starting rpiboot to expose CM4 eMMC as mass storage..."
# Run rpiboot with 90s timeout so we don't hang if it doesn't exit cleanly when device switches to mass storage
rpiboot_exit=0
timeout 90 "$RPIBOOT_BIN" -d "$RPIBOOT_GADGET" || rpiboot_exit=$?
# timeout returns 124 if killed by timeout; 0 or other if rpiboot exited on its own
if [[ "$rpiboot_exit" -eq 124 ]]; then
log "rpiboot timed out after 90s (device may have switched to mass storage)"
elif [[ "$rpiboot_exit" -ne 0 ]]; then
log "rpiboot exited with code $rpiboot_exit"
write_status "error" "rpiboot failed" "null" "rpiboot failed or no device connected. Check flash.log on host. Try unplug/replug USB."
exit 1
fi
echo "[$(date -Iseconds)] rpiboot finished (exit=$rpiboot_exit); starting device scan"
log "rpiboot completed; waiting for block device..."
write_status "rpiboot" "rpiboot done, waiting for block device…" "10"
# rpiboot exits when device switches to mass storage; udev may need several seconds to create /dev/sdX
# Poll for new block device for up to 30s (device switch can be slow)
target_dev=""
for wait_sec in $(seq 2 2 10) $(seq 12 2 30); do
sleep 2
for dev in /dev/sd[a-z] /dev/sd[a-z][a-z]; do
[[ -b "$dev" ]] || continue
[[ "$dev" =~ [0-9]$ ]] && continue
size=$(blockdev --getsize64 "$dev" 2>/dev/null || true)
if [[ -n "$size" ]]; then
if (( size >= EMMC_SIZE_BYTES * 95 / 100 && size <= EMMC_SIZE_BYTES * 105 / 100 )); then
target_dev=$dev
break 2
fi
if [[ -z "$target_dev" && "$before_devs" != *"${dev#/dev/}"* ]]; then
target_dev=$dev
fi
fi
done
[[ -n "$target_dev" ]] && break
log "Waiting for block device... ${wait_sec}s"
write_status "rpiboot" "Waiting for eMMC block device… (${wait_sec}s)" "10"
done
log "Device scan complete. before_devs=[$before_devs] target_dev=[$target_dev]"
if [[ -z "$target_dev" ]]; then
log "No suitable block device found after rpiboot (expected ~${EMMC_SIZE_BYTES} bytes)"
write_status "error" "No eMMC device found" "null" "No suitable block device after rpiboot"
exit 1
fi
# Ask user (dashboard): Backup or Deploy?
write_status "waiting_choice" "Device connected (USB boot mode). Choose Backup or Deploy in the dashboard." "null"
echo "usb" > "$DEVICE_SOURCE_FILE" 2>/dev/null || true
echo "$target_dev" > "$CURRENT_DEVICE_FILE" 2>/dev/null || true
log "Waiting for user choice (Backup or Deploy) in dashboard; timeout ${WAIT_TIMEOUT}s..."
for (( i = 0; i < WAIT_TIMEOUT; i += 2 )); do
sleep 2
if [[ -f "$ACTION_REQUEST_FILE" ]]; then
action=$(cat "$ACTION_REQUEST_FILE" 2>/dev/null | tr -d '[:space:]' | tr '[:upper:]' '[:lower:]')
rm -f "$ACTION_REQUEST_FILE" 2>/dev/null || true
if [[ "$action" == "backup" ]]; then
backup_name="backup-$(date +%Y%m%d-%H%M%S).img"
backup_path="$BACKUPS_DIR/$backup_name"
write_status "backup" "Creating backup…" "null"
log "Backing up $target_dev to $backup_path..."
if dd if="$target_dev" of="$backup_path" bs=4M status=progress conv=fsync; then
final_name="$backup_name"
shrink_requested=false
[[ "$SHRINK_BACKUP" == "1" || "$SHRINK_BACKUP" == "true" ]] && shrink_requested=true
[[ -f "$(dirname "$STATUS_FILE")/shrink_next_backup" ]] && shrink_requested=true
rm -f "$(dirname "$STATUS_FILE")/shrink_next_backup" 2>/dev/null || true
if [[ "$shrink_requested" == "true" ]]; then
pishrink_cmd=""
for p in /usr/local/bin/pishrink.sh /usr/local/bin/pishrink; do
[[ -x "$p" ]] && pishrink_cmd="$p" && break
done
if [[ -n "$pishrink_cmd" ]]; then
write_status "backup" "Shrinking backup image…" "null"
log "Shrinking backup with PiShrink (timeout 30 min)..."
pishrink_opts="-n"
[[ "$PISHRINK_COMPRESS" == "gz" || "$PISHRINK_COMPRESS" == "gzip" ]] && pishrink_opts="$pishrink_opts -z -a"
[[ "$PISHRINK_COMPRESS" == "xz" ]] && pishrink_opts="$pishrink_opts -Z -a"
PISHRINK_TIMEOUT="${PISHRINK_TIMEOUT:-1800}"
if timeout "$PISHRINK_TIMEOUT" $pishrink_cmd $pishrink_opts "$backup_path" 2>&1; then
if [[ "$PISHRINK_COMPRESS" == "gz" || "$PISHRINK_COMPRESS" == "gzip" ]]; then
final_name="${backup_name}.gz"
elif [[ "$PISHRINK_COMPRESS" == "xz" ]]; then
final_name="${backup_name}.xz"
fi
log "Shrunk backup: $BACKUPS_DIR/$final_name"
else
rc=$?
if [[ "$rc" -eq 124 ]]; then
log "PiShrink timed out after ${PISHRINK_TIMEOUT}s; keeping full backup $backup_path"
else
log "PiShrink failed (exit $rc); keeping full backup $backup_path"
fi
fi
else
log "SHRINK_BACKUP=1 but pishrink not found; keeping full backup. Install with scripts/install-pishrink-on-host.sh"
fi
fi
log "Backup complete: $BACKUPS_DIR/$final_name"
write_status "done" "Backup complete: $final_name" "100"
else
write_status "error" "Backup failed" "null" "dd failed"
fi
elif [[ "$action" == "deploy" ]]; then
if [[ ! -f "$GOLDEN_IMAGE" ]]; then
log "Golden image not found; cannot deploy."
write_status "error" "Deploy unavailable" "null" "Golden image not found. Add golden.img to /var/lib/cm4-provisioning/ for deploy."
exit 1
fi
write_status "flashing" "Writing golden image…" "null"
log "Flashing $GOLDEN_IMAGE to $target_dev..."
if dd if="$GOLDEN_IMAGE" of="$target_dev" bs=4M status=progress conv=fsync; then
log "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal."
write_status "done" "Flash complete. Remove eMMC disable jumper and power cycle the reTerminal." "100"
else
write_status "error" "Flash failed" "null" "dd failed"
fi
else
write_status "error" "Unknown action" "null" "action_request must be 'backup' or 'deploy'"
fi
exit 0
fi
done
log "Timeout waiting for user choice"
write_status "idle" "Timeout waiting for choice. Connect the device again to retry." "null"
exit 0

View File

@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# Run on the Proxmox host when shrink_request.json appears in the provisioning dir.
# Runs PiShrink on the requested backup image (shrink or shrink+compress).
# Requires PiShrink on host (scripts/install-pishrink-on-host.sh). Triggered by cm4-shrink.path.
set -e
PROV_DIR="${CM4_PROVISIONING_DIR:-/var/lib/cm4-provisioning}"
REQUEST_FILE="$PROV_DIR/shrink_request.json"
STATUS_FILE="$PROV_DIR/shrink_status.json"
[[ -f /opt/cm4-provisioning/env ]] && source /opt/cm4-provisioning/env
BACKUPS_DIR="${BACKUPS_DIR:-$PROV_DIR/backups}"
PISHRINK="${PISHRINK_SCRIPT:-/usr/local/bin/pishrink.sh}"
SHRINK_TIMEOUT="${SHRINK_TIMEOUT:-2100}"
write_status() {
local phase="$1" name="$2" message="$3" error="$4"
printf '{"phase":"%s","name":"%s","message":"%s","error":%s,"updated":%s}\n' \
"$phase" "$name" "$message" \
"$([ -n "$error" ] && echo "\"${error//\"/\\\"}\"" || echo "null")" \
"$(date +%s)" > "$STATUS_FILE"
}
[[ -f "$REQUEST_FILE" ]] || { echo "No request file"; exit 0; }
# Parse request
NAME=$(python3 -c "import json; print(json.load(open('$REQUEST_FILE')).get('name',''))")
ACTION=$(python3 -c "import json; print(json.load(open('$REQUEST_FILE')).get('action','shrink'))")
FORMAT=$(python3 -c "import json; print(json.load(open('$REQUEST_FILE')).get('format','xz'))")
rm -f "$REQUEST_FILE"
# Validate
if [[ -z "$NAME" ]]; then
write_status "error" "" "" "Missing name in request"
exit 0
fi
if [[ "$NAME" != "${NAME##*/}" ]] || [[ "$NAME" == *..* ]]; then
write_status "error" "$NAME" "" "Invalid name"
exit 0
fi
if [[ ! "$NAME" =~ \.img$ ]]; then
write_status "error" "$NAME" "" "Only .img files can be shrunk"
exit 0
fi
PATH_FILE="$BACKUPS_DIR/$NAME"
if [[ ! -f "$PATH_FILE" ]]; then
write_status "error" "$NAME" "" "File not found"
exit 0
fi
if [[ ! -x "$PISHRINK" ]]; then
write_status "error" "$NAME" "" "PiShrink not installed on host. Run scripts/install-pishrink-on-host.sh"
exit 0
fi
write_status "running" "$NAME" "Shrinking…" ""
OPTS=(-n)
if [[ "$ACTION" == "compress" ]]; then
if [[ "$FORMAT" == "gz" ]] || [[ "$FORMAT" == "gzip" ]]; then
OPTS+=(-z -a)
else
OPTS+=(-Z -a)
fi
fi
cd "$BACKUPS_DIR" || exit 1
if timeout "$SHRINK_TIMEOUT" "$PISHRINK" "${OPTS[@]}" "$NAME" 2>&1; then
if [[ "$ACTION" == "compress" ]]; then
ext=".xz"
[[ "$FORMAT" == "gz" ]] || [[ "$FORMAT" == "gzip" ]] && ext=".gz"
write_status "done" "$NAME" "Compressed to ${NAME}${ext}" ""
else
write_status "done" "$NAME" "Shrunk $NAME" ""
fi
else
rc=$?
write_status "error" "$NAME" "" "PiShrink failed (exit $rc)"
fi