#!/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 — 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") 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" <&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