#!/bin/sh
# Hatchetfish CPU_A/B exchange/sync implementation
# Used by initramfs/updater and at various points in the RFS (rc.S and rc.app)

set -u

VERSION=3			# API version only (not incremented after *every* change)
UPD_TMP=~/.updater
CLEANUP=$UPD_TMP/cleanup	# Run after updater has complete to tear down services (etc)
LOGFILE=/run/hf-sync.log
SYNCFAIL=/run/hf-sync.syncfail
FF_DIR=/dev/shm			# Flag File Directory
SYSCTRL_GATEID=32
TIMEOUT_MAX=65535		# Maximum timeout supported by mcu-util (in seconds. >18hours).

add_cleanup() {
  { # New commands (from stdin) and a newline
    cat; echo
    # Existing commands (if any)
    cat $CLEANUP 2>/dev/null || true
  } >$CLEANUP.new

  mv $CLEANUP.new $CLEANUP
}

run_cleanup() {
  if [ -r $CLEANUP ]; then
    # Finalise the script - add a header
    { echo "#!/bin/sh"
      [ "${DEBUG:-}" = "true" ] && echo "set -x"
    } | add_cleanup

    chmod +x $CLEANUP && $CLEANUP && rm $CLEANUP
  fi
}

log_debug() {
  tee -a $LOGFILE >&2 || true
}

print_debug() {
  local alt_dest=/dev/null

  if ! grep -q 'console=tty' /proc/cmdline; then
    # Serial console is disabled, force output on serial port 0.
    # (ttymxc0 is a link to /dev/null in the RFS when console disabled, so check it last)
    alt_dest=/dev/ttyS0
    [ -e $alt_dest ] || alt_dest=/dev/tts/0
    [ -e $alt_dest ] || alt_dest=/dev/ttymxc0
    stty -F $alt_dest 115200
  fi

  {
    echo "----cut----cut----cut----cut----"
    echo "$0 debug log (SFID=$(cut -f1 /proc/navico_platform/sub_family_id),VER=$VERSION:$(stat -c'%Y:%s' $0)):"
    cat $LOGFILE
    echo "----cut----cut----cut----cut----"
  } | tee $alt_dest
}

fixup_fields() {
  # Ensures the specfied number of fields are present, padding or truncating as required
  echo $@ | awk '{ f=$1; for (n=1;n<=f;n++) printf("%d%c", 0+$(n+1), n==f ? "\n" : " "); }'
}

