#!/bin/sh
# (C) Copyright 2020-2026
# Scott Wiederhold, s.e.wiederhold@gmail.com
# https://community.openglow.org
# SPDX-License-Identifier:    MIT
#
# Boot-slot tool for Glowforge factory hardware running ForgeFIRM or
# factory firmware: inventories what is installed on each bootable
# partition and switches the boot target by rewriting the saved U-Boot
# environment (mmcdev/mmchwpart/mmcpart/mmcroot).
#
# The flip writes all four variables in one fw_setenv -s transaction and
# read-back verifies them, retrying with the classic u-boot-tools script
# format ("name value") if the libubootenv format ("name=value") did not
# take, and per-variable calls as a last resort. A switch target must
# pass a content probe (rootfs mounts, kernel present) unless forced.

usage () {
  cat <<END
usage: ffboot -l | -s|-e[<partition>] [-n] [-f]
  -l: inventory bootable partitions (key=value lines) and exit
  -s: SDCARD OpenGlow/ForgeFIRM
  -e[<partition>]: eMMC
     1: eMMC slot 1 (factory image 1)
     2: eMMC slot 2 (factory image 2)
     4: legacy OpenGlow/ForgeFIRM partition
     Default: factory image with the most recent firmware
  -n: No reboot
  -f: Force - skip the target content probe
END
}

# --- fw_env config selection -------------------------------------------------
# The env lives on the eMMC user area (0x80000/0x82000, redundant). Factory
# firmware ships per-device configs; the mmcblk2-specific override is honored
# if present, otherwise the standard /etc/fw_env.config is used.
if [ -f "/etc/fw_env_mmcblk2.config" ] && [ ! -d "/factory" ]; then
  FWCONFIG="/etc/fw_env_mmcblk2.config"
else
  FWCONFIG="/etc/fw_env.config"
fi

BOOTED_ROOT=$(sed -n 's/.*root=\([^ ]*\).*/\1/p' /proc/cmdline)

# --- partition probe ---------------------------------------------------------
# probe_part <device>
# Sets: P_PRESENT P_STATE(ok|empty|unreadable) P_TYPE(forgefirm|factory|unknown)
#       P_VERSION (display) P_DATE (build datetime, for ordering) P_KERNEL(yes|no)
probe_part () {
  P_PRESENT=no; P_STATE=unreadable; P_TYPE=unknown
  P_VERSION=""; P_DATE=""; P_KERNEL=no
  P_MOUNTED=""
  [ -b "$1" ] || return 1
  P_PRESENT=yes
  if [ "$1" = "$BOOTED_ROOT" ]; then
    ROOT_DIR=""
  else
    # Reuse an existing mount (the image keeps the factory slots mounted
    # under /factory); a fresh mount is explicit -t ext4 - letting mount
    # iterate types provokes a cosmetic kernel "Can't open blockdev" for
    # each foreign-type claim against an already-mounted device.
    ROOT_DIR=$(sed -n "s|^$1 \([^ ]*\).*|\1|p" /proc/mounts | head -n 1)
    if [ -z "$ROOT_DIR" ]; then
      P_MOUNTED=yes
      ROOT_DIR=$(mktemp -d /tmp/ffboot.probe.XXXXXX) || return 1
      if ! mount -o ro -t ext4 "$1" "$ROOT_DIR" 2>/dev/null; then
        rmdir "$ROOT_DIR" 2>/dev/null
        return 1
      fi
    fi
  fi
  if [ -f "$ROOT_DIR/etc/forgefirm-version" ]; then
    P_TYPE=forgefirm; P_STATE=ok
    P_VERSION=$(cat "$ROOT_DIR/etc/forgefirm-version")
  elif [ -f "$ROOT_DIR/etc/version" ]; then
    P_TYPE=factory; P_STATE=ok
    # The build datetime orders releases reliably (used by -e); the
    # semantic FIRMWARE_VERSION in /etc/build is what the factory calls
    # the release and what we display (fall back to the datetime).
    P_DATE=$(cat "$ROOT_DIR/etc/version")
    FV=$(sed -n 's/^FIRMWARE_VERSION[[:space:]]*=[[:space:]]*\([^[:space:]]*\).*/\1/p' \
         "$ROOT_DIR/etc/build" 2>/dev/null)
    if [ -n "$FV" ]; then
      P_VERSION="v$FV"
    else
      P_VERSION="$P_DATE"
    fi
  else
    P_STATE=empty
  fi
  [ -f "$ROOT_DIR/boot/zImage" ] && P_KERNEL=yes
  if [ -n "$P_MOUNTED" ]; then
    umount "$ROOT_DIR" 2>/dev/null
    rmdir "$ROOT_DIR" 2>/dev/null
  fi
  return 0
}

env_get () {
  fw_printenv -c "$FWCONFIG" -n "$1" 2>/dev/null
}

