#!/usr/bin/perl
# tlp-usblist - list usb device info with autosuspend attributes
#
# Copyright (c) 2025 Thomas Koch <linrunner at gmx.net> and others.
# SPDX-License-Identifier: GPL-2.0-or-later

package tlp_usblist;
use strict;
use warnings;

# --- Constants
use constant USBD => "/sys/bus/usb/devices";

# --- Modules
use Getopt::Long;

# --- Global vars
my %usbdevices;

my $verbose = 0;

# --- Subroutines

# Read content from a sysfile
# $_[0]: input file
# return: content / empty string if nonexistent or not readable
sub catsysf {
    my $fname = "$_[0]";
    my $sysval = "";
    if ( open (my $sysf, "<", $fname) ) {
        chomp ($sysval = <$sysf>);
        close ($sysf);
    }
    return $sysval;
}

# Read device driver from DEVICE/uevent
# $_[0]: (sub)device base path
# return: driver / empty string if uevent nonexistent or not readable
sub getdriver {
    my $dpath = "$_[0]";
    my $driver = "";
    if ( open (my $sysf, "<", $dpath . "/uevent") ) {
        # read file line by line
        while (<$sysf>) {
            # match line content and return DRIVER= value
            if ( s/^DRIVER=(.*)/$1/ ) {
                chomp ($driver = $_);
                last; # break loop
            }
        }
        close ($sysf);
    }
    return $driver
}

# Get drivers associated with USB device by iterating subdevices
# $_[0]: device base path
# return:  driver list / "no driver" if none found
sub usbdriverlist {
    my $dpath = "$_[0]";
    my $driverlist = "";
    # iterate subdevices
    foreach my $subdev (glob $dpath . "/*:*") {
        # get subdevice driver
        my $driver = getdriver ("$subdev");
        if ( $driver ) {
            if (index ($driverlist, $driver) == -1) {
                if ($driverlist) { $driverlist = $driverlist . ", " . $driver; }
                else { $driverlist = $driver; }
            } # if index
        } # if $driver
    } # foreach $subdev

    if (! $driverlist) { $driverlist = "no driver"; }
    return $driverlist
}

# --- MAIN
# parse arguments
GetOptions ('verbose' => \$verbose);

# Read USB device tree attributes as arrays into %usbdevices hash, indexed by Bus_Device
foreach my $udev (grep { ! /:/ } glob USBD . "/*") {
    my $usbv = "(autosuspend not available)";

    # get device id
    my $usbk = sprintf ("%03d_%03d", catsysf ("$udev/busnum"), catsysf ("$udev/devnum") );

    # get device mode and timeout
    if ( length (my $ptimeout = catsysf ("$udev/power/autosuspend_delay_ms"))
        && length (my $pmode = catsysf ("$udev/power/control")) ) {
        if ( $verbose ) {
            # get device status
            my $pstatus = catsysf ("$udev/power/runtime_status");
            # format: device mode, timeout, status
            $usbv = sprintf ("control = %-5s autosuspend_delay_ms = %4d, runtime_status = %-9s", $pmode . ",", $ptimeout, $pstatus);
        } else {
            # format: device mode, timeout
            $usbv = sprintf ("control = %-5s autosuspend_delay_ms = %4d", $pmode . ",", $ptimeout);
        }
    }

    # store formatted result in hash
    @{$usbdevices{$usbk}} = ($udev, $usbv);
}

# Output device list with attributes and drivers
foreach (`lsusb 2> /dev/null`) {
    my ($bus, $dev, $usbid, $desc) = /Bus (\S+) Device (\S+): ID (\S+)[ ]+(.*)/;
    if (length ($bus) and length ($dev) and length ($usbid) ) {
        my $usbk = $bus . "_" . $dev;
        $desc =~ s/\s+$//;
        $desc ||= "<unknown>";
        print "Bus $bus Device $dev ID $usbid $usbdevices{$usbk}[1] -- $desc ("
            . usbdriverlist($usbdevices{$usbk}[0]) . ")\n";
    }
}

exit 0;