mcu_util_cmdline() {
  # mcu-util is installed in /bin for initramfs, /usr/bin for RFS
  local cmdline=/bin/mcu-util
  [ -x $cmdline ] || cmdline="/usr$cmdline"
  [ $# -gt 0 ] && cmdline="$cmdline $@"
  [ -n "${MCU_TIMEOUT:-}" ] && cmdline="$cmdline --mcu-timeout $MCU_TIMEOUT"
  [ -n "${SYNC_TIMEOUT:-}" ] && cmdline="$cmdline --sync-timeout $SYNC_TIMEOUT"
  [ -n "${POLL_INTERVAL:-}" ] && cmdline="$cmdline --poll-interval $POLL_INTERVAL"
  echo "$cmdline"
}

sync_xch() {
  # input: gate-index, [input-value, combine-function, [shm_buf_ofs, shm_buf_str]]
  # return value ($?): 0=OK, 1=sync-timeout, 2=mcu-timeout
  # globals: REPLY_ALL, REPLY_SHM=shm_buf_contents, REPLY_VAL=combined_value
  # stderr: status/debug messages
  # stdout: (no output)

  local gate_id=$1
  local input_val=$(( ${2:-0} ))  cmb_funct=${3:-max}	# optional args
  local shm_buf=$(( ${4:-(-1)} )) shm_buf_str=${5:-""}	# optional args
  local ofs_a ofs_b result mcu_commands cmdline

  echo -n "Entering sync point $gate_id ($input_val) ... " | log_debug

  if [ -r $SYNCFAIL -a "${FORCE_SYNC:-}" != "true" ]; then
    # Previous sync_xch attempt failed, so assume failure and exit early
    result=100

  else
    [ $IS_CPUB = "false" ] &&
      { ofs_a=0; ofs_b=1; } ||	# CPU_A and Solo
      { ofs_a=1; ofs_b=0; }	# CPU_B

    mcu_commands=$(
      echo "t 14 0 $((4*ofs_a)) $((4*ofs_b)) 2 $gate_id 0 $((input_val%256)) $((input_val/256))"
      [ $shm_buf -gt 0 ] && echo "t 11 $((shm_buf+ofs_a)) 0 $shm_buf_str"
      echo "t 14 0 $((4*ofs_a)) $((4*ofs_b)) 2 $gate_id 1"
      [ $shm_buf -gt 0 ] && echo "t 12 $((shm_buf+ofs_b)) 0"
    )

    cmdline=$(mcu_util_cmdline)
    local retries=$(( ${XCH_RETRIES:-0} ))
    local time_output=/run/time-mcu-util.$$

    while true; do
      REPLY_ALL=$(echo "$mcu_commands" | time $cmdline 2>$time_output); result=$?

      # Add mcu-util execution time to log
      sed -ne 's/^real\s\+\(.*\)$/[\1] /p' $time_output | tr -d '\n' | log_debug
      rm -f $time_output

      if [ $result -eq 0 ]; then
	# If no errors were reported, sanity check the output:
	# - Check output is numeric only
	# - Check output has correct number of lines
	if echo "$REPLY_ALL"|grep -q '[^ 0-9]'; then
	  result=10
	elif [ $(echo "$REPLY_ALL"|wc -l) -ne $(( shm_buf>0 ? 4 : 2 )) ]; then
	  result=11
	fi
      fi

      if [ $retries -gt 0 -a $result -ne 0 ]; then
	retries=$((retries-1))
	echo -n "RETRY($((XCH_RETRIES-retries)),$result) " | log_debug
      else
	break
      fi
    done
  fi

  if [ $result -ne 0 ]; then
    REPLY_SHM=""
    REPLY_VAL=$input_val
    echo -e "$gate_id\t$result" >> $SYNCFAIL

  else
    local rmt_input=$(echo "$REPLY_ALL" | awk 'NR==1 { f='$ofs_b'; print $(4*f+3)+256*$(4*f+4); }')

    case "$cmb_funct" in
      or)	REPLY_VAL=$(( input_val | rmt_input )) ;;
      min)	REPLY_VAL=$(( input_val < rmt_input ? input_val : rmt_input )) ;;
      max)	REPLY_VAL=$(( input_val > rmt_input ? input_val : rmt_input )) ;;
      *)	REPLY_VAL=$input_val; echo "Unknown function $cmb_funct" | log_debug ;;
    esac

    REPLY_SHM=$(echo "$REPLY_ALL" | sed '4!d')
  fi

  {
    if [ $result -eq 0 ]; then
      echo "OK ($REPLY_VAL)"
    else
      echo "FAILED ($result)"
    fi

    if [ $result -ne 0 -a -n "${cmdline:-}" ]; then
      echo -e "$(uptime)\nCommand line: $cmdline\nInput commands:\n$mcu_commands\nOutput:\n$REPLY_ALL" | sed 's_^_\t_'
    fi
  } | log_debug

  return $result
}

xch_preboot() {
  # Arg: updater-count
  # Exchange updater-count as a boolean together with flags for files in /dev/shm/
  local UPD_COUNT=$1
  local flags=0

  [ $UPD_COUNT != 0 ] &&  flags=$((flags | 0x01))
  [ -e $FF_DIR/debug ] && flags=$((flags | 0x02))

  local keys=$(head -n1 $FF_DIR/bootkeys 2>/dev/null)
  keys=$(fixup_fields 8 $keys)

  if sync_xch 10 $flags or 4 "$keys"; then
    [ $((REPLY_VAL & 0x01)) -ne 0 ] && [ $UPD_COUNT -eq 0 ] && UPD_COUNT=1
    [ $((REPLY_VAL & 0x02)) -ne 0 ] && touch $FF_DIR/debug
  fi

  # Always reformat the bootkeys, even if the sync failed (for consistent behaviour after rc.checkkeys)
  # Combine the keys, sorting and removing duplicates and null-entries ("0")
  keys=$(printf "%d\n" $keys $REPLY_SHM | sort -g | uniq | sed '/^0$/d' | tr '\n' ' ' | sed 's/ $/\n/')
  # If any keys were pressed, save the result
  [ -n "$keys" ] && echo "$keys" >$FF_DIR/bootkeys

  echo $UPD_COUNT
}

