|
#!/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
|