jlsusb (Source)

#!/usr/bin/perl -w
##---------------------------------------------------------------------------##
#   Similar to 'lsusb -t', but uses /sys/bus/usb/devices/usb* to generate
#   the tree
#
#   Parameter:
#       -v  verbose mode
##---------------------------------------------------------------------------##
# BUUS: This script is part of Brian's Useful Utilities Set
use strict;
use Encode qw(decode encode);
use File::Basename;
use charnames ':full';

#--- Parse the command line parameter
my $verbose = $ARGV[0] && ($ARGV[0] eq '-v' || $ARGV[0] eq '--verbose') ? 1 : 0;

#--- Define the Box-drawing characters
my $charmap = `locale charmap`; chomp $charmap;
binmode STDOUT, ':utf8' if $charmap =~ /utf/i;
my $gfx_start = '';
my $box_h  = "\N{BOX DRAWINGS LIGHT HORIZONTAL}";               # ─  U+2500
my $box_v  = "\N{BOX DRAWINGS LIGHT VERTICAL}";                 #  │ U+2502
my $box_ll = "\N{BOX DRAWINGS LIGHT UP AND RIGHT}";             # └  U+2514 (vert middle, horiz left)
my $box_ml = "\N{BOX DRAWINGS LIGHT VERTICAL AND RIGHT}";       # ├  U+251C (lower left)
my $gfx_stop = '';

if ($charmap ne 'UTF-8') {
    $gfx_start = chr(27).'(0';  # Switch to VT-100 graphics characters
    $box_h = 'q';
    $box_v = 'x'; 
    $box_ml = 't';
    $box_ll = 'm';
    $gfx_stop = chr(27).'(B';   # Switch to alphabetic characters
}

#--- Load information from the USB IDS file into memory
my $usb_ids_fn;
foreach ( qw( /usr/share/misc /usr/share/hwdata ) ) {
    $usb_ids_fn = "$_/usb.ids", last if -f "$_/usb.ids";
}
die "Unable to locate usb.ids file" if ! $usb_ids_fn;

my @vendor;         # Vendor names
my %vendor_idx;     # Key=vendor name, data=index in @vendor where name was first seen
my %vendor_id_idx;  # Key=vendor ID, data=index in @vendor where name was first seen
my $vendor_count = 0;
my @desc;           # Device descriptions
my %desc_idx;       # Key=description, data=index in @desc where name was first seen
my %device;         # Key=USB vendor_id:desc_id pair, data=[vendor_idx, desc_idx]
my %list_data;      # Key='list_type code1[ code2[ code3]]'; data = description

$vendor[0] = '(Vendor not found in database)';
$desc[0] = '(No description available)';

my $vendor_device_count = 0;
my $vendor_device_high_count = 0;
my $vendor_with_most_devices = 0;
my $usb_ids_date;

