[usb] Add "usbscan" command for iterating over USB devices

Implement a "usbscan" command as a direct analogy of the existing
"pciscan" command, allowing scripts to iterate over all detected USB
devices.

Signed-off-by: Michael Brown <mcb30@ipxe.org>
diff --git a/src/config/config.c b/src/config/config.c
index f532c1d..0f950eb 100644
--- a/src/config/config.c
+++ b/src/config/config.c
@@ -302,6 +302,9 @@
 #ifdef IMAGE_CRYPT_CMD
 REQUIRE_OBJECT ( image_crypt_cmd );
 #endif
+#ifdef USB_CMD
+REQUIRE_OBJECT ( usb_cmd );
+#endif
 
 /*
  * Drag in miscellaneous objects
diff --git a/src/config/general.h b/src/config/general.h
index b82e560..763a34a 100644
--- a/src/config/general.h
+++ b/src/config/general.h
@@ -171,6 +171,7 @@
 //#define IMAGE_MEM_CMD		/* Read memory command */
 #define IMAGE_ARCHIVE_CMD	/* Archive image management commands */
 #define SHIM_CMD		/* EFI shim command (or dummy command) */
+//#define USB_CMD		/* USB commands */
 
 /*
  * ROM-specific options
diff --git a/src/drivers/bus/usb.c b/src/drivers/bus/usb.c
index 428ae26..e1e7d51 100644
--- a/src/drivers/bus/usb.c
+++ b/src/drivers/bus/usb.c
@@ -1323,7 +1323,8 @@
 		func->name = func->dev.name;
 		func->usb = usb;
 		func->dev.desc.bus_type = BUS_TYPE_USB;
-		func->dev.desc.location = usb->address;
+		func->dev.desc.location =
+			USB_BUSDEV ( bus->address, usb->address );
 		func->dev.desc.vendor = le16_to_cpu ( usb->device.vendor );
 		func->dev.desc.device = le16_to_cpu ( usb->device.product );
 		snprintf ( func->dev.name, sizeof ( func->dev.name ),
@@ -1725,6 +1726,25 @@
 	free ( usb );
 }
 
+/**
+ * Find USB device by address
+ *
+ * @v bus		USB bus
+ * @v address		Device address
+ * @ret usb		USB device, or NULL if not found
+ */
+struct usb_device * find_usb ( struct usb_bus *bus, unsigned int address ) {
+	struct usb_device *usb;
+
+	/* Search for a matching non-zero address */
+	list_for_each_entry ( usb, &bus->devices, list ) {
+		if ( address && ( usb->address == address ) )
+			return usb;
+	}
+
+	return NULL;
+}
+
 /******************************************************************************
  *
  * USB device hotplug event handling
@@ -2115,6 +2135,11 @@
 	/* Sanity checks */
 	assert ( bus->hub != NULL );
 
+	/* Assign the first available bus address */
+	bus->address = 0;
+	while ( find_usb_bus ( bus->address ) != NULL )
+		bus->address++;
+
 	/* Open bus */
 	if ( ( rc = bus->host->open ( bus ) ) != 0 )
 		goto err_open;
@@ -2188,6 +2213,23 @@
 }
 
 /**
+ * Find USB bus by address
+ *
+ * @v address		Bus address
+ * @ret bus		USB bus, or NULL
+ */
+struct usb_bus * find_usb_bus ( unsigned int address ) {
+	struct usb_bus *bus;
+
+	for_each_usb_bus ( bus ) {
+		if ( bus->address == address )
+			return bus;
+	}
+
+	return NULL;
+}
+
+/**
  * Find USB bus by device location
  *
  * @v bus_type		Bus type
@@ -2209,7 +2251,7 @@
 
 /******************************************************************************
  *
- * USB address assignment
+ * USB device addressing
  *
  ******************************************************************************
  */
@@ -2250,6 +2292,35 @@
 	bus->addresses &= ~( 1ULL << ( address - 1 ) );
 }
 