xch_mcu_upd_reqd_mac() {
  # RFS only
  # Arg: mcu-update-reqd
  local MCU_UPD_REQD=$1
  local PEER_MAC_FILE=/run/peer-mac
  local PEER_MAC_BAK=/root/peer-mac.bak
  local LOCAL_MAC=$(awk '
    BEGIN { FS=":" }
    { for (i=1; i<=NF; i++) printf("%d%c", ("0x"$i), (i<NF ? " " : "\n") ); }
    ' /sys/class/net/eth0/address || echo "0 0 0 0 0 0"
  )

  if sync_xch 20 $MCU_UPD_REQD max 6 "$LOCAL_MAC"; then
    echo $REPLY_SHM | awk '{ printf("%02x:%02x:%02x:%02x:%02x:%02x\n", $1,$2,$3,$4,$5,$6 ); }' >$PEER_MAC_FILE
    cmp -s "$PEER_MAC_FILE" "$PEER_MAC_BAK" || cp -f "$PEER_MAC_FILE" "$PEER_MAC_BAK"
    
    # If an update will be performed, delay a little before programming (and resetting) the MCU.
    # This provides some timing margin for CPU_B to complete the final sync_xch command.
    [ $REPLY_VAL -ne 0 ] && sleep 2
  else
    if cp -f "$PEER_MAC_BAK" "$PEER_MAC_FILE" 2>/dev/null; then
      echo "Warning: Peer MAC address retrieved from backup" | log_debug
    fi
  fi

  echo $REPLY_VAL
}

xch_mcu_upd_reqd() {
  # Simplified version of xch_mcu_upd_reqd_mac() for use by initramfs/updater.
  # (Peer-mac isn't exchanged and timeouts are different)
  # Arg: mcu-update-reqd
  local MCU_UPD_REQD=$1

  SYNC_TIMEOUT=60

  if sync_xch 21 $MCU_UPD_REQD max; then
    # If an update will be performed, delay a little before programming (and resetting) the MCU.
    # This provides some timing margin for CPU_B to complete the final sync_xch command.
    [ $REPLY_VAL -ne 0 ] && sleep 2
  fi

  unset SYNC_TIMEOUT

  echo $REPLY_VAL
}

post_mcu_upd_sync() {
  if [ $IS_CPUB = "true" ]; then
    XCH_RETRIES=30
  else
    XCH_RETRIES=5
  fi

  SYNC_TIMEOUT=1
  MCU_TIMEOUT=1000	# ms
  POLL_INTERVAL=250	# ms

  sync_xch 22

  unset XCH_RETRIES SYNC_TIMEOUT MCU_TIMEOUT POLL_INTERVAL
}

if_up() {
  # This function is only used in the initrd
  # The file path is prefixed with /mnt to match the entry in /etc/fstab
  local MAC_FILE=/mnt/etc/NOS/MAC
  local MAC_MOUNT=$(dirname $MAC_FILE)
  local UMOUNT_REQD=false

  if ! mountpoint -q "$MAC_MOUNT"; then
    mkdir -p "$MAC_MOUNT" && mount "$MAC_MOUNT" && UMOUNT_REQD=true
  fi

  local MAC=$(grep -s -m 1 "^..:..:..:..:..:..$" "$MAC_FILE")

  [ "$UMOUNT_REQD" = "true" ] && umount "$MAC_MOUNT"

  if [ -z "$MAC" ]; then
    echo "Warning: Generating random MAC address" | log_debug
    # Seed the generator with the boot ID
    local RANDOM=$(cat /proc/sys/kernel/random/boot_id)
    # SMSC address block (Organisationally Unique Identifier)
    local OUI="00:80:0f"
    local NIC_1=$((${RANDOM}%256))
    local NIC_2=$((${RANDOM}%256))
    local NIC_3=$((${RANDOM}%256))
    # Ensure CPU A/B have differing addresses
    $IS_CPUA && NIC_3=$(( NIC_3 & 0xFE ))
    $IS_CPUB && NIC_3=$(( NIC_3 | 0x01 ))
    MAC=$(printf '%s:%02x:%02x:%02x' $OUI $NIC_1 $NIC_2 $NIC_3)
  fi

  if (
    set -e
    # Bring up the interfaces
    ifconfig lo up
    ifconfig eth0 hw ether $MAC
    ifconfig eth0 up
    # Run in foreground, quit once address obtained
    zcip -fqv eth0 /usr/libexec/busybox/zcip.sh
    )
  then
    # Queue shutdown commands
    add_cleanup <<-EOCMDS
	ifconfig eth0 down
	ifconfig lo down
EOCMDS

  else
    local error=$?
    echo "if_up failed ($error)" | log_debug
    return $error
  fi
}

