lesavka/scripts/daemon/lesavka-core.sh

395 lines
14 KiB
Bash
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# lesavkacore.sh - oneshot USBgadget bringup (Pi5 / ArchARM)
# Presents: • Bootprotocol keyboard (hidg0)
# • Bootprotocol 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"
}
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() { echo "" >"$G/UDC" 2>/dev/null || true; }
DISABLE_UAC=${LESAVKA_DISABLE_UAC:-}
DISABLE_UVC=${LESAVKA_DISABLE_UVC:-}
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}
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<tries; i++)); do
UDC_STATE=$(cat "/sys/class/udc/$UDC/state" 2>/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; }
modprobe -r uvcvideo 2>/dev/null || true
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 10s)
#──────────────────────────────────────────────────
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 5s
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"
#──────────────────────────────────────────────────
# 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×48kHz 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" # 16bit
# 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 (usbvideo) ------------------
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 <<EOF >"$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 <<EOF >"$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. VideoControl 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