#!/usr/bin/python3
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only
#
# Displays your USB devices in reasonable form.
#
# Copyright (c) 2009 Kurt Garloff <garloff@suse.de>
# Copyright (c) 2013,2018 Kurt Garloff <kurt@garloff.de>
#
# Usage: See usage()

# Py2 compat
from __future__ import print_function
import getopt
import os
import re
import sys

HUB_ICLASS = 0x09

# Global options
showint = False
showhubint = False
noemptyhub = False
nohub = False
showeps = False
showwakeup = False

prefix = "/sys/bus/usb/devices/"
usbids = [
	"/usr/share/hwdata/usb.ids",
	"/usr/share/usb.ids",
]
cols = ("", "", "", "", "", "")

norm	= "\033[0;0m"
bold	= "\033[0;1m"
red	= "\033[0;31m"
green	= "\033[0;32m"
amber	= "\033[0;33m"
blue	= "\033[0;34m"

usbvendors = {}
usbproducts = {}
usbclasses = {}

def colorize(num, text):
	return cols[num] + str(text) + cols[0]

def ishexdigit(str):
	"return True if all digits are valid hex digits"
	for dg in str:
		if not dg.isdigit() and not dg in 'abcdef':
			return False
	return True

def open_read_ign(fn):
	try:
		return open(fn, 'r', errors='ignore')
	except:
		return open(fn, 'r')

def myenum(*args):
	enums = dict(zip(args, range(len(args))))
	return type('MyEnum', (), enums)

def parse_usb_ids():
	"Parse /usr/share/{hwdata/,}usb.ids and fill usbvendors, usbproducts, usbclasses"
	vid = 0
	did = 0
	modes = myenum('Vendor', 'Class', 'Misc')
	mode = modes.Vendor
	strg = ""
	cstrg = ""
	for unm in usbids:
		if os.path.exists(unm):
			break
	for ln in open_read_ign(unm).readlines():
		if ln[0] == '#':
			continue
		ln = ln.rstrip('\n')
		if len(ln) == 0:
			continue
		if ishexdigit(ln[0:4]):
			mode = modes.Vendor
			vid = int(ln[:4], 16)
			usbvendors[vid] = ln[6:]
			continue
		if ln[0] == '\t' and ishexdigit(ln[1:3]):
			# usb.ids has a device id of 01xy, sigh
			if ln[3:5] == "xy":
				did = int(ln[1:3], 16)*256
			else:
				did = int(ln[1:5], 16)
			# USB devices
			if mode == modes.Vendor:
				usbproducts[vid, did] = ln[7:]
				continue
			elif mode == modes.Class:
				nm = ln[5:]
				if nm != "Unused":
					strg = cstrg + ":" + nm
				else:
					strg = cstrg + ":"
				usbclasses[vid, did, -1] = strg
				continue
		if ln[0] == 'C':
			mode = modes.Class
			cid = int(ln[2:4], 16)
			cstrg = ln[6:]
			usbclasses[cid, -1, -1] = cstrg
			continue
		if mode == modes.Class and ln[0] == '\t' and ln[1] == '\t' and ishexdigit(ln[2:4]):
			prid = int(ln[2:4], 16)
			usbclasses[cid, did, prid] = ln[6:]
			continue
		mode = modes.Misc
	usbclasses[0xFF, 0xFF, 0xFF] = "Vendor Specific"

def find_usb_prod(vid, pid):
	"Return device name from USB Vendor:Product list"
	strg = ""
	vendor = usbvendors.get(vid)
	if vendor:
		strg = str(vendor)
	else:
		return ""
	product = usbproducts.get((vid, pid))
	if product:
		return strg + " " + str(product)
	return strg

def find_usb_class(cid, sid, pid):
	"Return USB protocol from usbclasses list"
	lnlst = len(usbclasses)
	cls = usbclasses.get((cid, sid, pid)) \
		or usbclasses.get((cid, sid, -1)) \
		or usbclasses.get((cid, -1, -1))
	if cls:
		return str(cls)
	return ""


devlst = [
	'host',			# usb-storage
	'video4linux/video',	# uvcvideo et al.
	'sound/card',		# snd-usb-audio
	'net/',			# cdc_ether, ...
	'input/input',		# usbhid
	'usb:hiddev',		# usb hid
	'bluetooth/hci',	# btusb
	'ttyUSB',		# btusb
	'tty/',			# cdc_acm
	'usb:lp',		# usblp
	#'usb/lp',		# usblp
	'usb/',			# hiddev, usblp
	#'usbhid',		# hidraw
]