+/**
+ * Find next USB device
+ *
+ * @v usb		USB device to fill in
+ * @v busdev		Starting bus:dev address
+ * @ret busdev		Bus:dev address of next USB device
+ * @ret rc		Return status code
+ */
+int usb_find_next ( struct usb_device **usb, uint16_t *busdev ) {
+	struct usb_bus *bus;
+
+	do {
+		/* Find USB bus, if any */
+		bus = find_usb_bus ( USB_BUS ( *busdev ) );
+		if ( ! bus ) {
+			*busdev |= ( USB_BUS ( 1 ) - 1 );
+			continue;
+		}
+
+		/* Find USB device, if any */
+		*usb = find_usb ( bus, USB_DEV ( *busdev ) );
+		if ( *usb )
+			return 0;
+
+	} while ( ++(*busdev) );
+
+	return -ENODEV;
+}
+
 /******************************************************************************
  *
  * USB bus topology
diff --git a/src/hci/commands/usb_cmd.c b/src/hci/commands/usb_cmd.c
new file mode 100644
index 0000000..d1086fd
--- /dev/null
+++ b/src/hci/commands/usb_cmd.c
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2024 Michael Brown <mbrown@fensystems.co.uk>.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License as
+ * published by the Free Software Foundation; either version 2 of the
+ * License, or any later version.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+ * 02110-1301, USA.
+ *
+ * You can also choose to distribute this program under the terms of
+ * the Unmodified Binary Distribution Licence (as given in the file
+ * COPYING.UBDL), provided that you have satisfied its requirements.
+ */
+
+#include <stdio.h>
+#include <errno.h>
+#include <getopt.h>
+#include <ipxe/usb.h>
+#include <ipxe/command.h>
+#include <ipxe/parseopt.h>
+
+FILE_LICENCE ( GPL2_OR_LATER_OR_UBDL );
+
+/** @file
+ *
+ * USB commands
+ *
+ */
+
+/** "usbscan" options */
+struct usbscan_options {};
+
+/** "usbscan" option list */
+static struct option_descriptor usbscan_opts[] = {};
+
+/** "usbscan" command descriptor */
+static struct command_descriptor usbscan_cmd =
+	COMMAND_DESC ( struct usbscan_options, usbscan_opts, 1, 1,
+		       "<setting>" );
+
+/**
+ * "usbscan" command
+ *
+ * @v argc		Argument count
+ * @v argv		Argument list
+ * @ret rc		Return status code
+ */
+static int usbscan_exec ( int argc, char **argv ) {
+	struct usbscan_options opts;
+	struct named_setting setting;
+	struct usb_device *usb;
+	unsigned long prev;
+	uint16_t busdev;
+	int len;
+	int rc;
+
+	/* Parse options */
+	if ( ( rc = parse_options ( argc, argv, &usbscan_cmd, &opts ) ) != 0 )
+		goto err_parse_options;
+
+	/* Parse setting name */
+	if ( ( rc = parse_autovivified_setting ( argv[optind],
+						 &setting ) ) != 0 )
+		goto err_parse_setting;
+
+	/* Determine starting bus:dev.fn address */
+	if ( ( len = fetchn_setting ( setting.settings, &setting.setting,
+				      NULL, &setting.setting, &prev ) ) < 0 ) {
+		/* Setting not yet defined: start searching from 00:00 */
+		busdev = 0;
+	} else {
+		/* Setting is defined: start searching from next location */
+		busdev = ( prev + 1 );
+		if ( ! busdev ) {
+			rc = -ENOENT;
+			goto err_end;
+		}
+	}
+
+	/* Find next existent USB device */
+	if ( ( rc = usb_find_next ( &usb, &busdev ) ) != 0 )
+		goto err_find_next;
+
+	/* Apply default type if necessary.  Use ":uint16" rather than
+	 * ":hex" to allow for easy inclusion within a
+	 * "${usb/${location}....}" constructed setting.
+	 */
+	if ( ! setting.setting.type )
+		setting.setting.type = &setting_type_uint16;
+
+	/* Store setting */
+	if ( ( rc = storen_setting ( setting.settings, &setting.setting,
+				     busdev ) ) != 0 ) {
+		printf ( "Could not store \"%s\": %s\n",
+			 setting.setting.name, strerror ( rc ) );
+		goto err_store;
+	}
+
+ err_store:
+ err_end:
+ err_find_next:
+ err_parse_setting:
+ err_parse_options:
+	return rc;
+}
+
+/** USB commands */
+struct command usb_commands[] __command = {
+	{
+		.name = "usbscan",
+		.exec = usbscan_exec,
+	},
+};
diff --git a/src/include/ipxe/errfile.h b/src/include/ipxe/errfile.h
index 42bc177..01a2be6 100644
--- a/src/include/ipxe/errfile.h
+++ b/src/include/ipxe/errfile.h
@@ -423,6 +423,7 @@
 #define ERRFILE_editstring	      ( ERRFILE_OTHER | 0x00610000 )
 #define ERRFILE_widget_ui	      ( ERRFILE_OTHER | 0x00620000 )
 #define ERRFILE_form_ui		      ( ERRFILE_OTHER | 0x00630000 )
