#!/bin/sh
# Switch only the USB-C controller. The stock network gadget stays intact.
set -eu
BASE=$(CDPATH= cd -- "$(dirname -- "$(readlink -f "$0")")/.." && pwd)
PATH="$BASE/bin:/home/root/.local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
export PATH
G=/sys/kernel/config/usb_gadget
NET=$G/g_ether
HID=$G/rm_keyboard
RUN=/run/rm-usb
FFS=$RUN/ffs
ROLE=/sys/bus/platform/devices/ci_hdrc.0/role
UDC=ci_hdrc.0

fail() { echo "usb-mode: $*" >&2; exit 1; }
usage() {
    cat <<'EOF'
Usage: usb-mode 1|network          Restore the original USB Ethernet gadget
       usb-mode 2|receive-keyboard Accept a USB keyboard (powered OTG adapter)
       usb-mode 3|send-keyboard    Present the tablet as a USB keyboard
       usb-mode 4|modem            Select the USB modem without dialing
       usb-mode 5|gotenna          Select goTenna Mesh radio mode without connecting
       usb-mode status|doctor
       usb-mode recover-after SECONDS   Schedule an automatic return to Ethernet
Modes reset to the original Ethernet configuration on reboot.
Run USB changes over Wi-Fi SSH or from the tablet itself.
Use keyboard-send to send text; modem-data policy on|auto to connect, off to hang up.
Short aliases host, keyboard, and the old satellite name are also accepted.

goTenna and goTenna Mesh are trademarks of goTenna, Inc.
Inkline Outpost is independent and is not affiliated with or endorsed by goTenna, Inc.
EOF
}
role() { cat "$ROLE"; }
set_role() {
    [ "$(role)" = "$1" ] || printf '%s\n' "$1" > "$ROLE"
    [ "$(role)" = "$1" ] || fail "Controller did not enter $1 mode."
}
unbind() { [ ! -f "$1/UDC" ] || [ -z "$(cat "$1/UDC")" ] || printf '\n' > "$1/UDC"; }
stop_keyboard() {
    unbind "$HID"
    systemctl stop rm-usb-keyboard.service 2>/dev/null || :
    rm -f "$RUN/ready" "$RUN/enabled"
}
network() {
    if [ -x "$BASE/bin/gotenna-data" ]; then "$BASE/bin/gotenna-data" disconnect; fi
    "$BASE/bin/modem-data" release
    stop_keyboard
    set_role gadget
    [ -d "$NET" ] || fail "Stock g_ether is missing; reboot to recreate it."
    [ "$(cat "$NET/UDC")" = "$UDC" ] || printf '%s\n' "$UDC" > "$NET/UDC"
    # A controller-driver reset can leave netdevs with dangling sysfs paths.
    # Their SSH sockets cannot start without those device records.
    for interface in usb0 usb1; do
        [ -r "/sys/class/net/$interface/uevent" ] || \
            fail "USB interface $interface has missing kernel records. Save your work and reboot the tablet."
    done
    systemctl start dropbear-usb0.socket dropbear-usb1.socket
    for interface in usb0 usb1; do
        systemctl is-active --quiet "dropbear-$interface.socket" || \
            fail "USB SSH on $interface did not start; check systemctl status dropbear-$interface.socket."
    done
    printf 'network\n' > "$RUN/mode"
}
host() {
    if [ -x "$BASE/bin/gotenna-data" ]; then "$BASE/bin/gotenna-data" disconnect; fi
    "$BASE/bin/modem-data" release
    stop_keyboard
    unbind "$NET"
    set_role host
    printf 'host\n' > "$RUN/mode"
}
keyboard() {
    command -v python3 >/dev/null || fail "python3 is required."
    if [ -x "$BASE/bin/gotenna-data" ]; then "$BASE/bin/gotenna-data" disconnect; fi
    "$BASE/bin/modem-data" release
    stop_keyboard
    # Prepare the unbound gadget before taking down USB Ethernet.
    mkdir -p "$HID"
    printf '0x1d6b\n' > "$HID/idVendor"
    printf '0x0104\n' > "$HID/idProduct"
    printf '0x0200\n' > "$HID/bcdUSB"
    printf '0x0100\n' > "$HID/bcdDevice"
    mkdir -p "$HID/strings/0x409" "$HID/configs/c.1/strings/0x409"
    printf 'rm-usb-local-1\n' > "$HID/strings/0x409/serialnumber"
    printf 'Personal USB tools\n' > "$HID/strings/0x409/manufacturer"
    printf 'reMarkable Keyboard\n' > "$HID/strings/0x409/product"
    printf 'Keyboard\n' > "$HID/configs/c.1/strings/0x409/configuration"
    printf '250\n' > "$HID/configs/c.1/MaxPower"
    mkdir -p "$HID/functions/ffs.rmkeys" "$FFS"
    [ -L "$HID/configs/c.1/ffs.rmkeys" ] || ln -s "$HID/functions/ffs.rmkeys" "$HID/configs/c.1/ffs.rmkeys"
    mountpoint -q "$FFS" || mount -t functionfs rmkeys "$FFS"
    systemctl reset-failed rm-usb-keyboard.service 2>/dev/null || :
    systemctl start rm-usb-keyboard.service
    count=0
    until [ -f "$RUN/ready" ]; do
        systemctl is-active --quiet rm-usb-keyboard.service || fail "Keyboard daemon failed; see journalctl -u rm-usb-keyboard."
        count=$((count+1)); [ "$count" -lt 100 ] || fail "Keyboard daemon timed out."
        sleep .1
    done
    unbind "$NET"
    set_role gadget
    printf '%s\n' "$UDC" > "$HID/UDC"
    printf 'keyboard\n' > "$RUN/mode"
}
status() {
    printf 'USB-C role: '; role
    for gadget in "$NET" "$HID"; do
        if [ -f "$gadget/UDC" ]; then printf '%s bound to: %s\n' "${gadget##*/}" "$(cat "$gadget/UDC")"; fi
    done
    if [ "$(role)" = host ]; then
        found=0
        for dev in /sys/bus/usb/devices/*; do
            [ -f "$dev/idVendor" ] || continue
            case "${dev##*/}" in usb*) continue;; esac
            found=1
            printf 'USB peripheral %s: %s:%s %s\n' "${dev##*/}" \
                "$(cat "$dev/idVendor")" "$(cat "$dev/idProduct")" \
                "$(cat "$dev/product" 2>/dev/null || :)"
        done
        [ "$found" -ne 0 ] || echo 'No USB peripherals detected.'
    else
        printf 'USB connection to computer: '; cat /sys/class/udc/ci_hdrc.0/state 2>/dev/null || :
    fi
    for interface in usb0 usb1; do
        printf 'USB SSH (%s): ' "$interface"
        systemctl is-active "dropbear-$interface.socket" || :
    done
    printf 'Wi-Fi SSH: '; systemctl is-active dropbear-wlan.socket || :
    ip -4 addr show dev wlan0 | sed -n '/inet /p'
    printf 'Keyboard daemon: '; systemctl is-active rm-usb-keyboard.service || :
    printf 'PPP daemon: '; systemctl is-active rm-usb-ppp.service || :
    for dev in /dev/ttyACM* /dev/ttyUSB*; do [ ! -c "$dev" ] || echo "Serial device: $dev"; done
    return 0
}

