120 lines
4.2 KiB
Bash
Executable File
120 lines
4.2 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# reset_anker_usb.sh — unbind+rebind snd-usb-audio for an Anker USB device.
|
||
#
|
||
# WHY THIS EXISTS
|
||
# The Anker PowerConf A3321 on this Jetson sometimes enumerates with only
|
||
# output USB Audio Class descriptors (no capture interface). PulseAudio
|
||
# then shows the card with only output-only profiles and the dashboard
|
||
# can't expose the mic. Restarting PulseAudio does nothing — UAC
|
||
# descriptors are parsed by snd-usb-audio at probe time, persist in
|
||
# kernel structs, and only get re-parsed on a fresh driver bind.
|
||
#
|
||
# `/api/audio/usb-reset` writes directly to
|
||
# /sys/bus/usb/drivers/snd-usb-audio/{unbind,bind} when possible. That
|
||
# path needs root. This script exists as a sudo fallback so the dashboard
|
||
# can recover without Sanad itself running as root.
|
||
#
|
||
# USAGE
|
||
# reset_anker_usb.sh <bus_id> — unbind+rebind given device
|
||
# (bus_id like "1-3")
|
||
# reset_anker_usb.sh --setup-sudoers — install one-time NOPASSWD entry
|
||
# (must be run via sudo)
|
||
#
|
||
# Exit codes:
|
||
# 0 — unbind + rebind both succeeded
|
||
# 1 — bus_id missing or device not present
|
||
# 2 — no snd-usb-audio interfaces bound to that device
|
||
# 3 — unbind or bind sysfs write failed
|
||
# 4 — --setup-sudoers used outside of sudo
|
||
|
||
set -u
|
||
|
||
USAGE="usage: $(basename "$0") <bus_id> or $(basename "$0") --setup-sudoers"
|
||
|
||
if [ "$#" -lt 1 ]; then
|
||
echo "$USAGE" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# ───────────────────── --setup-sudoers ─────────────────────
|
||
if [ "$1" = "--setup-sudoers" ]; then
|
||
if [ "$(id -u)" -ne 0 ]; then
|
||
echo "❌ --setup-sudoers must run as root (use: sudo $0 --setup-sudoers)" >&2
|
||
exit 4
|
||
fi
|
||
# Install a NOPASSWD entry so the unitree user can invoke THIS exact
|
||
# script path with sudo without typing a password. Scoped to one
|
||
# binary; not a blanket sudo grant.
|
||
SELF_PATH="$(readlink -f "$0")"
|
||
SUDO_FILE="/etc/sudoers.d/sanad-anker-usb-reset"
|
||
cat > "$SUDO_FILE" <<EOF
|
||
# Installed by reset_anker_usb.sh --setup-sudoers
|
||
# Allows the unitree user to unbind+rebind snd-usb-audio scoped to the
|
||
# Anker dongle via this script ONLY — no other sudo privilege granted.
|
||
unitree ALL=(root) NOPASSWD: ${SELF_PATH}
|
||
EOF
|
||
chmod 0440 "$SUDO_FILE"
|
||
echo "✅ Installed sudoers entry at $SUDO_FILE"
|
||
echo " Dashboard's /api/audio/usb-reset can now recover the Anker without password."
|
||
exit 0
|
||
fi
|
||
|
||
BUS_ID="$1"
|
||
|
||
# Validate the device exists.
|
||
DEV_PATH="/sys/bus/usb/devices/${BUS_ID}"
|
||
if [ ! -d "$DEV_PATH" ]; then
|
||
echo "❌ USB device $BUS_ID not found at $DEV_PATH" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# Discover snd-usb-audio interfaces on this device. Don't unbind anything
|
||
# else (some Anker firmwares present HID-Consumer for the mute button on
|
||
# a separate interface — we leave those alone).
|
||
declare -a IFACES=()
|
||
for iface_path in "${DEV_PATH}/${BUS_ID}:"*; do
|
||
[ -e "$iface_path" ] || continue
|
||
driver_link="${iface_path}/driver"
|
||
[ -L "$driver_link" ] || continue
|
||
driver=$(basename "$(readlink "$driver_link")")
|
||
if [ "$driver" = "snd-usb-audio" ]; then
|
||
IFACES+=("$(basename "$iface_path")")
|
||
fi
|
||
done
|
||
|
||
if [ "${#IFACES[@]}" -eq 0 ]; then
|
||
echo "❌ No snd-usb-audio interfaces bound to device $BUS_ID" >&2
|
||
exit 2
|
||
fi
|
||
|
||
echo "ℹ️ Re-binding snd-usb-audio for $BUS_ID (interfaces: ${IFACES[*]})"
|
||
|
||
UNBIND="/sys/bus/usb/drivers/snd-usb-audio/unbind"
|
||
BIND="/sys/bus/usb/drivers/snd-usb-audio/bind"
|
||
|
||
# Unbind first; on failure exit before rebind so we don't leave the device
|
||
# in a half-bound state.
|
||
for iface in "${IFACES[@]}"; do
|
||
if ! echo -n "$iface" > "$UNBIND" 2>/dev/null; then
|
||
echo "❌ unbind failed: $iface → $UNBIND" >&2
|
||
exit 3
|
||
fi
|
||
echo " unbound: $iface"
|
||
done
|
||
|
||
# Brief settle — snd-usb-audio's release path tears down ALSA card N.
|
||
sleep 0.5
|
||
|
||
for iface in "${IFACES[@]}"; do
|
||
if ! echo -n "$iface" > "$BIND" 2>/dev/null; then
|
||
echo "❌ rebind failed: $iface → $BIND" >&2
|
||
exit 3
|
||
fi
|
||
echo " bound: $iface"
|
||
done
|
||
|
||
# Let probe complete so callers can pactl list cards right after.
|
||
sleep 1.0
|
||
echo "✅ snd-usb-audio re-bound for $BUS_ID"
|
||
exit 0
|