print "* Reading USB ID file $usb_ids_fn\n" if $verbose;
open USB_IDS, $usb_ids_fn or die "Error opeing USB_IDS file '$usb_ids_fn' for reading: $!";
my $curr_vendor_id;  # USB vendor_id we're currently processing
my $curr_vendor_idx; # Index in @vendor for current vendor's name
my $sw = 1;          # 1 = processing vendor/device data; 0 = processing lists
my $list_type;
my ($code1, $code2);    # First and second level codes
my ($desc1, $desc2);    # First and second level descriptions
while (<USB_IDS>) {
    chomp;
    $usb_ids_date = $1, next if /^# Date:\s+(\d\d\d\d-\d\d\-\d\d)/;
    next if /^\s*$/ || /^\s*#/;
    $sw = 0 if /^[A-Z]+/;
    last if ! $sw && ! $verbose;    # No need to read list_data when not verbose

    if ($sw) {
        # Extract and store a vendor name
        if ( /^([0-9a-f]{4})\s+(.*)/ ) {
            $curr_vendor_id = $1;
            my $vendor_name = $2;
            if (! exists $vendor_idx{$vendor_name}) {
                push @vendor, $vendor_name;
                $vendor_idx{$vendor_name} = scalar(@vendor) - 1;
            }
            $curr_vendor_idx = $vendor_idx{$vendor_name};
            $vendor_id_idx{$curr_vendor_id} = $curr_vendor_idx;
            $vendor_count++;
            $vendor_device_count = 0;
            next;
        }

        # Extract a device ID and store its description
        if ( /^\t([0-9a-f]{4})\s+(.*)/ ) {
            my ($product_id, $desc) = ($1, $2);
            if (! exists $desc_idx{$desc}) {
                push @desc, $desc;
                $desc_idx{$desc} = scalar(@desc) - 1;
            }
            $device{"$curr_vendor_id:$product_id"} = [$curr_vendor_idx, $desc_idx{$desc}];

            $vendor_device_count++;
            if ($vendor_device_count > $vendor_device_high_count) {
                $vendor_device_high_count = $vendor_device_count;
                $vendor_with_most_devices = $curr_vendor_idx;
            }
        }
    } else {
        # Get list_type, code, and description
        /^([A-Z\t]+)\s*(\S+)\s+(.*)/;
        my $x;
        if (defined $1 && $1 eq "\t\t") {
            $x = "$list_type $code1 $code2 $2";
            $list_data{$x} = "$desc1/$desc2/" . (defined $3 ? $3 : 'undef-d3');
        } elsif (defined $1 && $1 eq "\t") {
            ($code2, $desc2) = ($2, $3);
            $x = "$list_type $code1 $2";
            $list_data{$x} = "$desc1/$desc2";
        } else {
            $list_type = $1;
            ($code1, $desc1) = ($2, $3);
            $x = "$list_type $code1";
            $list_data{$x} = "$desc1";
        }
    }
}
close USB_IDS;

if ($verbose) {
    print "  > File was last updated on $usb_ids_date\n" if $usb_ids_date;
    print "  > Loaded $vendor_count vendors (", scalar(@vendor), " unique) and ",
        scalar(keys %device), " devices\n";
    print "  > ", scalar(@desc), " unique device descriptions\n";
    print "  > Vendor with the most devices is $vendor[$vendor_with_most_devices] ",
        "with $vendor_device_high_count devices\n";
}

#--- If verbose, create a map of device paths to drivers
my %driver;     # Key = path to usb device; data = driver
if ($verbose) {
    my $dn = "/sys/bus/usb/drivers";
    opendir DIR, $dn or die "Error opening directory $dn for listing: $!";
    foreach my $subdir (sort readdir DIR) {
        next if $subdir eq '.' || $subdir eq '..' || $subdir eq 'usb';
        opendir SUBDIR, "$dn/$subdir"
            or die "Error opening directory $dn/$subdir for listing: $!";
        foreach my $usb_path (grep { /^\d+-\d+/ } sort readdir SUBDIR) {
            $usb_path =~ /^([^:]+)/;    # Remove the ':config-interface' part
            $driver{$1} = $subdir;
        }
    }
    closedir DIR;
    print "> Device class IDs are in '(##/##/##)' format: Class/Subclass/Protocol\n";
}

#--- Process the usb* files in /sys/bus/usb/devices
my $device_list = process_hub(0, "/sys/bus/usb/devices");
print_device_list(0, $device_list, '');
exit(0);