upd_bootstrap() {
  # Establish the NFS share and copy files in $BOOTSTRAP directory from CPU_A -> CPU_B
  local flags=0 if_up_err=${1:-0}
  local BOOTSTRAP=$UPD_TMP/bootstrap
  local sync_xch_args=""

  [ $if_up_err -ne 0 ] &&	flags=$((flags | 0x01))
  mkdir -p $BOOTSTRAP ||	flags=$((flags | 0x02))

  if [ $IS_CPUA = "true" ]; then
    local PORT=60000
    local IPADDR=$(ifconfig eth0 | sed -ne 's/.*inet addr:\([0-9.]*\).*/\1/p')
    local UPDFILE=$(head -n1 $UPD_TMP/runupdate)
    local EXPORT=$(dirname "$UPDFILE")
    local TOOLS=hf-tools.tar.xz

    (
      set -e

      # Create the init file that CPU_B will use to find the updater
      cat >$BOOTSTRAP/init <<-EOCMDS
	#!/bin/sh

	set -e
	mkdir -p "$EXPORT"
	mount -t nfs -o ro,nolock "$IPADDR:$EXPORT" "$EXPORT"
	echo "$UPDFILE" >$UPD_TMP/runupdate

	cat >&3 <<EO_CLEANUP || echo "Warning: unable to add cleanup commands"
		# \$0 cleanup commands:
		umount -f "$EXPORT" || true
	EO_CLEANUP

	echo "\$0 completed successfully"
EOCMDS

      # Start a simple netcat-based tarball file sharing service
      # This is only used to fetch the init script
      # (The path is deliberately not transferred as part of the tarball)
      nc -ll -p $PORT -e tar -C $BOOTSTRAP -cf - . >/dev/null 2>&1 &
      echo "kill $! # bootstrap nc -l" | add_cleanup

      # Unpack the tools tarball and verify it's sha1sum against update.sh (which has already been validated)
      local act_sha1=$(tar -xOf "$UPDFILE" $TOOLS | sha1sum)
      local ref_sha1=$(tar -xOf "$UPDFILE" update.sh | sed -ne '/^###SHA1MAGIC###$/,$ s/\b'$TOOLS'$/-/p')
      [ "$act_sha1" = "$ref_sha1" ]
      # Validation passed - unpack the tarball
      tar -xOf "$UPDFILE" $TOOLS | xzcat | tar -xC /

      # Start the NFS server
      if [ -x /usr/sbin/hf-tools-init ]; then
	/usr/sbin/hf-tools-init
      else
	mkdir -p /var/run
	modprobe nfsd
	mount -t nfsd nfsd /proc/fs/nfs
	rpcbind
	rpc.nfsd
	rpc.mountd
	add_cleanup <<-EOCMDS
		killall rpc.mountd nfsd
		exportfs -ua -f
		sleep 0.5
		killall -9 rpc.mountd nfsd || true
		umount /proc/fs/nfs
		modprobe -wr nfsd
		killall rpcbind
EOCMDS
      fi

      # Share the update media
      # (fsid=0 is required when the export is a tmpfs)
      exportfs -o ro        "169.254.0.0/16:$EXPORT" ||
      exportfs -o ro,fsid=0 "169.254.0.0/16:$EXPORT"

      # Test the netcat service
      nc $IPADDR $PORT >/dev/null
    )
    local error=$?

    if [ $error -eq 0 ]; then
      sync_xch_args="${IPADDR//./ } $((PORT%256)) $((PORT/256))"
    else
      echo "upd_bootstrap failed ($error)" | log_debug
      flags=$((flags | 0x04))
    fi
  fi

  if sync_xch 14 $flags or 2 "$sync_xch_args"; then
    flags=$((REPLY_VAL))
  else
    flags=$((flags | 0x08))
  fi

  if [ $IS_CPUB = "true" ] && [ $((flags & 0x0F)) -eq 0 ]; then
    # Get IP address and PORT from SHM_BUF
    local IP_PORT=$(echo $REPLY_SHM | awk '{ printf("%d.%d.%d.%d %d\n", $1,$2,$3,$4, $5+256*$6); }')

    nc $IP_PORT | tar -C $BOOTSTRAP -xf -

    # Run the init script prepared by CPU_A
    if chmod +x $BOOTSTRAP/init; then
      # Anything sent to fd3 is added to the (local) cleanup file
      exec 9>&1
      $BOOTSTRAP/init 3>&1 1>&9 | add_cleanup
      exec 9>&-
    fi
  fi

  return $((flags & 0x0F))
}