def find_storage(hostno):
	"Return SCSI block dev names for host"
	res = ""
	for ent in os.listdir("/sys/class/scsi_device/"):
		(host, bus, tgt, lun) = ent.split(":")
		if host == hostno:
			try:
				for ent2 in os.listdir("/sys/class/scsi_device/%s/device/block" % ent):
					res += ent2 + " "
			except:
				pass
	return res

def add_drv(path, drvnm):
	res = ""
	try:
		for e2 in os.listdir(path+"/"+drvnm):
			if e2[0:len(drvnm)] == drvnm:
				res += e2 + " "
		try:
			if res:
				res += "(" + os.path.basename(os.readlink(path+"/driver")) + ") "
		except:
			pass
	except:
		pass
	return res

def find_dev(driver, usbname):
	"Return pseudo devname that's driven by driver"
	res = ""
	for nm in devlst:
		dirnm = prefix + usbname
		prep = ""
		idx = nm.find('/')
		if idx != -1:
			prep = nm[:idx+1]
			dirnm += "/" + nm[:idx]
			nm = nm[idx+1:]
		ln = len(nm)
		try:
			for ent in os.listdir(dirnm):
				if ent[:ln] == nm:
					res += prep+ent+" "
					if nm == "host":
						res += "(" + find_storage(ent[ln:])[:-1] + ")"
		except:
			pass
	if driver == "usbhid":
		rg = re.compile(r'[0-9A-F]{4}:[0-9A-F]{4}:[0-9A-F]{4}\.[0-9A-F]{4}')
		for ent in os.listdir(prefix + usbname):
			m = rg.match(ent)
			if m:
				res += add_drv(prefix+usbname+"/"+ent, "hidraw")
				add = add_drv(prefix+usbname+"/"+ent, "input")
				if add:
					res += add
				else:
					for ent2 in os.listdir(prefix+usbname+"/"+ent):
						m = rg.match(ent2)
						if m:
							res += add_drv(prefix+usbname+"/"+ent+"/"+ent2, "input")
	return res


class UsbObject:
	def read_attr(self, name):
		path = prefix + self.path + "/" + name
		return open(path).readline().strip()

	def read_link(self, name):
		path = prefix + self.path + "/" + name
		return os.path.basename(os.readlink(path))

class UsbEndpoint(UsbObject):
	"Container for USB endpoint info"
	def __init__(self, parent, fname, level):
		self.parent = parent
		self.level = level
		self.fname = fname
		self.path = ""
		self.epaddr = 0
		self.len = 0
		self.ival = ""
		self.type = ""
		self.attr = 0
		self.max = 0
		if self.fname:
			self.read(self.fname)

	def read(self, fname):
		self.fname = fname
		self.path = self.parent.path + "/" + fname
		self.epaddr = int(self.read_attr("bEndpointAddress"), 16)
		ival = int(self.read_attr("bInterval"), 16)
		if ival:
			self.ival = " (%s)" % self.read_attr("interval")
		self.len = int(self.read_attr("bLength"), 16)
		self.type = self.read_attr("type")
		self.attr = int(self.read_attr("bmAttributes"), 16)
		self.max = int(self.read_attr("wMaxPacketSize"), 16)

	def __repr__(self):
		return "<UsbEndpoint[%r]>" % self.fname

	def __str__(self):
		indent = "  " * self.level
		#name = "%s/ep_%02X" % (self.parent.fname, self.epaddr)
		name = ""
		body = "(EP) %02x: %s%s attr %02x len %02x max %03x" % \
			(self.epaddr, self.type, self.ival, self.attr, self.len, self.max)
		body = colorize(5, body)
		return "%-17s %s\n" % (indent + name, indent + body)