##---------------------------------------------------------------------------##
#   process_hub: recursively process all the devices (and hubs) found on a hub
#   Returns a arrayref; each entry is a two-item arrayref of:
#     [0] = Device description
#     [1] = If the device is a hub, an arrayref of the attached devices
##---------------------------------------------------------------------------##
sub process_hub {
    my ($level, $sys_path) = @_;

    # Devices are presented as subdirectories with names ending in -#, -#.#, -#.#.# ...
    my $x = '';
    opendir DIR, $sys_path or $x = $!;
    if ($x) {
        print "Warning: unable to open directory '$sys_path': $!\n";
        return;
    }
    # (At level 0, we use the usb0 .. usbN directories instead)
    my $regexp = $level ? '-\d+(\.\d+)*$' : 'usb\d+';
    my @dir_list = grep { /$regexp/ } sort readdir DIR;
    closedir DIR;

    # Process the directories
    my $aref = [];
    foreach my $device (@dir_list) {
        $device =~ /(\d+)$/;    # Get the port from the final digit(s) of the path
        my $root_hub_or_port = ($level ? "Port" : "Root Hub") . " $1:";
        my $dev_info = get_device_info("$sys_path/$device", $root_hub_or_port);
        my $devices = process_hub($level+1, "$sys_path/$device")
            if read_sys_file("$sys_path/$device/bDeviceClass") eq '09';
        push @$aref, [ $dev_info, $devices ];
    }
    return $aref;
}

##---------------------------------------------------------------------------##
#   Return a string with 'Product (Manufacturer), speed' information.  In
#   verbose mode, returns an arrayref with additional information such as
#   manufacturer and product strings from the device and the device number
##---------------------------------------------------------------------------##
sub get_device_info {
    my ($path, $root_hub_or_port) = @_;

    my $devnum = read_sys_file("$path/devnum");
    my $vendor_id = read_sys_file("$path/idVendor");
    my $product_id = read_sys_file("$path/idProduct");
    my $speed = read_sys_file("$path/speed");

    # Get the manufacturer and product names from the device hash (which in turn
    # is built from the usb.ids file)
    my ($vendor_idx, $desc_idx) = (0,0);

    # Check for a vendor:device ID match in %device
    if ( exists $device{"$vendor_id:$product_id"} ) {
        my $aref = $device{"$vendor_id:$product_id"};
        ($vendor_idx, $desc_idx) = @$aref;
    }

    # No match: at least try to track down the vendor
    if (! $vendor_idx) {
        $vendor_idx = $vendor_id_idx{$vendor_id}
            if exists $vendor_id_idx{$vendor_id};
    }

    # Get the manufacturer and product names as returned by the USB device
    my ($manufac_string, $product_string) = ('', '');
    $manufac_string = read_sys_file("$path/manufacturer") if -f "$path/manufacturer";
    $manufac_string =~ s/^\s*//; $manufac_string =~ s/\s*$//;   # (Trim leading/trailing blanks)
    $manufac_string = 'Linux' if $manufac_string =~ /^Linux/;
    $product_string = read_sys_file("$path/product") if -f "$path/product";
    $product_string =~ s/^\s*//; $product_string =~ s/\s*$//;

    # In verbose mode, track down device class and driver information
    # (With help from "USB in a Nutshell, Chapter 5, USB Descriptors"
    # https://beyondlogic.org/usbnutshell/usb5.shtml)
    my ($class_string, $driver_string) = ('', '');
    if ($verbose) {
        my ($class, $subclass, $protocol) = ('00', '00', '00');
        $class = read_sys_file("$path/bDeviceClass") if -f "$path/bDeviceClass";
        if ($class eq '00') {       # If no device class, try an interface
            my $c_value = 0;
            $c_value = read_sys_file("$path/bConfigurationValue") if -f "$path/bConfigurationValue";
            my $if_path = "$path/" . basename($path) . ":$c_value.0";
            $class = read_sys_file("$if_path/bInterfaceClass") if -f "$if_path/bInterfaceClass";
            $subclass = read_sys_file("$if_path/bInterfaceSubClass") if -f "$if_path/bInterfaceSubClass";
            $protocol = read_sys_file("$if_path/bInterfaceProtocol") if -f "$if_path/bInterfaceProtocol";
        } else {
            $subclass = read_sys_file("$path/bDeviceSubClass") if -f "$path/bDeviceSsubclass";
            $protocol = read_sys_file("$path/bDeviceProtocol") if -f "$path/bDeviceProtocol";
        }
        # Build a string to return to the user
        my $x = "C $class $subclass $protocol";
        if (! exists $list_data{$x}) {
            $x = "C $class $subclass";
            $x = "C $class" if ! exists $list_data{$x};
        }
        $class_string = $list_data{$x} if exists $list_data{$x};
        $class_string .= ($class_string ? ' ' : '') . "($class/$subclass/$protocol)";
        $x = basename($path);
        $driver_string = ", driver=$driver{$x}" if exists $driver{$x};
    }

    # Build a suitable return string (in verbose mode, return an arrayref)
    my $r;
    if ($verbose) {
        my $x1 = "$root_hub_or_port $vendor_id:$product_id, dev ${devnum}${driver_string}, $speed Mb/s";
        my $x2 = "(Device)  Manf=\"" . ($manufac_string ? $manufac_string : '(No manufacturer string)') . "\" ";
        $x2 .= "Product=\"" .($product_string ? $product_string : '(No product string)') . "\"";
        my $x4 = "(usb.ids) Manf=\"$vendor[$vendor_idx]\" Product=\"$desc[$desc_idx]\"";
        $r = [$x1, "Class = $class_string", $x2, $x4];
    } else {
        my $manufacturer = $vendor_idx
            ? $vendor[$vendor_idx]
            : ($manufac_string ? $manufac_string : 'Unknown manufacturer');
        $manufacturer =~ s/\.$//;
        $r = "$root_hub_or_port " . ($desc_idx ? $desc[$desc_idx]
            : $product_string ? $product_string : 'unknown device');
        $r .= " ($manufacturer), $speed Mb/s"
    }

    return $r;
}