action=${1:-help}; [ "$#" -eq 0 ] || shift
case "$action" in
    receive-keyboard) action=host;;
    send-keyboard) action=keyboard;;
    satellite) action=modem;;
esac
case "$action" in help|-h|--help) usage; exit 0;; esac
[ "$(id -u)" -eq 0 ] || fail "Run this on the tablet as root."
[ -f "$ROLE" ] || fail "This firmware has no supported USB-C role interface."
mkdir -p "$RUN"
chmod 700 "$RUN"
case "$action" in
    status) status; exit 0;;
    doctor)
        status
        printf 'Kernel: '; uname -r
        printf 'Kernel lockdown: '; cat /sys/kernel/security/lockdown 2>/dev/null || :
        if [ "$(role)" = host ]; then
            python3 "$BASE/lib/usb-port-status.py" || :
        fi
        "$BASE/bin/modem-data" check
        exit $?;;
    recover-after)
        [ "$#" -eq 1 ] || fail "Usage: usb-mode recover-after SECONDS"
        case "$1" in ''|*[!0-9]*) fail "SECONDS must be an integer.";; esac
        [ "$1" -ge 10 ] && [ "$1" -le 3600 ] || fail "Use 10..3600 seconds."
        systemd-run --unit="rm-usb-recover-$(date +%s)" --on-active="${1}s" "$BASE/bin/usb-mode" network
        exit 0;;
    1|network|2|host|3|keyboard) [ "$#" -eq 0 ] || fail "Unexpected arguments.";;
    5|gotenna) [ "$#" -eq 0 ] || fail "Unexpected arguments."; "$BASE/bin/gotenna-data" check;;
    4|modem) [ "$#" -eq 0 ] || fail "Unexpected arguments."; "$BASE/bin/modem-data" check;;
    *) usage >&2; exit 2;;
esac
# Refuse to cut off the session delivering the command. Recovery/network is safe.
case "$action:${SSH_CONNECTION:-}" in
    1:*|network:*) ;;
    *10.11.99.1\ 22*) fail "Connect with ssh remarkable-wifi before changing USB mode.";;
esac
exec 9>"$RUN/mode.lock"
flock -x 9
rollback() {
    rc=$?
    trap - EXIT HUP INT TERM
    if [ "$rc" -ne 0 ]; then
        echo 'USB switch failed; restoring USB Ethernet.' >&2
        (set +e; network) || :
    fi
    exit "$rc"
}
trap rollback EXIT
trap 'exit 130' INT
trap 'exit 143' HUP TERM
case "$action" in
    1|network) network; echo 'Original USB Ethernet restored (10.11.99.1).';;
    2|host) host; echo 'USB-C host mode ready. Connect a keyboard with an externally powered OTG adapter.';;
    3|keyboard) keyboard; echo 'USB keyboard ready. Use keyboard-send --file FILE --cps 20.';;
    5|gotenna) host; printf 'gotenna\n' > "$RUN/mode"; echo 'goTenna Mesh radio mode selected. Connect from USB settings.';;
    4|modem)
        host
        "$BASE/bin/modem-data" select
        printf 'modem\n' > "$RUN/mode"
        echo 'Modem selected. Use Inkline USB settings to choose Off, On or Auto.';;
esac