showmsg() {
  local tty=/dev/tty1

  {
    local size_x=$(stty size|cut -d' ' -f2)
    local size_y=$(stty size|cut -d' ' -f1)
    local line=$(((size_y-${#})/2))
    clear
    for msg; do
      echo -e "\033[$line;$(((size_x-${#msg})/2))H${msg}"
      line=$((line+1))
    done
  } <$tty >$tty
}

do_selectupdate() {
  local upd_selected=0
  local msg_1="" msg_2=""
  local msg_preparing="Preparing to update, please wait..."

  if [ $IS_CPUB = "true" ]; then
    msg_1="Please select update on primary display."
  else
    # Solo and CPU_A

    # Ensure both USB ports are mapped to CPU_A.
    echo "t 9 1 1" | $(mcu_util_cmdline)

    selectupdate

    if [ -r $UPD_TMP/runupdate ]; then
      upd_selected=1
      msg_1=$msg_preparing
    fi
  fi

  if [ $IS_SOLO = "false" -a ! \( -r $SYNCFAIL -a "${FORCE_SYNC:-}" != "true" \) ]; then
    showmsg "$msg_1"

    # CPU_B needs an extended timeout as it will wait here until user selects an updater on CPU_A
    [ $IS_CPUB = "true" ] && SYNC_TIMEOUT=$TIMEOUT_MAX

    if sync_xch 12 $upd_selected max; then
      if [ $REPLY_VAL -gt 0 ]; then
	mkdir -p $UPD_TMP
	touch $UPD_TMP/runupdate
	msg_2=$msg_preparing
      fi
    fi

    unset SYNC_TIMEOUT

    [ "$msg_1" != "$msg_2" ] && showmsg "$msg_2"
  fi
}

do_runupdate() {
  if [ -r $SYNCFAIL -a "${FORCE_SYNC:-}" != "true" ]; then
    # This path allows CPU_A to be updated in cases where it can't communicate with CPU_B
    echo "Skipping update-bootstrap process due to previous errors" | log_debug
    runupdate

  else
    # Run the bootstrap process
    # If a setup error occurs on either CPU the result from upd_bootstrap will be non-zero
    if_up
    upd_bootstrap $?

    if [ $? -ne 0 ]; then
      showmsg "Unable to apply update, file may be corrupted." "No changes were made to your system."
      print_debug
      sleep 42d

    else
      showmsg ""
      runupdate

      if [ $IS_CPUB = "true" ]; then
	run_cleanup
	showmsg "Upgrade completed."
	# CPU_B needs an extended timeout as it will wait here until user removes SD card (from CPU_A)
	SYNC_TIMEOUT=$TIMEOUT_MAX
      else
	SYNC_TIMEOUT=60
      fi

      sync_xch 18
      unset SYNC_TIMEOUT

      [ $IS_CPUA = "true" ] && run_cleanup
      [ $IS_CPUB = "true" ] && showmsg ""
    fi
  fi

  [ "${DEBUG:-}" = "true" -o -r $SYNCFAIL ] && print_debug
}

finalise_update() {
  # This is run from within the updater (update.sh) immediately prior to the media-removal prompt
  # The do_runupdate() sync_xch occurs afterward
  # Arg: [update-result]
  local UPD_RESULT=$(( ${1:-0} ))

  SYNC_TIMEOUT=$TIMEOUT_MAX
  if sync_xch 16 $UPD_RESULT max; then
    # Allow CPU_B to run_cleanup before presenting user with media-removal prompt on CPU_A
    [ $IS_CPUA = "true" ] && sleep 2
  fi
  unset SYNC_TIMEOUT

  echo $REPLY_VAL
}

xch_rc_app() {
  local gate=$1 code=$2

  SYNC_TIMEOUT=30
  sync_xch $gate $code min
  unset SYNC_TIMEOUT

  echo $REPLY_VAL
}

process_alive() {
  kill -0 $1 2>/dev/null
}

app_monitor() {
  local pid=$1
  local kill_delay=$(( ${2:-0} / 100 ))	# Convert kill_delay to units of 100ms
  local poll_interval=1			# busybox "read" does not support fractional timeouts
  local fifo=/run/mcu-util-fifo.$pid
  local result=2 			# 0=App killed, 1=App exited by itself 2=setup failure

  if rm -f $fifo && mkfifo $fifo; then
    result=1

    <$fifo $(mcu_util_cmdline -l) | {
      exec 9>$fifo
      rm $fifo

      local reply_fields
      # CPU_A: "BUF_0: x x x x 32 0 x x"
      # CPU_B: "BUF_0: 32 0 x x x x x x"
      [ $IS_CPUB = "false" ] &&
	reply_fields="1,6,7" ||
	reply_fields="1,2,3"

      while process_alive $pid; do
	if read -t $poll_interval; then
	  if [ "$(echo "$REPLY" | cut -d' ' -f $reply_fields)" = "BUF_0: $SYSCTRL_GATEID 0" ]; then
	    echo "Peer sync request while running pid $pid: [$REPLY]" | log_debug
	    break
	  fi
	fi
      done

      exec 9>&-	# Quit mcu-util
    }

    while process_alive $pid; do
	# once only
	if [ $kill_delay -eq 0 ]; then
	    echo "Killing pid $pid" | log_debug
	    killtree $pid
	    result=0
	fi
	kill_delay=$((kill_delay-1))
	sleep 0.1
    done
  fi

  return $result
}

usb_switcher() {
  local poll_interval=${1:-1}
  local cmdline=$(mcu_util_cmdline)

  while sleep $poll_interval; do
    local cmd=""

    # event0 will always be gpiokeys - no need to poll them
    for dev in /dev/input/event[1-9]*; do
      case $(inpdev --showkeys --device $dev 2>/dev/null) in
	"29 56 59"|"59 97 100")	cmd="t 9 1 1" ;; # Ctrl-Alt-F1 = CPU_A,CPU_A
	"29 56 60"|"60 97 100")	cmd="t 9 0 0" ;; # Ctrl-Alt-F2 = CPU_B,CPU_B
	"29 56 61"|"61 97 100")	cmd="t 9 0 1" ;; # Ctrl-Alt-F3 = CPU_A,CPU_B
	"29 56 62"|"62 97 100")	cmd="t 9 1 0" ;; # Ctrl-Alt-F4 = CPU_B,CPU_A
      esac
    done

    [ -n "$cmd" ] && echo "$cmd" | $cmdline
  done
}