sub read_sys_file {
    my $fn = shift;
    return "[$fn: not found]" if ! -f $fn;
    my $x = '';
    open FH, $fn or $x = $!;
    return "[$fn: cannot open: $x]" if $x;
    while ( <FH> ) {
        chomp;
        $x = $x ? "|$_" : $_;
    }
    return $x;
}

##---------------------------------------------------------------------------##
#   Print the final device list
#   $box_h = '-';   $box_v = '|'; 
#   $box_ml = '|';      # ml = Middle left-hand side
#   $box_ll = '`';      # ml = Lower left-hand box corner
##---------------------------------------------------------------------------##
sub print_device_list {
    my ($level, $device_list, $prefix) = @_;
    for (my $i = 0; $i < @$device_list; $i++) {
        my $d = $$device_list[$i];
        my $dev_info;
        my $x = '';         # Determine a suitable prefix for this line (e.g. '├─')
        if ($level) {
            # (Replace some box drawing characters in the prefix)
            $prefix =~ s/$box_ml$box_h/$box_v /go;
            $x = $prefix . ($i < (@$device_list-1) ? $box_ml : $box_ll ) . "$box_h " ;
        }

        # Display the device information
        if (ref($$d[0])) {          # In verbose mode, $desc is an arrayref and
            $dev_info = $$d[0];     # only the first line is displayed here
            print "${gfx_start}${x}${gfx_stop}$$dev_info[0]\n";
        } else {
            print "${gfx_start}${x}${gfx_stop}$$d[0]\n";
        }

        # (If this is the last device for *this* hub, remove the "├─" 
        # part of the prefix string at this level)
        $x = $prefix . ($level ? '   ' : '') if $i == @$device_list - 1;

        # Having patched up '$x', print the remaining three entries of the
        # verbose device description
        if ($dev_info) {
            my $y = $x . (defined $$d[1] && defined $$d[1][0] ? "$box_v " : '  ');
            $y =~ s/${box_ml}${box_h}/$box_v /go;
            print "${gfx_start}${y}${gfx_stop}$$dev_info[$_]\n" foreach (1 .. 3);
        }

        # If this device is a hub, process its device list
        if ($$d[1]) {
            # Now print the devices
            print_device_list($level+1, $$d[1], $x);
        }
    }
}

# vim: tabstop=4