#!/bin/sh
# tlp-func-usb - USB Functions
#
# Copyright (c) 2026 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later

# Needs: tlp-func-base

# ----------------------------------------------------------------------------
# Constants

readonly USBD=/sys/bus/usb/devices
readonly USB_TIMEOUT_MS=2000
readonly USB_DONE=usb_done

# ----------------------------------------------------------------------------
# Functions

# --- USB Autosuspend

usb_suspend_device () {
    # enable/disable usb autosuspend for a single device
    # except input, scanners and denylisted
    # $1: device syspath
    # $2: udev/serial/batch
    # $3: auto=enable/on=disable
    local usbdev usbdev4log
    local caller="$2"
    local control="${3:-auto}"

    if [ "$caller" = "serial" ]; then
        # cut child device path
        usbdev="${1%/*/*}"
        usbdev4log="$1"
    else
        usbdev="$1"
        usbdev4log="$1"
    fi

    # quit if device is not autosuspendable
    [ -f "$usbdev/power/autosuspend_delay_ms" ] || return 0

    local vendor usbid busdev dclass

    vendor="$(read_sysf "$usbdev/idVendor")"
    usbid="$vendor:$(read_sysf "$usbdev/idProduct")"
    busdev="Bus $(read_sysf "$usbdev/busnum") Dev $(read_sysf "$usbdev/devnum")"
    dclass="$(read_sysf "$usbdev/bDeviceClass")"

    local exc=""
    local utype=""
    local chg=0 rc1=0 rc2=0
    local drvlist=""

    # trace only: get drivers for all subdevices
    if [ "$X_USB_DRIVER_TRACE" = "1" ]; then
        local dl
        drvlist=$(for dl in "$usbdev"/*:*/driver; do readlink "$dl" | \
            sed -r 's/.+\///'; done | sort -u | tr '\n' ' ')
        drvlist=" [${drvlist% }]"
    fi

    if wordinlist "$usbid" "$USB_ALLOWLIST"; then
        # device is in allowlist -- allowlist always wins
        control="auto"
        exc="_dev_allow"

    elif wordinlist "$usbid" "$USB_DENYLIST"; then
        # device is in denylist
        control="on"
        exc="_dev_deny"

    else
        # device is neither deny nor allowlisted
        local subdev

        # usb device --> wait for subdevices to populate
        [ "$caller" = "udev" ] && sleep 0.5

        if [ "$caller" != "serial" ]; then
            # --- discover wwan/usb-serial device types, first match wins
            if [ "$dclass" = "e0" ] \
                && [ "$(read_sysf "$usbdev/bDeviceSubClass")" = "01" ] \
                && [ "$(read_sysf "$usbdev/bDeviceProtocol")" = "01" ]; then
                # bluetooth device detected
               utype="btusb"
               if [ "$USB_EXCLUDE_BTUSB" = "1" ]; then
                    control="on"
                    exc="_btusb_deny"
               fi
            fi

            # discover libsane scanners
            # notes:
            # - do it *before* detecting other subdevices, because there are multifunction devices.
            #   the “deny” rule for the scanner must take precedence over a printer subdevice.
            # - libsane_matched envvar is set by libsane's udev rules
            # shellcheck disable=SC2154
            if [ -z "$utype" ] && [ "$X_USB_IGNORE_SANE" != "1" ] \
                && { [ "$libsane_matched" = "yes" ] \
                    || [ "$caller" = "batch" ] && $UDEVADM info -q property "$usbdev" 2>/dev/null | grep -q 'libsane_matched=yes'
                }; then
                # scanner detected --> do not touch this device,
                # libsane manages autosuspend on its own
                utype="libsane"
                control="deny"
                exc="_libsane"
            fi

            # iterate subdevices for further type classification
            if [ -z "$utype" ]; then
                for subdev in "$usbdev"/*:*; do
                    if [ "$(read_sysf "$subdev/bInterfaceClass")" = "03" ]; then
                        # input device detected
                        utype="hid"
                        control="on"
                        exc="_hid_deny"
                        break

                    elif [ "$(read_sysf "$subdev/bInterfaceClass")" = "01" ]; then
                        # audio device detected
                        utype="audio"
                        if [ "$USB_EXCLUDE_AUDIO" = "1" ]; then
                            control="on"
                            exc="_audio_deny"
                        fi
                        break

                    elif [ "$dclass" = "00" ] && [ "$(read_sysf "$subdev/bInterfaceClass")" = "07" ]; then
                        utype="printer"
                        if [ "$USB_EXCLUDE_PRINTER" = "1" ]; then
                            control="on"
                            exc="_printer_deny"
                        fi
                        break
                    fi
                done # subdevices
            fi # subdevices

            if [ -z "$utype" ]; then
                # discover phone devices
                if [ "$vendor" = "05ac" ] && [ "$(read_sysf "$usbdev/product")" = "iPhone" ]; then
                    # iPhone detected
                    utype="phone"

                elif [ "$dclass" = "00" ]; then
                    # class defined at interface level --> iterate subdevices
                    for subdev in "$usbdev"/*:*; do
                        if [ -d "$subdev" ]; then
                            if [ "$(read_sysf "$subdev/interface")" = "MTP" ]; then
                                # MTP: mostly Android
                                utype="phone"
                                break

                            elif [ "$(read_sysf "$subdev/bInterfaceClass")" = "ff" ] \
                                && [ "$(read_sysf "$subdev/bInterfaceSubClass")" = "42" ] \
                                && [ "$(read_sysf "$subdev/bInterfaceProtocol")" = "01" ]; then
                                # ADB: Android
                                utype="phone"
                                break

                            elif [ "$(read_sysf "$subdev/bInterfaceClass")" = "06" ] \
                                && [ "$(read_sysf "$subdev/bInterfaceSubClass")" = "01" ] \
                                && [ "$(read_sysf "$subdev/bInterfaceProtocol")" = "01" ]; then
                                # PTP: iPhone et al.
                                # caveat: may also be a camera
                                utype="phone"
                                break
                            fi
                        fi
                    done # subdevices
                fi # dclass 00

                if [ "$utype" = "phone" ] && [ "$USB_EXCLUDE_PHONE" = "1" ]; then
                    control="on"
                    exc="_phone_deny"
                fi
            fi # phone

        fi # !serial

        # --- check wwan/usb-serial device types, first match wins
        if [ -z "$utype" ]; then
            # discover wwan devices: check if device has both CDC Control and CDC Data interfaces
            local iclass
            local cdc_ctrl=0
            local cdc_data=0
            for subdev in "$usbdev"/*:*; do
                iclass="$(read_sysf "$subdev/bInterfaceClass")"
                [ "$iclass" = "02" ] && cdc_ctrl=1
                [ "$iclass" = "0a" ] && cdc_data=1
                if [ "$cdc_ctrl" = "1" ] && [ "$cdc_data" = "1" ]; then
                    utype="wwan"
                    break
                fi
            done # subdevices

            if [ -z "$utype" ]; then
                # fallback: check for a matching subdevice driver
                local driver
                for subdev in "$usbdev"/*:*; do
                    driver="$(readlink "$subdev/driver")"
                    driver="${driver##*/}"
                    if wordinlist "$driver" "cdc_acm cdc_ether cdc_mbim cdc_ncm cdc_wdm option"; then
                        utype="wwan"
                        break
                    fi
                done # subdevices
            fi # fallback

            if [ "$utype" = "wwan" ] && [ "$USB_EXCLUDE_WWAN" = "1" ]; then
                control="on"
                exc="_wwan_deny"
            fi
        fi # wwan

        if [ -z "$utype" ]; then
            # discover serial child devices
            local ttydev
            for ttydev in "$usbdev"/*:*/tty*; do
                if [ -d "$ttydev" ]; then
                    utype="serial"
                    control="on"
                    exc="_serial_deny"
                    break
                fi
            done
        fi
    fi # else !allowlist, !denylist

    if [ "$(read_sysf "$usbdev/power/control")" != "$control" ]; then
        # set control, write actual changes only
        case $control in
            auto|on)
                write_sysf "$control" "$usbdev/power/control"; rc1=$?
                chg=1
                ;;

            deny) # do not touch denylisted device
                ;;
        esac
    fi

    local logtag="usb_suspend_device.$caller.$control$exc($utype)"
    if [ "$X_USB_AUTOSUSPEND_DELAY" = "1" ] && [ "$control" != "deny" ]; then
        # set autosuspend delay
        write_sysf "$USB_TIMEOUT_MS" "$usbdev/power/autosuspend_delay_ms"; rc2=$?
        echo_debug "usb" "${logtag}: $busdev ID $usbid ${usbdev4log}${drvlist}; control: rc=$rc1; delay: rc=$rc2" \
            "$(rc2log "$rc1")"
    elif [ $chg -eq 1 ]; then
        # default: control was changed but not autosuspend_delay, i.e. kernel default kept
        echo_debug "usb" "${logtag}: $busdev ID $usbid ${usbdev4log}${drvlist}; control: rc=$rc1" \
            "$(rc2log "$rc1")"
    else
        # nothing changed actually
        echo_debug "usb" "${logtag}.no_change: $busdev ID $usbid ${usbdev4log}${drvlist}"
    fi

    return 0
}

set_usb_suspend () {
    # enable/disable usb autosuspend for all devices
    # $1: 0=silent/1=report result
    # $2: auto=enable/on=disable

    local usbdev

    if [ "$USB_AUTOSUSPEND" = "1" ]; then
        # autosuspend is configured --> iterate devices
        for usbdev in "$USBD"/*; do
            case "$usbdev" in
                *:*) ;; # colon in device name --> do nothing

                *) usb_suspend_device "$usbdev" "batch" "$2" ;;
            esac
        done

        [ "$1" = "1" ] && echo "USB autosuspend settings applied."
        echo_debug "usb" "set_usb_suspend.done"

        # set "startup completion" flag for tlp-usb-udev
        set_run_flag $USB_DONE
    else
        [ "$1" = "1" ] && cecho "Error: USB autosuspend is disabled. Set USB_AUTOSUSPEND=1 in ${CONF_USR}." 1>&2
        echo_debug "usb" "set_usb_suspend.not_configured"
    fi

    return 0
}