+#define ERRFILE_usb_cmd		      ( ERRFILE_OTHER | 0x00640000 )
 
 /** @} */
 
diff --git a/src/include/ipxe/usb.h b/src/include/ipxe/usb.h
index 911247e..d9891b7 100644
--- a/src/include/ipxe/usb.h
+++ b/src/include/ipxe/usb.h
@@ -54,6 +54,20 @@
 	USB_SPEED_SUPER = USB_SPEED ( 5, 3 ),
 };
 
+/** Define a USB bus:device address
+ *
+ * @v bus		Bus address
+ * @v dev		Device address
+ * @ret busdev		Bus:device address
+ */
+#define USB_BUSDEV( bus, dev ) ( ( (bus) << 8 ) | (dev) )
+
+/** Extract USB bus address */
+#define USB_BUS( busdev ) ( (busdev) >> 8 )
+
+/** Extract USB device address */
+#define USB_DEV( busdev ) ( (busdev) & 0xff )
+
 /** USB packet IDs */
 enum usb_pid {
 	/** IN PID */
@@ -956,6 +970,12 @@
 	/** Host controller operations set */
 	struct usb_host_operations *op;
 
+	/** Bus address
+	 *
+	 * This is an internal index used only to allow a USB device
+	 * to be identified via a nominal bus:device address.
+	 */
+	unsigned int address;
 	/** Largest transfer allowed on the bus */
 	size_t mtu;
 	/** Address in-use mask
@@ -1269,6 +1289,9 @@
 usb_endpoint_companion_descriptor ( struct usb_configuration_descriptor *config,
 				    struct usb_endpoint_descriptor *desc );
 
+extern struct usb_device * find_usb ( struct usb_bus *bus,
+				      unsigned int address );
+
 extern struct usb_hub * alloc_usb_hub ( struct usb_bus *bus,
 					struct usb_device *usb,
 					unsigned int ports,
@@ -1285,11 +1308,13 @@
 extern int register_usb_bus ( struct usb_bus *bus );
 extern void unregister_usb_bus ( struct usb_bus *bus );
 extern void free_usb_bus ( struct usb_bus *bus );
+extern struct usb_bus * find_usb_bus ( unsigned int address );
 extern struct usb_bus * find_usb_bus_by_location ( unsigned int bus_type,
 						   unsigned int location );
 
 extern int usb_alloc_address ( struct usb_bus *bus );
 extern void usb_free_address ( struct usb_bus *bus, unsigned int address );
+extern int usb_find_next ( struct usb_device **usb, uint16_t *busdev );
 extern unsigned int usb_route_string ( struct usb_device *usb );
 extern struct usb_port * usb_root_hub_port ( struct usb_device *usb );
 extern struct usb_port * usb_transaction_translator ( struct usb_device *usb );