class UsbInterface(UsbObject):
	"Container for USB interface info"
	def __init__(self, parent, fname, level=1):
		self.parent = parent
		self.level = level
		self.fname = fname
		self.path = ""
		self.iclass = 0
		self.isclass = 0
		self.iproto = 0
		self.noep = 0
		self.driver = ""
		self.devname = ""
		self.protoname = ""
		self.eps = []
		if self.fname:
			self.read(self.fname)

	def read(self, fname):
		self.fname = fname
		self.path = self.parent.path + "/" + fname
		self.iclass = int(self.read_attr("bInterfaceClass"),16)
		self.isclass = int(self.read_attr("bInterfaceSubClass"),16)
		self.iproto = int(self.read_attr("bInterfaceProtocol"),16)
		self.noep = int(self.read_attr("bNumEndpoints"))
		try:
			self.driver = self.read_link("driver")
			self.devname = find_dev(self.driver, self.path)
		except:
			pass
		self.protoname = find_usb_class(self.iclass, self.isclass, self.iproto)
		if showeps:
			for dirent in os.listdir(prefix + self.path):
				if dirent.startswith("ep_"):
					ep = UsbEndpoint(self, dirent, self.level+1)
					self.eps.append(ep)

	def __repr__(self):
		return "<UsbInterface[%r]>" % self.fname

	def __str__(self):
		indent = "  " * self.level
		name = self.fname
		plural = (" " if self.noep == 1 else "s")
		body = "(IF) %02x:%02x:%02x %iEP%s (%s) %s %s" % \
			(self.iclass, self.isclass, self.iproto, self.noep, plural,
			 self.protoname, colorize(3, self.driver), colorize(4, self.devname))
		strg = "%-17s %s\n" % (indent + name, indent + body)
		if showeps and self.eps:
			for ep in self.eps:
				strg += str(ep)
		return strg

class UsbDevice(UsbObject):
	"Container for USB device info"
	def __init__(self, parent, fname, level=0):
		self.parent = parent
		self.level = level
		self.fname = fname
		self.path = ""
		self.iclass = 0
		self.isclass = 0
		self.iproto = 0
		self.vid = 0
		self.pid = 0
		self.name = ""
		self.usbver = ""
		self.speed = ""
		self.maxpower = ""
		self.wakeup = ""
		self.noports = 0
		self.nointerfaces = 0
		self.driver = ""
		self.devname = ""
		self.interfaces = []
		self.children = []
		if self.fname:
			self.read(self.fname)
			self.readchildren()

	def read(self, fname):
		self.fname = fname
		self.path = fname
		self.iclass = int(self.read_attr("bDeviceClass"), 16)
		self.isclass = int(self.read_attr("bDeviceSubClass"), 16)
		self.iproto = int(self.read_attr("bDeviceProtocol"), 16)
		self.vid = int(self.read_attr("idVendor"), 16)
		self.pid = int(self.read_attr("idProduct"), 16)
		try:
			self.name = self.read_attr("manufacturer") + " " \
				  + self.read_attr("product")
		except:
			pass
		if self.name:
			mch = re.match(r"Linux [^ ]* (.hci[_-]hcd) .HCI Host Controller", self.name)
			if mch:
				self.name = mch.group(1)
		if not self.name:
			self.name = find_usb_prod(self.vid, self.pid)
		# Some USB Card readers have a better name then Generic ...
		if self.name.startswith("Generic"):
			oldnm = self.name
			self.name = find_usb_prod(self.vid, self.pid)
			if not self.name:
				self.name = oldnm
		try:
			ser = self.read_attr("serial")
			# Some USB devs report "serial" as serial no. suppress
			if (ser and ser != "serial"):
				self.name += " " + ser
		except:
			pass
		self.usbver = self.read_attr("version")
		self.speed = self.read_attr("speed")
		self.maxpower = self.read_attr("bMaxPower")
		self.noports = int(self.read_attr("maxchild"))
		try:
			self.nointerfaces = int(self.read_attr("bNumInterfaces"))
		except:
			self.nointerfaces = 0
		try:
			self.driver = self.read_link("driver")
			self.devname = find_dev(self.driver, self.path)
		except:
			pass
		if showwakeup:
			try:
				self.wakeup = self.read_attr('power/wakeup')
			except:
				self.wakeup = "unsupported"

	def readchildren(self):
		if self.fname[0:3] == "usb":
			fname = self.fname[3:]
		else:
			fname = self.fname
		for dirent in os.listdir(prefix + self.fname):
			if not dirent[0:1].isdigit():
				continue
			if os.access(prefix + dirent + "/bInterfaceClass", os.R_OK):
				iface = UsbInterface(self, dirent, self.level+1)
				self.interfaces.append(iface)
			else:
				usbdev = UsbDevice(self, dirent, self.level+1)
				self.children.append(usbdev)
		usbsortkey = lambda obj: [int(x) for x in re.split(r"[-:.]", obj.fname)]
		self.interfaces.sort(key=usbsortkey)
		self.children.sort(key=usbsortkey)

	def __repr__(self):
		return "<UsbDevice[%r]>" % self.fname

	def __str__(self):
		is_hub = (self.iclass == HUB_ICLASS)
		if is_hub:
			if noemptyhub and len(self.children) == 0:
				return ""
		strg = ""
		if not (nohub and is_hub):
			indent = "  " * self.level
			name = self.fname
			plural = (" " if self.nointerfaces == 1 else "s")
			body = "%s %02x %iIF%s [USB %s, %5s Mbps, %5s%s] (%s)%s" % \
				(colorize(1, "%04x:%04x" % (self.vid, self.pid)),
				 self.iclass, self.nointerfaces, plural,
				 self.usbver.strip(), self.speed, self.maxpower,
				 ("" if self.wakeup == "" else (",  power wakeup: %s" % self.wakeup)),
				 colorize(2 if is_hub else 1, self.name),
				 colorize(2, " hub") if is_hub else "")
			strg = "%-17s %s\n" % (indent + name, indent + body)
			if not (is_hub and not showhubint):
				if showeps:
					ep = UsbEndpoint(self, "ep_00", self.level+1)
					strg += str(ep)
				if showint:	
					for iface in self.interfaces:
						strg += str(iface)
		for child in self.children:
			strg += str(child)
		return strg


