diff --git a/examples/buses.rs b/examples/buses.rs new file mode 100644 index 0000000..1c4aac7 --- /dev/null +++ b/examples/buses.rs @@ -0,0 +1,6 @@ +fn main() { + env_logger::init(); + for dev in nusb::list_buses().unwrap() { + println!("{:#?}", dev); + } +} diff --git a/src/enumeration.rs b/src/enumeration.rs index f5fecfd..e3b59ba 100644 --- a/src/enumeration.rs +++ b/src/enumeration.rs @@ -427,3 +427,268 @@ impl std::fmt::Debug for InterfaceInfo { .finish() } } + +/// USB host controller type +#[derive(Copy, Clone, Eq, PartialOrd, Ord, PartialEq, Hash, Debug)] +#[non_exhaustive] +pub enum UsbControllerType { + /// xHCI controller (USB 3.0+) + XHCI, + + /// EHCI controller (USB 2.0) + EHCI, + + /// OHCI controller (USB 1.1) + OHCI, + + /// UHCI controller (USB 1.x) (proprietary interface created by Intel) + UHCI, + + /// VHCI controller (virtual internal USB) + VHCI, +} + +impl UsbControllerType { + #[allow(dead_code)] // not used on all platforms + pub(crate) fn from_str(s: &str) -> Option { + let lower_s = s.to_owned().to_ascii_lowercase(); + match lower_s + .find("hci") + .filter(|i| *i > 0) + .and_then(|i| lower_s.bytes().nth(i - 1)) + { + Some(b'x') => Some(UsbControllerType::XHCI), + Some(b'e') => Some(UsbControllerType::EHCI), + Some(b'o') => Some(UsbControllerType::OHCI), + Some(b'v') => Some(UsbControllerType::VHCI), + Some(b'u') => Some(UsbControllerType::UHCI), + _ => None, + } + } +} + +/// Information about a system USB bus. +/// +/// Platform-specific fields: +/// * Linux: `path`, `parent_path`, `busnum`, `root_hub` +/// * Windows: `instance_id`, `parent_instance_id`, `location_paths`, `devinst`, `root_hub_description` +/// * macOS: `registry_id`, `location_id`, `name`, `provider_class_name`, `class_name` +pub struct BusInfo { + #[cfg(any(target_os = "linux", target_os = "android"))] + pub(crate) path: SysfsPath, + + #[cfg(any(target_os = "linux", target_os = "android"))] + pub(crate) parent_path: SysfsPath, + + /// The phony root hub device + #[cfg(any(target_os = "linux", target_os = "android"))] + pub(crate) root_hub: DeviceInfo, + + #[cfg(any(target_os = "linux", target_os = "android"))] + pub(crate) busnum: u8, + + #[cfg(target_os = "windows")] + pub(crate) instance_id: OsString, + + #[cfg(target_os = "windows")] + pub(crate) location_paths: Vec, + + #[cfg(target_os = "windows")] + pub(crate) devinst: crate::platform::DevInst, + + #[cfg(target_os = "windows")] + pub(crate) root_hub_description: String, + + #[cfg(target_os = "windows")] + pub(crate) parent_instance_id: OsString, + + #[cfg(target_os = "macos")] + pub(crate) registry_id: u64, + + #[cfg(target_os = "macos")] + pub(crate) location_id: u32, + + #[cfg(target_os = "macos")] + pub(crate) provider_class_name: String, + + #[cfg(target_os = "macos")] + pub(crate) class_name: String, + + #[cfg(target_os = "macos")] + pub(crate) name: Option, + + pub(crate) driver: Option, + + /// System ID for the bus + pub(crate) bus_id: String, + + /// Detected USB controller type + pub(crate) controller_type: Option, +} + +impl BusInfo { + /// *(Linux-only)* Sysfs path for the bus. + #[cfg(any(target_os = "linux", target_os = "android"))] + pub fn sysfs_path(&self) -> &std::path::Path { + &self.path.0 + } + + /// *(Linux-only)* Sysfs path for the parent controller + #[cfg(any(target_os = "linux", target_os = "android"))] + pub fn parent_sysfs_path(&self) -> &std::path::Path { + &self.parent_path.0 + } + + /// *(Linux-only)* Bus number. + /// + /// On Linux, the `bus_id` is an integer and this provides the value as `u8`. + #[cfg(any(target_os = "linux", target_os = "android"))] + pub fn busnum(&self) -> u8 { + self.busnum + } + + /// *(Linux-only)* The root hub [`DeviceInfo`] representing the bus. + #[cfg(any(target_os = "linux", target_os = "android"))] + pub fn root_hub(&self) -> &DeviceInfo { + &self.root_hub + } + + /// *(Windows-only)* Instance ID path of this device + #[cfg(target_os = "windows")] + pub fn instance_id(&self) -> &OsStr { + &self.instance_id + } + + /// *(Windows-only)* Instance ID path of the parent device + #[cfg(target_os = "windows")] + pub fn parent_instance_id(&self) -> &OsStr { + &self.parent_instance_id + } + + /// *(Windows-only)* Location paths property + #[cfg(target_os = "windows")] + pub fn location_paths(&self) -> &[OsString] { + &self.location_paths + } + + /// *(Windows-only)* Device Instance ID + #[cfg(target_os = "windows")] + pub fn devinst(&self) -> crate::platform::DevInst { + self.devinst + } + + /// *(macOS-only)* IOKit Location ID + #[cfg(target_os = "macos")] + pub fn location_id(&self) -> u32 { + self.location_id + } + + /// *(macOS-only)* IOKit [Registry Entry ID](https://developer.apple.com/documentation/iokit/1514719-ioregistryentrygetregistryentryi?language=objc) + #[cfg(target_os = "macos")] + pub fn registry_entry_id(&self) -> u64 { + self.registry_id + } + + /// *(macOS-only)* IOKit provider class name + #[cfg(target_os = "macos")] + pub fn provider_class_name(&self) -> &str { + &self.provider_class_name + } + + /// *(macOS-only)* IOKit class name + #[cfg(target_os = "macos")] + pub fn class_name(&self) -> &str { + &self.class_name + } + + /// *(macOS-only)* Name of the bus + #[cfg(target_os = "macos")] + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Driver associated with the bus + pub fn driver(&self) -> Option<&str> { + self.driver.as_deref() + } + + /// Identifier for the bus + pub fn bus_id(&self) -> &str { + &self.bus_id + } + + /// Detected USB controller type + /// + /// None means the controller type could not be determined. + /// + /// ### Platform-specific notes + /// + /// * Linux: Parsed from driver in use. + /// * macOS: The IOService entry matched. + /// * Windows: Parsed from the numbers following ROOT_HUB in the instance_id. + pub fn controller_type(&self) -> Option { + self.controller_type + } + + /// System name of the bus + /// + /// ### Platform-specific notes + /// + /// * Linux: The root hub product string. + /// * macOS: The [IONameMatched](https://developer.apple.com/documentation/bundleresources/information_property_list/ionamematch) key of the IOService entry. + /// * Windows: Description field of the root hub device. How the bus will appear in Device Manager. + pub fn system_name(&self) -> Option<&str> { + #[cfg(any(target_os = "linux", target_os = "android"))] + { + self.root_hub.product_string() + } + + #[cfg(target_os = "windows")] + { + Some(&self.root_hub_description) + } + + #[cfg(target_os = "macos")] + { + self.name.as_deref() + } + } +} + +impl std::fmt::Debug for BusInfo { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut s = f.debug_struct("BusInfo"); + + #[cfg(any(target_os = "linux", target_os = "android"))] + { + s.field("sysfs_path", &self.path); + s.field("parent_sysfs_path", &self.parent_path); + s.field("busnum", &self.busnum); + } + + #[cfg(target_os = "windows")] + { + s.field("instance_id", &self.instance_id); + s.field("parent_instance_id", &self.parent_instance_id); + s.field("location_paths", &self.location_paths); + } + + #[cfg(target_os = "macos")] + { + s.field("location_id", &format_args!("0x{:08X}", self.location_id)); + s.field( + "registry_entry_id", + &format_args!("0x{:08X}", self.registry_id), + ); + s.field("class_name", &self.class_name); + s.field("provider_class_name", &self.provider_class_name); + } + + s.field("bus_id", &self.bus_id) + .field("system_name", &self.system_name()) + .field("controller_type", &self.controller_type) + .field("driver", &self.driver); + + s.finish() + } +} diff --git a/src/lib.rs b/src/lib.rs index 8fa2b36..881c4d0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -120,7 +120,7 @@ mod platform; pub mod descriptors; mod enumeration; -pub use enumeration::{DeviceId, DeviceInfo, InterfaceInfo, Speed}; +pub use enumeration::{BusInfo, DeviceId, DeviceInfo, InterfaceInfo, Speed, UsbControllerType}; mod device; pub use device::{Device, Interface}; @@ -149,6 +149,31 @@ pub fn list_devices() -> Result, Error> { platform::list_devices() } +/// Get an iterator listing the system USB buses. +/// +/// ### Example +/// +/// Group devices by bus: +/// +/// ```no_run +/// use std::collections::HashMap; +/// +/// let devices = nusb::list_devices().unwrap().collect::>(); +/// let buses: HashMap)> = nusb::list_buses().unwrap() +/// .map(|bus| { +/// let bus_id = bus.bus_id().to_owned(); +/// (bus.bus_id().to_owned(), (bus, devices.clone().into_iter().filter(|dev| dev.bus_id() == bus_id).collect())) +/// }) +/// .collect(); +/// ``` +/// +/// ### Platform-specific notes +/// * On Linux, the abstraction of the "bus" is a phony device known as the root hub. This device is available at bus.root_hub() +/// * On Android, this will only work on rooted devices due to sysfs path usage +pub fn list_buses() -> Result, Error> { + platform::list_buses() +} + /// Get a [`Stream`][`futures_core::Stream`] that yields an /// [event][`hotplug::HotplugEvent`] when a USB device is connected or /// disconnected from the system. diff --git a/src/platform/linux_usbfs/enumeration.rs b/src/platform/linux_usbfs/enumeration.rs index 12af27e..5fbca77 100644 --- a/src/platform/linux_usbfs/enumeration.rs +++ b/src/platform/linux_usbfs/enumeration.rs @@ -8,9 +8,7 @@ use log::debug; use log::warn; use crate::enumeration::InterfaceInfo; -use crate::DeviceInfo; -use crate::Error; -use crate::Speed; +use crate::{BusInfo, DeviceInfo, Error, Speed, UsbControllerType}; #[derive(Debug, Clone)] pub struct SysfsPath(pub(crate) PathBuf); @@ -62,12 +60,34 @@ impl SysfsPath { .map_err(|e| SysfsError(attr_path, e)) } + fn readlink_attr(&self, attr: &str) -> Result { + let attr_path = self.0.join(attr); + fs::read_link(&attr_path).map_err(|e| SysfsError(attr_path, SysfsErrorKind::Io(e))) + } + pub(crate) fn read_attr(&self, attr: &str) -> Result { self.parse_attr(attr, |s| s.parse()) } fn read_attr_hex(&self, attr: &str) -> Result { - self.parse_attr(attr, |s| T::from_hex_str(s)) + self.parse_attr(attr, |s| T::from_hex_str(s.strip_prefix("0x").unwrap_or(s))) + } + + pub(crate) fn readlink_attr_filename(&self, attr: &str) -> Result { + self.readlink_attr(attr).map(|p| { + p.file_name() + .and_then(|s| s.to_str().to_owned()) + .map(str::to_owned) + .ok_or_else(|| { + SysfsError( + p, + SysfsErrorKind::Parse(format!( + "Failed to read filename for readlink attribute {}", + attr + )), + ) + }) + })? } fn children(&self) -> impl Iterator { @@ -97,10 +117,10 @@ impl FromHexStr for u16 { } } -const SYSFS_PREFIX: &'static str = "/sys/bus/usb/devices/"; +const SYSFS_USB_PREFIX: &'static str = "/sys/bus/usb/devices/"; pub fn list_devices() -> Result, Error> { - Ok(fs::read_dir(SYSFS_PREFIX)?.flat_map(|entry| { + Ok(fs::read_dir(SYSFS_USB_PREFIX)?.flat_map(|entry| { let path = entry.ok()?.path(); let name = path.file_name()?; @@ -122,6 +142,47 @@ pub fn list_devices() -> Result, Error> { })) } +pub fn list_root_hubs() -> Result, Error> { + Ok(fs::read_dir(SYSFS_USB_PREFIX)?.filter_map(|entry| { + let path = entry.ok()?.path(); + let name = path.file_name()?; + + // root hubs are named `usbX` where X is the bus number + if !name.to_string_lossy().starts_with("usb") { + return None; + } + + probe_device(SysfsPath(path)) + .inspect_err(|e| warn!("{e}; ignoring root hub")) + .ok() + })) +} + +pub fn list_buses() -> Result, Error> { + Ok(list_root_hubs()?.filter_map(|rh| { + // get the parent by following the absolute symlink; root hub in /bus/usb is a symlink to a dir in parent bus + let parent_path = rh + .path + .0 + .canonicalize() + .ok() + .and_then(|p| p.parent().map(|p| SysfsPath(p.to_owned())))?; + + debug!("Probing parent device {:?}", parent_path.0); + let driver = parent_path.readlink_attr_filename("driver").ok(); + + Some(BusInfo { + bus_id: rh.bus_id.to_owned(), + path: rh.path.to_owned(), + parent_path: parent_path.to_owned(), + busnum: rh.busnum, + controller_type: driver.as_ref().and_then(|p| UsbControllerType::from_str(p)), + driver, + root_hub: rh, + }) + })) +} + pub fn probe_device(path: SysfsPath) -> Result { debug!("Probing device {:?}", path.0); @@ -131,6 +192,7 @@ pub fn probe_device(path: SysfsPath) -> Result { let port_chain = path .read_attr::("devpath") .ok() + .filter(|p| p != "0") // root hub should be empty but devpath is 0 .and_then(|p| { p.split('.') .map(|v| v.parse::().ok()) diff --git a/src/platform/linux_usbfs/mod.rs b/src/platform/linux_usbfs/mod.rs index 6159028..80566e6 100644 --- a/src/platform/linux_usbfs/mod.rs +++ b/src/platform/linux_usbfs/mod.rs @@ -5,7 +5,7 @@ mod usbfs; mod enumeration; mod events; -pub use enumeration::{list_devices, SysfsPath}; +pub use enumeration::{list_buses, list_devices, SysfsPath}; mod device; pub(crate) use device::LinuxDevice as Device; diff --git a/src/platform/macos_iokit/enumeration.rs b/src/platform/macos_iokit/enumeration.rs index e6063dd..1f9f850 100644 --- a/src/platform/macos_iokit/enumeration.rs +++ b/src/platform/macos_iokit/enumeration.rs @@ -2,6 +2,7 @@ use std::io::ErrorKind; use core_foundation::{ base::{CFType, TCFType}, + data::CFData, number::CFNumber, string::CFString, ConcreteCFType, @@ -14,9 +15,25 @@ use io_kit_sys::{ }; use log::debug; -use crate::{DeviceInfo, Error, InterfaceInfo, Speed}; +use crate::{BusInfo, DeviceInfo, Error, InterfaceInfo, Speed, UsbControllerType}; use super::iokit::{IoService, IoServiceIterator}; +/// IOKit class name for PCI USB XHCI high-speed controllers (USB 3.0+) +#[allow(non_upper_case_globals)] +const kAppleUSBXHCI: *const ::std::os::raw::c_char = + b"AppleUSBXHCI\x00" as *const [u8; 13usize] as *const ::std::os::raw::c_char; +/// IOKit class name for PCI USB EHCI high-speed controllers (USB 2.0) +#[allow(non_upper_case_globals)] +const kAppleUSBEHCI: *const ::std::os::raw::c_char = + b"AppleUSBEHCI\x00" as *const [u8; 13usize] as *const ::std::os::raw::c_char; +/// IOKit class name for PCI USB OHCI full-speed controllers (USB 1.1) +#[allow(non_upper_case_globals)] +const kAppleUSBOHCI: *const ::std::os::raw::c_char = + b"AppleUSBOHCI\x00" as *const [u8; 13usize] as *const ::std::os::raw::c_char; +/// IOKit class name for virtual internal controller (T2 chip) +#[allow(non_upper_case_globals)] +const kAppleUSBVHCI: *const ::std::os::raw::c_char = + b"AppleUSBVHCI\x00" as *const [u8; 13usize] as *const ::std::os::raw::c_char; fn usb_service_iter() -> Result { unsafe { @@ -35,10 +52,51 @@ fn usb_service_iter() -> Result { } } +fn usb_controller_service_iter( + controller_type: &UsbControllerType, +) -> Result { + unsafe { + let dictionary = match controller_type { + UsbControllerType::XHCI => IOServiceMatching(kAppleUSBXHCI), + UsbControllerType::EHCI => IOServiceMatching(kAppleUSBEHCI), + UsbControllerType::OHCI | UsbControllerType::UHCI => IOServiceMatching(kAppleUSBOHCI), + UsbControllerType::VHCI => IOServiceMatching(kAppleUSBVHCI), + }; + if dictionary.is_null() { + return Err(Error::new(ErrorKind::Other, "IOServiceMatching failed")); + } + + let mut iterator = 0; + let r = IOServiceGetMatchingServices(kIOMasterPortDefault, dictionary, &mut iterator); + if r != kIOReturnSuccess { + return Err(Error::from_raw_os_error(r)); + } + + Ok(IoServiceIterator::new(iterator)) + } +} + pub fn list_devices() -> Result, Error> { Ok(usb_service_iter()?.filter_map(probe_device)) } +pub fn list_buses() -> Result, Error> { + // Chain all the HCI types into one iterator + // A bit of a hack, could maybe probe IOPCIDevice and filter on children with IOClass.starts_with("AppleUSB") + Ok([ + UsbControllerType::XHCI, + UsbControllerType::EHCI, + UsbControllerType::OHCI, + UsbControllerType::VHCI, + ] + .iter() + .flat_map(|hci_type| { + usb_controller_service_iter(hci_type) + .map(|iter| iter.flat_map(|dev| probe_bus(dev, hci_type))) + }) + .flatten()) +} + pub(crate) fn service_by_registry_id(registry_id: u64) -> Result { usb_service_iter()? .find(|dev| get_registry_id(dev) == Some(registry_id)) @@ -85,6 +143,27 @@ pub(crate) fn probe_device(device: IoService) -> Option { }) } +pub(crate) fn probe_bus(device: IoService, host_controller: &UsbControllerType) -> Option { + let registry_id = get_registry_id(&device)?; + log::debug!("Probing bus {registry_id:08x}"); + + let location_id = get_integer_property(&device, "locationID")? as u32; + // name is a CFData of ASCII characters + let name = get_ascii_array_property(&device, "name"); + + // Can run `ioreg -rc AppleUSBXHCI -d 1` to see all properties + Some(BusInfo { + registry_id, + location_id, + bus_id: format!("{:02x}", (location_id >> 24) as u8), + driver: get_string_property(&device, "CFBundleIdentifier"), + provider_class_name: get_string_property(&device, "IOProviderClass")?, + class_name: get_string_property(&device, "IOClass")?, + name, + controller_type: Some(host_controller.to_owned()), + }) +} + pub(crate) fn get_registry_id(device: &IoService) -> Option { unsafe { let mut out = 0; @@ -139,6 +218,17 @@ fn get_integer_property(device: &IoService, property: &'static str) -> Option Option { + let d = get_property::(device, property)?; + Some( + d.bytes() + .iter() + .map(|b| *b as char) + .filter(|c| *c != '\0') + .collect(), + ) +} + fn get_children(device: &IoService) -> Result { unsafe { let mut iterator = 0; diff --git a/src/platform/macos_iokit/mod.rs b/src/platform/macos_iokit/mod.rs index 2609596..922f121 100644 --- a/src/platform/macos_iokit/mod.rs +++ b/src/platform/macos_iokit/mod.rs @@ -6,7 +6,7 @@ pub(crate) use transfer::TransferData; mod enumeration; mod events; -pub use enumeration::list_devices; +pub use enumeration::{list_buses, list_devices}; mod device; pub(crate) use device::MacDevice as Device; diff --git a/src/platform/windows_winusb/enumeration.rs b/src/platform/windows_winusb/enumeration.rs index 275d3c7..0dfe6b8 100644 --- a/src/platform/windows_winusb/enumeration.rs +++ b/src/platform/windows_winusb/enumeration.rs @@ -7,10 +7,10 @@ use log::debug; use windows_sys::Win32::Devices::{ Properties::{ DEVPKEY_Device_Address, DEVPKEY_Device_BusReportedDeviceDesc, DEVPKEY_Device_CompatibleIds, - DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, DEVPKEY_Device_LocationPaths, - DEVPKEY_Device_Parent, DEVPKEY_Device_Service, + DEVPKEY_Device_DeviceDesc, DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, + DEVPKEY_Device_LocationPaths, DEVPKEY_Device_Parent, DEVPKEY_Device_Service, }, - Usb::GUID_DEVINTERFACE_USB_DEVICE, + Usb::{GUID_DEVINTERFACE_USB_DEVICE, GUID_DEVINTERFACE_USB_HUB}, }; use crate::{ @@ -18,7 +18,7 @@ use crate::{ decode_string_descriptor, language_id::US_ENGLISH, validate_config_descriptor, Configuration, DESCRIPTOR_TYPE_CONFIGURATION, DESCRIPTOR_TYPE_STRING, }, - DeviceInfo, Error, InterfaceInfo, + BusInfo, DeviceInfo, Error, InterfaceInfo, UsbControllerType, }; use super::{ @@ -37,6 +37,16 @@ pub fn list_devices() -> Result, Error> { Ok(devs.into_iter()) } +pub fn list_buses() -> Result, Error> { + let devs: Vec = cfgmgr32::list_interfaces(GUID_DEVINTERFACE_USB_HUB, None) + .iter() + .flat_map(|i| get_device_interface_property::(i, DEVPKEY_Device_InstanceId)) + .flat_map(|d| DevInst::from_instance_id(&d)) + .flat_map(probe_bus) + .collect(); + Ok(devs.into_iter()) +} + pub fn probe_device(devinst: DevInst) -> Option { let instance_id = devinst.get_property::(DEVPKEY_Device_InstanceId)?; debug!("Probing device {instance_id:?}"); @@ -50,6 +60,7 @@ pub fn probe_device(devinst: DevInst) -> Option { let product_string = devinst .get_property::(DEVPKEY_Device_BusReportedDeviceDesc) .and_then(|s| s.into_string().ok()); + // DEVPKEY_Device_Manufacturer exists but is often wrong and appears not to be read from the string descriptor but the .inf file let serial_number = if info.device_desc.iSerialNumber != 0 { // Experimentally confirmed, the string descriptor is cached and this does @@ -131,6 +142,50 @@ pub fn probe_device(devinst: DevInst) -> Option { }) } +pub fn probe_bus(devinst: DevInst) -> Option { + let instance_id = devinst.get_property::(DEVPKEY_Device_InstanceId)?; + // Skip non-root hubs; buses which have instance IDs starting with "USB\\ROOT_HUB" + if !instance_id.to_string_lossy().starts_with("USB\\ROOT_HUB") { + return None; + } + + debug!("Probing bus {instance_id:?}"); + + let parent_instance_id = devinst.get_property::(DEVPKEY_Device_Parent)?; + let parent_devinst = DevInst::from_instance_id(&parent_instance_id)?; + // parent service contains controller type in service field + let controller_type = parent_devinst + .get_property::(DEVPKEY_Device_Service) + .and_then(|s| UsbControllerType::from_str(&s.to_string_lossy())); + + let root_hub_description = devinst + .get_property::(DEVPKEY_Device_DeviceDesc)? + .to_string_lossy() + .to_string(); + + let driver = get_driver_name(devinst); + + let location_paths = devinst + .get_property::>(DEVPKEY_Device_LocationPaths) + .unwrap_or_default(); + + let (bus_id, _) = location_paths + .iter() + .find_map(|p| parse_location_path(p)) + .unwrap_or_default(); + + Some(BusInfo { + instance_id, + parent_instance_id: parent_instance_id.into(), + location_paths, + devinst, + driver: Some(driver).filter(|s| !s.is_empty()), + bus_id, + controller_type, + root_hub_description, + }) +} + fn list_interfaces_from_desc(hub_port: &HubPort, active_config: u8) -> Option> { let buf = hub_port .get_descriptor( diff --git a/src/platform/windows_winusb/mod.rs b/src/platform/windows_winusb/mod.rs index ef4c815..d3ee002 100644 --- a/src/platform/windows_winusb/mod.rs +++ b/src/platform/windows_winusb/mod.rs @@ -1,5 +1,5 @@ mod enumeration; -pub use enumeration::list_devices; +pub use enumeration::{list_buses, list_devices}; mod events;