# --- inventory ---------------------------------------------------------------
inventory () {
  E_DEV=$(env_get mmcdev); E_PART=$(env_get mmcpart); E_ROOT=$(env_get mmcroot)
  echo "env.mmcdev=$E_DEV"
  echo "env.mmchwpart=$(env_get mmchwpart)"
  echo "env.mmcpart=$E_PART"
  echo "env.mmcroot=$E_ROOT"
  echo "booted.root=$BOOTED_ROOT"
  for ENTRY in "sd /dev/mmcblk1p1" "a /dev/mmcblk2p1" "b /dev/mmcblk2p2" "legacy /dev/mmcblk2p4"; do
    NAME=${ENTRY%% *}; DEV=${ENTRY#* }
    probe_part "$DEV"
    echo "slot.$NAME.device=$DEV"
    echo "slot.$NAME.present=$P_PRESENT"
    [ "$P_PRESENT" = "yes" ] || continue
    echo "slot.$NAME.state=$P_STATE"
    echo "slot.$NAME.type=$P_TYPE"
    echo "slot.$NAME.version=$P_VERSION"
    echo "slot.$NAME.kernel=$P_KERNEL"
    [ "$DEV" = "$BOOTED_ROOT" ] && echo "slot.$NAME.booted=yes"
    [ "$DEV" = "$E_ROOT" ] && echo "slot.$NAME.next=yes"
  done
}

# --- verified env flip -------------------------------------------------------
# set_env <mmcdev> <mmchwpart> <mmcpart> <mmcroot>
env_verify () {
  [ "$(env_get mmcdev)" = "$1" ] && [ "$(env_get mmchwpart)" = "$2" ] && \
  [ "$(env_get mmcpart)" = "$3" ] && [ "$(env_get mmcroot)" = "$4" ]
}

set_env () {
  SCRIPT=$(mktemp /tmp/ffboot.env.XXXXXX) || return 1
  # libubootenv format
  printf 'mmcdev=%s\nmmchwpart=%s\nmmcpart=%s\nmmcroot=%s\n' "$1" "$2" "$3" "$4" > "$SCRIPT"
  fw_setenv -c "$FWCONFIG" -s "$SCRIPT" 2>/dev/null
  if env_verify "$1" "$2" "$3" "$4"; then rm -f "$SCRIPT"; return 0; fi
  # classic u-boot-tools format
  printf 'mmcdev %s\nmmchwpart %s\nmmcpart %s\nmmcroot %s\n' "$1" "$2" "$3" "$4" > "$SCRIPT"
  fw_setenv -c "$FWCONFIG" -s "$SCRIPT" 2>/dev/null
  rm -f "$SCRIPT"
  if env_verify "$1" "$2" "$3" "$4"; then return 0; fi
  # last resort: per-variable
  fw_setenv -c "$FWCONFIG" mmcdev "$1"    && \
  fw_setenv -c "$FWCONFIG" mmchwpart "$2" && \
  fw_setenv -c "$FWCONFIG" mmcpart "$3"   && \
  fw_setenv -c "$FWCONFIG" mmcroot "$4"
  env_verify "$1" "$2" "$3" "$4"
}

# --- argument parsing --------------------------------------------------------
REBOOT=1
FORCE=0
MODE=""

for ARG in "$@"; do
  case "$ARG" in
    -l) MODE=list ;;
    -n) REBOOT=0 ;;
    -f) FORCE=1 ;;
    -s|-e|-e1|-e2|-e4)
      [ -z "$MODE" ] || { usage; exit 2; }
      MODE="$ARG" ;;
    *) usage; exit 2 ;;
  esac
done
[ -n "$MODE" ] || { usage; exit 2; }

if [ ! -f "$FWCONFIG" ]; then
  echo "ERROR: $FWCONFIG not found; cannot access the U-Boot environment" >&2
  exit 1
fi

if [ "$MODE" = "list" ]; then
  inventory
  exit 0
fi

# --- resolve the switch target ----------------------------------------------
case "$MODE" in
  -s)
    MMCDEV=0; MMCPART=1 ;;
  -e)
    # Pick the factory slot with the newest firmware; a slot occupied by
    # ForgeFIRM is not a factory-restore target.
    MMCDEV=1; MMCPART=""
    BEST=0
    for CAND in 1 2; do
      probe_part "/dev/mmcblk2p$CAND" || continue
      [ "$P_TYPE" = "factory" ] || continue
      # Order by the build datetime (monotonic), not the semantic
      # version string.
      V=$(echo "$P_DATE" | tr -cd '0-9')
      [ -n "$V" ] || V=0
      if [ -z "$MMCPART" ] || [ "$V" -gt "$BEST" ]; then
        MMCPART=$CAND; BEST=$V
      fi
    done
    [ -n "$MMCPART" ] || {
      echo "ERROR: no factory image found in eMMC slot 1 or 2." >&2
      echo "Use -e1/-e2/-e4 to select a slot explicitly." >&2
      exit 1
    } ;;
  -e1) MMCDEV=1; MMCPART=1 ;;
  -e2) MMCDEV=1; MMCPART=2 ;;
  -e4) MMCDEV=1; MMCPART=4 ;;
esac

MMCROOT="/dev/mmcblk$((MMCDEV + 1))p$MMCPART"

# --- target sanity probe -----------------------------------------------------
if [ "$FORCE" -eq 0 ]; then
  if ! probe_part "$MMCROOT"; then
    echo "ERROR: $MMCROOT is missing or unreadable (use -f to override)" >&2
    exit 1
  fi
  if [ "$P_STATE" != "ok" ] || [ "$P_KERNEL" != "yes" ]; then
    echo "ERROR: $MMCROOT does not look bootable (state=$P_STATE kernel=$P_KERNEL; use -f to override)" >&2
    exit 1
  fi
  echo "Target $MMCROOT: $P_TYPE $P_VERSION"
fi

echo "Setting boot to $MMCROOT"
if ! set_env "$MMCDEV" 0 "$MMCPART" "$MMCROOT"; then
  echo "ERROR: environment write did not verify; boot selection unchanged or inconsistent" >&2
  exit 1
fi

if [ "$REBOOT" -gt 0 ]; then
  echo "Rebooting..."
  reboot
fi

exit 0
