#!/usr/bin/env bash # lesavka‑core.sh - one‑shot USB‑gadget bring‑up (Pi‑5 / Arch‑ARM) # Presents: • Boot‑protocol keyboard (hidg0) # • Boot‑protocol mouse (hidg1) # • Stereo UAC2 speaker + microphone set -euo pipefail log() { printf '[lesavka-core] %s\n' "$*"; } G=/sys/kernel/config/usb_gadget/lesavka find_udc() { ls /sys/class/udc 2>/dev/null | head -n1 || true } udc_state() { local udc="$1" if [[ -z $udc ]]; then echo "unknown" return 0 fi cat "/sys/class/udc/$udc/state" 2>/dev/null || echo "unknown" } is_attached_state() { case "$1" in configured|addressed|default|suspended) return 0 ;; esac return 1 } detach_gadget() { local udc="" udc="$(find_udc)" local state state="$(udc_state "$udc")" case "$state" in configured|addressed|default|suspended) log "detach skipped (state=$state)" return 0 ;; esac if [[ -n $udc && -w /sys/class/udc/$udc/soft_connect ]]; then echo 0 >"/sys/class/udc/$udc/soft_connect" 2>/dev/null || true fi if [[ -n ${LESAVKA_DETACH_CLEAR_UDC:-} && -e $G/UDC ]]; then echo "" >"$G/UDC" 2>/dev/null || true fi if [[ -n $udc ]]; then log "detached (state=${state:-unknown})" else log "detached (no UDC)" fi } attach_gadget() { if [[ ! -d $G ]]; then log "gadget path missing; need full setup" return 1 fi local udc="" udc="$(find_udc)" if [[ -z $udc ]]; then log "UDC not found; need full setup" return 1 fi if [[ -n $udc && -w /sys/class/udc/$udc/soft_connect ]]; then echo 1 >"/sys/class/udc/$udc/soft_connect" 2>/dev/null || true fi if [[ -n ${LESAVKA_ATTACH_WRITE_UDC:-} && -e $G/UDC ]]; then echo "$udc" >"$G/UDC" 2>/dev/null || true fi log "attached to $udc" return 0 } case "${1:-}" in --detach) detach_gadget exit 0 ;; --detach-hard) LESAVKA_DETACH_CLEAR_UDC=1 detach_gadget exit 0 ;; --attach) if attach_gadget; then exit 0 fi ;; --help|-h) echo "Usage: $0 [--attach|--detach|--detach-hard]" exit 0 ;; esac cleanup() { if [[ -n ${LESAVKA_ALLOW_GADGET_RESET:-} ]]; then LESAVKA_DETACH_CLEAR_UDC=1 detach_gadget else detach_gadget fi } DISABLE_UAC=${LESAVKA_DISABLE_UAC:-} DISABLE_UVC=${LESAVKA_DISABLE_UVC:-} ALLOW_RESET=${LESAVKA_ALLOW_GADGET_RESET:-} UVC_FALLBACK=${LESAVKA_UVC_FALLBACK:-1} UVC_STREAMING_INTERVAL=${LESAVKA_UVC_STREAMING_INTERVAL:-1} UVC_MAXPACKET=${LESAVKA_UVC_MAXPACKET:-1024} UVC_MAXBURST=${LESAVKA_UVC_MAXBURST:-1} UVC_INTERVAL=${LESAVKA_UVC_INTERVAL:-} UVC_WIDTH=${LESAVKA_UVC_WIDTH:-1280} UVC_HEIGHT=${LESAVKA_UVC_HEIGHT:-720} UVC_FPS=${LESAVKA_UVC_FPS:-25} UVC_DISABLE_IRQ=${LESAVKA_UVC_DISABLE_IRQ:-} UVC_BULK=${LESAVKA_UVC_BULK:-} UVC_CODEC=${LESAVKA_UVC_CODEC:-yuyv} uvc_fifo_min() { local path="$1" local raw="" raw="$(cat "$path" 2>/dev/null || true)" if [[ -z $raw ]]; then return 0 fi echo "$raw" | tr ', ' '\n' | awk 'NF{print $1}' | awk ' $1 > 0 { if (min == "" || $1 < min) min = $1 } END { if (min != "") print min }' } uvc_fifo_min_debugfs() { local path="$1" awk -F': ' '/^g_tx_fifo_size\\[/{print $2}' "$path" 2>/dev/null | awk ' $1 > 0 { if (min == "" || $1 < min) min = $1 } END { if (min != "") print min }' } uvc_fifo_np_debugfs() { local path="$1" awk -F': ' '/^g_np_tx_fifo_size/{print $2; exit}' "$path" 2>/dev/null } compute_uvc_payload_cap() { UVC_PAYLOAD_CAP="" UVC_PAYLOAD_SRC="" UVC_PAYLOAD_PCT="" UVC_FIFO_PERIODIC="$(uvc_fifo_min /sys/module/dwc2/parameters/g_tx_fifo_size)" UVC_FIFO_NP="$(uvc_fifo_min /sys/module/dwc2/parameters/g_np_tx_fifo_size)" UVC_FIFO_SRC_PERIODIC="" UVC_FIFO_SRC_NP="" if [[ -n $UVC_FIFO_PERIODIC ]]; then UVC_FIFO_SRC_PERIODIC="dwc2.params" fi if [[ -n $UVC_FIFO_NP ]]; then UVC_FIFO_SRC_NP="dwc2.params" fi UVC_UDC="$(ls /sys/class/udc 2>/dev/null | head -n1 || true)" if [[ -n $UVC_UDC ]]; then local params="/sys/kernel/debug/usb/$UVC_UDC/params" if [[ -r $params ]]; then if [[ -z $UVC_FIFO_PERIODIC ]]; then UVC_FIFO_PERIODIC="$(uvc_fifo_min_debugfs "$params")" if [[ -n $UVC_FIFO_PERIODIC ]]; then UVC_FIFO_SRC_PERIODIC="debugfs.params" fi fi if [[ -z $UVC_FIFO_NP ]]; then UVC_FIFO_NP="$(uvc_fifo_np_debugfs "$params")" if [[ -n $UVC_FIFO_NP ]]; then UVC_FIFO_SRC_NP="debugfs.params" fi fi fi fi if [[ -n ${LESAVKA_UVC_MAXPAYLOAD_LIMIT:-} ]]; then UVC_PAYLOAD_CAP="${LESAVKA_UVC_MAXPAYLOAD_LIMIT}" UVC_PAYLOAD_SRC="env" UVC_PAYLOAD_PCT=100 return fi local chosen="" if [[ -n $UVC_BULK ]]; then if [[ -n $UVC_FIFO_NP ]]; then chosen="$UVC_FIFO_NP" UVC_PAYLOAD_SRC="${UVC_FIFO_SRC_NP:-dwc2.g_np_tx_fifo_size}" elif [[ -n $UVC_FIFO_PERIODIC ]]; then chosen="$UVC_FIFO_PERIODIC" UVC_PAYLOAD_SRC="${UVC_FIFO_SRC_PERIODIC:-dwc2.g_tx_fifo_size}" fi else if [[ -n $UVC_FIFO_PERIODIC ]]; then chosen="$UVC_FIFO_PERIODIC" UVC_PAYLOAD_SRC="${UVC_FIFO_SRC_PERIODIC:-dwc2.g_tx_fifo_size}" elif [[ -n $UVC_FIFO_NP ]]; then chosen="$UVC_FIFO_NP" UVC_PAYLOAD_SRC="${UVC_FIFO_SRC_NP:-dwc2.g_np_tx_fifo_size}" fi fi if [[ -z $chosen ]]; then return fi local pct=${LESAVKA_UVC_LIMIT_PCT:-95} if ((pct < 1)); then pct=1 elif ((pct > 100)); then pct=100 fi UVC_PAYLOAD_PCT=$pct local bytes=$((chosen * 4)) UVC_PAYLOAD_CAP=$((bytes * pct / 100)) } compute_uvc_payload_cap if [[ -n $UVC_PAYLOAD_CAP && $UVC_PAYLOAD_CAP -gt 0 ]]; then log "UVC fifo periodic=${UVC_FIFO_PERIODIC:-?} np=${UVC_FIFO_NP:-?} cap=${UVC_PAYLOAD_CAP}B pct=${UVC_PAYLOAD_PCT:-?} src=${UVC_PAYLOAD_SRC:-?} udc=${UVC_UDC:-?}" if ((UVC_MAXPACKET > UVC_PAYLOAD_CAP)); then log "clamping UVC maxpacket $UVC_MAXPACKET -> $UVC_PAYLOAD_CAP" UVC_MAXPACKET=$UVC_PAYLOAD_CAP fi fi if [[ -n $UVC_BULK && $UVC_MAXPACKET -gt 512 ]]; then log "clamping UVC maxpacket $UVC_MAXPACKET -> 512 (bulk)" UVC_MAXPACKET=512 fi if [[ -n ${LESAVKA_UVC_MJPEG:-} ]]; then UVC_CODEC=mjpeg fi MAX_SPEED=${LESAVKA_MAX_SPEED:-high-speed} if [[ -z $UVC_INTERVAL ]]; then UVC_INTERVAL=$((10000000 / UVC_FPS)) fi UVC_FRAME_SIZE=${LESAVKA_UVC_FRAME_SIZE:-$((UVC_WIDTH * UVC_HEIGHT * 2))} wait_for_enum() { local tries=${1:-50} # 50 x 100ms = 5s UDC_STATE="unknown" UDC_SPEED="unknown" for ((i=0; i/dev/null || echo "unknown") UDC_SPEED=$(cat "/sys/class/udc/$UDC/current_speed" 2>/dev/null || echo "unknown") if [[ "$UDC_STATE" != "not attached" && "$UDC_STATE" != "unknown" ]]; then return 0 fi sleep 0.1 done return 1 } exec 2> >(tee -a /tmp/lesavka-core.debug.$(date +%s).log) set -x echo "[lesavka-core] running: $0 (sha1sum=$(sha1sum "$0" | cut -d' ' -f1))" #────────────────────────────────────────────────── # 1. Ensure overlay + kernel modules #────────────────────────────────────────────────── CFG=/boot/config.txt grep -q 'dtoverlay=dwc2,dr_mode=peripheral' "$CFG" || echo 'dtoverlay=dwc2,dr_mode=peripheral' >> "$CFG" modprobe dwc2 || { echo "dwc2 not in kernel; abort" >&2; exit 1; } modprobe libcomposite || { echo "libcomposite not in kernel; abort" >&2; exit 1; } if [[ -n ${LESAVKA_RELOAD_UVCVIDEO:-} ]]; then modprobe -r uvcvideo 2>/dev/null || true fi modprobe uvcvideo || { echo "uvcvideo not in kernel; abort" >&2; exit 1; } udevadm control --reload udevadm trigger --subsystem-match=video4linux udevadm settle --timeout=5 || log "⚠️ udevadm settle timed out" #────────────────────────────────────────────────── # 2. Wait for UDC device to appear (max 10 s) #────────────────────────────────────────────────── log "⏳ waiting for UDC to register ..." UDC="" for _ in {1..100}; do # 100 × 100ms = 10s UDC=$(ls /sys/class/udc 2>/dev/null | head -n1) && [[ -n $UDC ]] && break sleep 0.1 done if [[ -z $UDC ]]; then log "⚠️ UDC still absent - trying manual bind" for drv in dwc2 dwc3; do drv_root="/sys/bus/platform/drivers/$drv" [[ -d $drv_root ]] || continue for node in /sys/bus/platform/devices/*usb*; do node=${node##*/} # strip path echo "$node" >"$drv_root/bind" 2>/dev/null || continue done done # re-check for another 5 s for i in {1..50}; do UDC=$(ls /sys/class/udc 2>/dev/null | head -n1) && [[ -n $UDC ]] && break sleep 0.1 done fi [[ -n $UDC ]] || { log "❌ UDC not present after manual bind"; exit 1; } log "✅ UDC detected: $UDC" # If a gadget is already configured, avoid tearing it down unless forced. if [[ -d $G && -z $ALLOW_RESET ]]; then if [[ -s $G/UDC || -d $G/configs/c.1 ]]; then log "🔒 gadget already configured; skipping reset." log " Set LESAVKA_ALLOW_GADGET_RESET=1 to force rebuild." attach_gadget || true exit 0 fi fi # Guard against lockups: if the gadget is already bound, don't reset unless forced. BOUND_UDC="" if [[ -r $G/UDC ]]; then BOUND_UDC=$(cat "$G/UDC" 2>/dev/null || true) fi if [[ -n $BOUND_UDC && -z $ALLOW_RESET ]]; then log "🔒 gadget already bound to '$BOUND_UDC' - refusing reset." log " Set LESAVKA_ALLOW_GADGET_RESET=1 to force." exit 0 fi # Guard against lockups: don't reset gadget while host is attached unless forced. UDC_STATE="$(udc_state "$UDC")" if [[ -z $ALLOW_RESET ]] && is_attached_state "$UDC_STATE"; then log "🔒 UDC state is '$UDC_STATE' - refusing gadget reset while host attached." log " Set LESAVKA_ALLOW_GADGET_RESET=1 to force." exit 0 fi #────────────────────────────────────────────────── # 3. (Re‑)create gadget #────────────────────────────────────────────────── mountpoint -q /sys/kernel/config || mount -t configfs none /sys/kernel/config if [[ -d $G ]]; then echo '' >"$G/UDC" 2>/dev/null || true sleep 0.2 # configfs doesn't allow unlinking attribute files; remove links then rmdir. find "$G" -type l -delete 2>/dev/null || true for dir in "$G/functions" "$G/configs" "$G/strings" "$G/os_desc" "$G/webusb"; do [[ -d $dir ]] || continue find "$dir" -mindepth 1 -depth -type d -exec rmdir {} \; 2>/dev/null || true done fi mkdir -p "$G" echo 0x1d6b >"$G/idVendor" # Linux Foundation echo 0x0104 >"$G/idProduct" # Multifunction Composite Gadget echo 0x0200 >"$G/bcdUSB" echo "$MAX_SPEED" >"$G/max_speed" mkdir -p "$G/strings/0x409" echo "$(cat /proc/sys/kernel/random/uuid)" >"$G/strings/0x409/serialnumber" echo "Lesavka" >"$G/strings/0x409/manufacturer" echo "Lesavka Composite" >"$G/strings/0x409/product" # ----------------------- HID keyboard (usb0) ----------------------- mkdir -p "$G/functions/hid.usb0" echo 1 >"$G/functions/hid.usb0/protocol" echo 1 >"$G/functions/hid.usb0/subclass" echo 8 >"$G/functions/hid.usb0/report_length" printf '\x05\x01\x09\x06\xa1\x01\x05\x07\x19\xe0\x29\xe7\x15\x00\x25\x01'\ '\x75\x01\x95\x08\x81\x02\x95\x01\x75\x08\x81\x01\x95\x05\x75\x01\x05'\ '\x08\x19\x01\x29\x05\x91\x02\x95\x01\x75\x03\x91\x01\x95\x06\x75\x08'\ '\x15\x00\x25\x65\x05\x07\x19\x00\x29\x65\x81\x00\xc0' \ >"$G/functions/hid.usb0/report_desc" # ----------------------- HID mouse (usb1) -------------------------- mkdir -p "$G/functions/hid.usb1" echo 2 > "$G/functions/hid.usb1/protocol" # Boot mouse echo 1 > "$G/functions/hid.usb1/subclass" echo 4 > "$G/functions/hid.usb1/report_length" printf '\x05\x01\x09\x02\xa1\x01\x09\x01\xa1\x00'\ '\x05\x09\x19\x01\x29\x03\x15\x00\x25\x01\x95\x03\x75\x01\x81\x02'\ '\x95\x01\x75\x05\x81\x03'\ '\x05\x01\x09\x30\x09\x31\x09\x38\x15\x81\x25\x7f\x75\x08\x95\x03\x81\x06'\ '\xc0\xc0' >"$G/functions/hid.usb1/report_desc" if [[ -z $DISABLE_UAC ]]; then # ---------- UAC2 function - speaker + mic, 2×48 kHz stereo --------- mkdir -p "$G/functions/uac2.usb0" U="$G/functions/uac2.usb0" # Playback (speaker) echo 0x3 >"$U/p_chmask" # L+R echo 48000 >"$U/p_srate" echo 2 >"$U/p_ssize" # 16 bit # Capture (microphone) echo 0x3 >"$U/c_chmask" echo 48000 >"$U/c_srate" echo 2 >"$U/c_ssize" # Optional: allocate a few extra request buffers echo 32 >"$U/req_number" 2>/dev/null || true else log "🔇 UAC2 disabled (LESAVKA_DISABLE_UAC set)" fi if [[ -z $DISABLE_UVC ]]; then # ----------------------- UVC function (usb‑video) ------------------ mkdir -p "$G/functions/uvc.usb0" F="$G/functions/uvc.usb0" echo "$UVC_STREAMING_INTERVAL" >"$F/streaming_interval" echo "$UVC_MAXPACKET" >"$F/streaming_maxpacket" echo "$UVC_MAXBURST" >"$F/streaming_maxburst" if [[ -n $UVC_BULK ]]; then echo 1 >"$F/streaming_bulk" 2>/dev/null || true fi # ── 1. FORMAT DESCRIPTOR ────────────────────────────────────────── if [[ "$UVC_CODEC" == "mjpeg" ]]; then mkdir -p "$F/streaming/mjpeg/m" echo 1 >"$F/streaming/mjpeg/m/bDefaultFrameIndex" 2>/dev/null || true echo 0 >"$F/streaming/mjpeg/m/bmaControls" 2>/dev/null || true mkdir -p "$F/streaming/mjpeg/m/720p" echo 0 >"$F/streaming/mjpeg/m/720p/bmCapabilities" echo "$UVC_WIDTH" >"$F/streaming/mjpeg/m/720p/wWidth" echo "$UVC_HEIGHT" >"$F/streaming/mjpeg/m/720p/wHeight" echo "$UVC_FRAME_SIZE" >"$F/streaming/mjpeg/m/720p/dwMaxVideoFrameBufferSize" echo "$UVC_INTERVAL" >"$F/streaming/mjpeg/m/720p/dwDefaultFrameInterval" cat <"$F/streaming/mjpeg/m/720p/dwFrameInterval" ${UVC_INTERVAL} $((UVC_INTERVAL * 2)) EOF else # uncompressed YUY2, 16 bpp mkdir -p "$F/streaming/uncompressed/yuyv" # GUID = {59555932-0000-0010-8000-00aa00389b71} (“YUY2”) little-endian printf '\x59\x55\x59\x32\x00\x00\x10\x00\x80\x00\x00\xaa\x00\x38\x9b\x71' \ >"$F/streaming/uncompressed/yuyv/guidFormat" echo 16 >"$F/streaming/uncompressed/yuyv/bBitsPerPixel" mkdir -p "$F/streaming/uncompressed/yuyv/480p" echo "$UVC_WIDTH" >"$F/streaming/uncompressed/yuyv/480p/wWidth" echo "$UVC_HEIGHT" >"$F/streaming/uncompressed/yuyv/480p/wHeight" echo "$UVC_FRAME_SIZE" >"$F/streaming/uncompressed/yuyv/480p/dwMaxVideoFrameBufferSize" echo "$UVC_INTERVAL" >"$F/streaming/uncompressed/yuyv/480p/dwDefaultFrameInterval" cat <"$F/streaming/uncompressed/yuyv/480p/dwFrameInterval" ${UVC_INTERVAL} $((UVC_INTERVAL * 2)) EOF fi # ── 3. REQUIRED HEADER LINKS (per UVC gadget docs) ──────────────── mkdir -p "$F/streaming/header/h" pushd "$F/streaming/header/h" >/dev/null if [[ "$UVC_CODEC" == "mjpeg" ]]; then ln -s ../../mjpeg/m mjpeg else ln -s ../../uncompressed/yuyv yuyv fi popd >/dev/null for s in fs hs ss; do mkdir -p "$F/streaming/class/$s" pushd "$F/streaming/class/$s" >/dev/null ln -s ../../header/h h popd >/dev/null done # ── 4. Video‑Control interface ───────────────────────────────────── mkdir -p "$F/control/header/h" set +e for s in fs hs ss; do mkdir -p "$F/control/class/$s" 2>/dev/null || continue pushd "$F/control/class/$s" >/dev/null ln -s ../../header/h h 2>/dev/null || true popd >/dev/null done set -e if [[ -n $UVC_DISABLE_IRQ ]]; then echo 0 >"$F/control/enable_interrupt_ep" 2>/dev/null || true fi # optional: hide unsupported controls echo 0 >"$F/control/terminal/camera/default/bmControls" 2>/dev/null || true echo 0 >"$F/control/processing/default/bmControls" 2>/dev/null || true # friendly label mkdir -p "$F/control/header/h/strings/0x409" 2>/dev/null || true echo "Lesavka UVC" >"$F/control/header/h/strings/0x409/label" 2>/dev/null || true else log "📷 UVC disabled (LESAVKA_DISABLE_UVC set)" fi # ----------------------- configuration ----------------------------- mkdir -p "$G/configs/c.1/strings/0x409" echo 500 > "$G/configs/c.1/MaxPower" # echo "Config 1" > "$G/configs/c.1/strings/0x409/configuration" config_label="Config 1: HID" if [[ -z $DISABLE_UAC ]]; then config_label+=" + UAC2" fi if [[ -z $DISABLE_UVC ]]; then config_label+=" + UVC" fi echo "$config_label" >"$G/configs/c.1/strings/0x409/configuration" ln -s $G/functions/hid.usb0 $G/configs/c.1/ ln -s $G/functions/hid.usb1 $G/configs/c.1/ if [[ -z $DISABLE_UAC ]]; then ln -s $U $G/configs/c.1/ fi if [[ -z $DISABLE_UVC ]]; then ln -s $G/functions/uvc.usb0 $G/configs/c.1/ fi # mkdir -p $G/functions/hid.usb0/os_desc # mkdir -p $G/functions/hid.usb1/os_desc # mkdir -p $U/os_desc # ---------- optional Microsoft OS descriptors ---------------------- # if [ -e "$G/os_desc/use" ]; then # echo 1 >"$G/os_desc/use" # echo 0xcd >"$G/os_desc/b_vendor_code" # echo "MSFT100" >"$G/os_desc/qw_sign" # ln -s "$G/configs/c.1" "$G/os_desc" # creates $G/os_desc/conf # echo "Lesavka Keyboard" >"$G/functions/hid.usb0/os_desc/interface" # echo "Lesavka Mouse" >"$G/functions/hid.usb1/os_desc/interface" # echo "Lesavka Mic+Spkr" >"$U/os_desc/interface" # fi #────────────────────────────────────────────────── # 4. Bind gadget #────────────────────────────────────────────────── echo "$UDC" >"$G/UDC" parts="hidg0,hidg1" [[ -z $DISABLE_UAC ]] && parts+=",UAC2" [[ -z $DISABLE_UVC ]] && parts+=",UVC" log "🎉 gadget bound on $UDC ($parts)" if wait_for_enum 50; then log "✅ UDC state is '$UDC_STATE' (speed=$UDC_SPEED)" else log "⚠️ UDC state is '$UDC_STATE' (speed=$UDC_SPEED). Host not enumerated." if [[ -z $DISABLE_UVC && "$UVC_FALLBACK" != "0" ]]; then log "♻️ retrying without UVC (LESAVKA_UVC_FALLBACK=0 to disable)" exec env LESAVKA_DISABLE_UVC=1 LESAVKA_UVC_FALLBACK=0 "$0" fi fi exit 0