case "$(cut -f1 /proc/navico_platform/model_id)" in
  224) IS_SOLO=true  IS_CPUA=false IS_CPUB=false ;; # 0xE0 = Single CPU baseboard
  225) IS_SOLO=false IS_CPUA=true  IS_CPUB=false ;; # 0xE1 = Dual CPU baseboard, card A
  226) IS_SOLO=false IS_CPUA=false IS_CPUB=true  ;; # 0xE2 = Dual CPU baseboard, card B
    *) IS_SOLO=false IS_CPUA=false IS_CPUB=false ;; # Not a Hatchetfish
esac

case ${1:-} in
  # Common (initramfs/RFS) commands
  is_solo)		$IS_SOLO ;;
  is_cpu_a)		$IS_CPUA ;;
  is_cpu_b)		$IS_CPUB ;;
  *version)		echo $VERSION ;;
  post_mcu_upd_sync)	post_mcu_upd_sync ;;

  # initramfs/updater commands
  xch_preboot)		shift && xch_preboot "$@"; exit 0 ;;
  selectupdate)		do_selectupdate; exit 0 ;;
  runupdate)		do_runupdate; exit 0 ;;
  finalise_update)	finalise_update; exit 0 ;;
  xch_mcu_upd_reqd)	shift && xch_mcu_upd_reqd "$@" ;;

  # RFS commands
  xch_mcu_upd_reqd_mac)	shift && xch_mcu_upd_reqd_mac "$@" ;;
  app_monitor)		shift && app_monitor "$@" ;;
  usb_switcher)		shift && usb_switcher "$@" ;;
  xch_app_mode)		shift && xch_rc_app 30 "$@" ;;
  xch_sys_ctrl)		shift && xch_rc_app $SYSCTRL_GATEID "$@" ;;

  # Debug/test commands
  _add_cleanup)		add_cleanup ;;
  _upd_bootstrap)	upd_bootstrap ;;
  _if_up)		if_up ;;
  _showmsg)		shift && showmsg "$@" ;;
  print_debug)		print_debug ;;
  sourced)		: ;; # Do nothing (for script inclusion/testing)

  *)
    printf '%s: Unsupported arguments "%s"\n' "${0##*/}" "$*" >&2
    exit 1
    ;;
esac