def usage():
	"Displays usage information"
	print("Usage: lsusb.py [options]")
	print()
	print("Options:")
	print("  -h, --help            display this help")
	print("  -i, --interfaces      display interface information")
	print("  -I, --hub-interfaces  display interface information, even for hubs")
	print("  -u, --hide-empty-hubs suppress empty hubs")
	print("  -U, --hide-hubs       suppress all hubs")
	print("  -c, --color           use colors")
	print("  -C, --no-color        disable colors")
	print("  -e, --endpoints       display endpoint info")
	print("  -f FILE, --usbids-path FILE")
	print("                        override filename for /usr/share/{hwdata/,}usb.ids")
	print("  -w, --wakeup          display power wakeup setting")
	print()
	print("Use lsusb.py -ciu to get a nice overview of your USB devices.")

def read_usb():
	"Read toplevel USB entries and print"
	root_hubs = []
	for dirent in os.listdir(prefix):
		if not dirent[0:3] == "usb":
			continue
		usbdev = UsbDevice(None, dirent, 0)
		root_hubs.append(usbdev)
	root_hubs.sort(key=lambda x: int(x.fname[3:]))
	for usbdev in root_hubs:
		print(usbdev, end="")

def main(argv):
	"main entry point"
	global showint, showhubint, noemptyhub, nohub
	global cols, usbids, showeps, showwakeup
	usecols = None

	long_options = [
		"help",
		"interfaces",
		"hub-interfaces",
		"hide-empty-hubs",
		"hide-hubs",
		"color",
		"no-color",
		"usbids-path=",
		"endpoints",
		"wakeup",
	]

	try:
		(optlist, args) = getopt.gnu_getopt(argv[1:], "hiIuUwcCef:", long_options)
	except getopt.GetoptError as exc:
		print("Error:", exc, file=sys.stderr)
		sys.exit(2)
	for opt in optlist:
		if opt[0] in {"-h", "--help"}:
			usage()
			sys.exit(0)
		elif opt[0] in {"-i", "--interfaces"}:
			showint = True
		elif opt[0] in {"-I", "--hub-interfaces"}:
			showint = True
			showhubint = True
		elif opt[0] in {"-u", "--hide-empty-hubs"}:
			noemptyhub = True
		elif opt[0] in {"-U", "--hide-hubs"}:
			noemptyhub = True
			nohub = True
		elif opt[0] in {"-c", "--color"}:
			usecols = True
		elif opt[0] in {"-C", "--no-color"}:
			usecols = False
		elif opt[0] in {"-f", "--usbids-path"}:
			usbids = [opt[1]]
		elif opt[0] in {"-e", "--endpoints"}:
			showeps = True
		elif opt[0] in {"-w", "--wakeup"}:
			showwakeup = True
	if len(args) > 0:
		print("Error: excess args %s ..." % args[0], file=sys.stderr)
		sys.exit(2)

	if usecols is None:
		usecols = sys.stdout.isatty() and os.environ.get("TERM", "dumb") != "dumb"

	if usecols:
		cols = (norm, bold, red, green, amber, blue)

	if usbids[0]:
		try:
			parse_usb_ids()
		except:
			print(" WARNING: Failure to read usb.ids", file=sys.stderr)
			#print(sys.exc_info(), file=sys.stderr)
	read_usb()

# Entry point
if __name__ == "__main__":
	main(sys.argv)


