diff --git a/examples/descriptors.rs b/examples/descriptors.rs index b8f59b1..fbc2d03 100644 --- a/examples/descriptors.rs +++ b/examples/descriptors.rs @@ -10,7 +10,7 @@ fn main() { fn inspect_device(dev: DeviceInfo) { println!( "Device {:03}.{:03} ({:04x}:{:04x}) {} {}", - dev.bus_number(), + dev.bus_id(), dev.device_address(), dev.vendor_id(), dev.product_id(), diff --git a/examples/string_descriptors.rs b/examples/string_descriptors.rs index f21752f..8d81cb0 100644 --- a/examples/string_descriptors.rs +++ b/examples/string_descriptors.rs @@ -12,7 +12,7 @@ fn main() { fn inspect_device(dev: DeviceInfo) { println!( "Device {:03}.{:03} ({:04x}:{:04x}) {} {}", - dev.bus_number(), + dev.bus_id(), dev.device_address(), dev.vendor_id(), dev.product_id(), diff --git a/src/enumeration.rs b/src/enumeration.rs index 4718ded..996094f 100644 --- a/src/enumeration.rs +++ b/src/enumeration.rs @@ -25,9 +25,15 @@ pub struct DeviceInfo { #[cfg(target_os = "linux")] pub(crate) path: SysfsPath, + #[cfg(target_os = "linux")] + 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) parent_instance_id: OsString, @@ -46,8 +52,9 @@ pub struct DeviceInfo { #[cfg(target_os = "macos")] pub(crate) location_id: u32, - pub(crate) bus_number: u8, + pub(crate) bus_id: String, pub(crate) device_address: u8, + pub(crate) port_chain: Vec, pub(crate) vendor_id: u16, pub(crate) product_id: u16, @@ -79,7 +86,7 @@ impl DeviceInfo { #[cfg(target_os = "linux")] { DeviceId(crate::platform::DeviceId { - bus: self.bus_number, + bus: self.busnum, addr: self.device_address, }) } @@ -104,12 +111,26 @@ impl DeviceInfo { &self.path.0 } + /// *(Linux-only)* Bus number. + /// + /// On Linux, the `bus_id` is an integer and this provides the value as `u8`. + #[cfg(target_os = "linux")] + pub fn busnum(&self) -> u8 { + self.busnum + } + /// *(Windows-only)* Instance ID path of this device #[cfg(target_os = "windows")] pub fn instance_id(&self) -> &OsStr { &self.instance_id } + /// *(Windows-only)* Location paths property + #[cfg(target_os = "windows")] + pub fn location_paths(&self) -> &[OsString] { + &self.location_paths + } + /// *(Windows-only)* Instance ID path of the parent hub #[cfg(target_os = "windows")] pub fn parent_instance_id(&self) -> &OsStr { @@ -122,6 +143,17 @@ impl DeviceInfo { self.port_number } + /// Path of port numbers identifying the port where the device is connected. + /// + /// Together with the bus ID, it identifies a physical port. The path is + /// expected to remain stable across device insertions or reboots. + /// + /// Since USB SuperSpeed is a separate topology from USB 2.0 speeds, a + /// physical port may be identified differently depending on speed. + pub fn port_chain(&self) -> &[u8] { + &self.port_chain + } + /// *(Windows-only)* Driver associated with the device as a whole #[cfg(target_os = "windows")] pub fn driver(&self) -> Option<&str> { @@ -140,9 +172,9 @@ impl DeviceInfo { self.registry_id } - /// Number identifying the bus / host controller where the device is connected. - pub fn bus_number(&self) -> u8 { - self.bus_number + /// Identifier for the bus / host controller where the device is connected. + pub fn bus_id(&self) -> &str { + &self.bus_id } /// Number identifying the device within the bus. @@ -261,8 +293,9 @@ impl std::fmt::Debug for DeviceInfo { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut s = f.debug_struct("DeviceInfo"); - s.field("bus_number", &self.bus_number) + s.field("bus_id", &self.bus_id) .field("device_address", &self.device_address) + .field("port_chain", &format_args!("{:?}", self.port_chain)) .field("vendor_id", &format_args!("0x{:04X}", self.vendor_id)) .field("product_id", &format_args!("0x{:04X}", self.product_id)) .field( @@ -281,12 +314,14 @@ impl std::fmt::Debug for DeviceInfo { #[cfg(target_os = "linux")] { s.field("sysfs_path", &self.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); s.field("port_number", &self.port_number); s.field("driver", &self.driver); } diff --git a/src/platform/linux_usbfs/device.rs b/src/platform/linux_usbfs/device.rs index 8d1d733..dc130b9 100644 --- a/src/platform/linux_usbfs/device.rs +++ b/src/platform/linux_usbfs/device.rs @@ -46,7 +46,7 @@ pub(crate) struct LinuxDevice { impl LinuxDevice { pub(crate) fn from_device_info(d: &DeviceInfo) -> Result, Error> { - let busnum = d.bus_number(); + let busnum = d.busnum(); let devnum = d.device_address(); let active_config = d.path.read_attr("bConfigurationValue")?; diff --git a/src/platform/linux_usbfs/enumeration.rs b/src/platform/linux_usbfs/enumeration.rs index d72792b..12af27e 100644 --- a/src/platform/linux_usbfs/enumeration.rs +++ b/src/platform/linux_usbfs/enumeration.rs @@ -124,9 +124,25 @@ pub fn list_devices() -> Result, Error> { pub fn probe_device(path: SysfsPath) -> Result { debug!("Probing device {:?}", path.0); + + let busnum = path.read_attr("busnum")?; + let device_address = path.read_attr("devnum")?; + + let port_chain = path + .read_attr::("devpath") + .ok() + .and_then(|p| { + p.split('.') + .map(|v| v.parse::().ok()) + .collect::>>() + }) + .unwrap_or_default(); + Ok(DeviceInfo { - bus_number: path.read_attr("busnum")?, - device_address: path.read_attr("devnum")?, + busnum, + bus_id: format!("{busnum:03}"), + device_address, + port_chain, vendor_id: path.read_attr_hex("idVendor")?, product_id: path.read_attr_hex("idProduct")?, device_version: path.read_attr_hex("bcdDevice")?, diff --git a/src/platform/macos_iokit/enumeration.rs b/src/platform/macos_iokit/enumeration.rs index 7432b29..e6063dd 100644 --- a/src/platform/macos_iokit/enumeration.rs +++ b/src/platform/macos_iokit/enumeration.rs @@ -49,12 +49,15 @@ pub(crate) fn probe_device(device: IoService) -> Option { let registry_id = get_registry_id(&device)?; log::debug!("Probing device {registry_id:08x}"); + let location_id = get_integer_property(&device, "locationID")? as u32; + // Can run `ioreg -p IOUSB -l` to see all properties Some(DeviceInfo { registry_id, - location_id: get_integer_property(&device, "locationID")? as u32, - bus_number: 0, // TODO: does this exist on macOS? + location_id, + bus_id: format!("{:02x}", (location_id >> 24) as u8), device_address: get_integer_property(&device, "USB Address")? as u8, + port_chain: parse_location_id(location_id), vendor_id: get_integer_property(&device, "idVendor")? as u16, product_id: get_integer_property(&device, "idProduct")? as u16, device_version: get_integer_property(&device, "bcdDevice")? as u16, @@ -161,3 +164,25 @@ fn map_speed(speed: i64) -> Option { _ => None, } } + +fn parse_location_id(id: u32) -> Vec { + let mut chain = vec![]; + let mut shift = id << 8; + + while shift != 0 { + let port = shift >> 28; + chain.push(port as u8); + shift = shift << 4; + } + + chain +} + +#[test] +fn test_parse_location_id() { + assert_eq!(parse_location_id(0x01234567), vec![2, 3, 4, 5, 6, 7]); + assert_eq!(parse_location_id(0xff875000), vec![8, 7, 5]); + assert_eq!(parse_location_id(0x08400000), vec![4]); + assert_eq!(parse_location_id(0x02040100), vec![0, 4, 0, 1]); + assert_eq!(parse_location_id(0), vec![]); +} diff --git a/src/platform/windows_winusb/enumeration.rs b/src/platform/windows_winusb/enumeration.rs index 07976e8..275d3c7 100644 --- a/src/platform/windows_winusb/enumeration.rs +++ b/src/platform/windows_winusb/enumeration.rs @@ -6,8 +6,8 @@ use std::{ use log::debug; use windows_sys::Win32::Devices::{ Properties::{ - DEVPKEY_Device_Address, DEVPKEY_Device_BusNumber, DEVPKEY_Device_BusReportedDeviceDesc, - DEVPKEY_Device_CompatibleIds, DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, + DEVPKEY_Device_Address, DEVPKEY_Device_BusReportedDeviceDesc, DEVPKEY_Device_CompatibleIds, + DEVPKEY_Device_HardwareIds, DEVPKEY_Device_InstanceId, DEVPKEY_Device_LocationPaths, DEVPKEY_Device_Parent, DEVPKEY_Device_Service, }, Usb::GUID_DEVINTERFACE_USB_DEVICE, @@ -42,7 +42,6 @@ pub fn probe_device(devinst: DevInst) -> Option { debug!("Probing device {instance_id:?}"); let parent_instance_id = devinst.get_property::(DEVPKEY_Device_Parent)?; - let bus_number = devinst.get_property::(DEVPKEY_Device_BusNumber)?; let port_number = devinst.get_property::(DEVPKEY_Device_Address)?; let hub_port = HubPort::by_child_devinst(devinst).ok()?; @@ -98,13 +97,24 @@ pub fn probe_device(devinst: DevInst) -> Option { interfaces.sort_unstable_by_key(|i| i.interface_number); + let location_paths = devinst + .get_property::>(DEVPKEY_Device_LocationPaths) + .unwrap_or_default(); + + let (bus_id, port_chain) = location_paths + .iter() + .find_map(|p| parse_location_path(p)) + .unwrap_or_default(); + Some(DeviceInfo { instance_id, + location_paths, parent_instance_id, devinst, port_number, + port_chain, driver: Some(driver).filter(|s| !s.is_empty()), - bus_number: bus_number as u8, + bus_id, device_address: info.address, vendor_id: info.device_desc.idVendor, product_id: info.device_desc.idProduct, @@ -287,3 +297,47 @@ fn test_parse_compatible_id() { Some((3, 17, 34)) ); } + +fn parse_location_path(s: &OsStr) -> Option<(String, Vec)> { + let s = s.to_str()?; + + let usbroot = "#USBROOT("; + let start_i = s.find(usbroot)?; + let close_i = s[start_i + usbroot.len()..].find(')')?; + let (bus, mut s) = s.split_at(start_i + usbroot.len() + close_i + 1); + + let mut path = vec![]; + + while let Some((_, next)) = s.split_once("#USB(") { + let (port_num, next) = next.split_once(")")?; + path.push(port_num.parse().ok()?); + s = next; + } + + Some((bus.to_owned(), path)) +} + +#[test] +fn test_parse_location_path() { + assert_eq!( + parse_location_path(OsStr::new( + "PCIROOT(0)#PCI(0201)#PCI(0000)#USBROOT(0)#USB(23)#USB(2)#USB(1)#USB(3)#USB(4)" + )), + Some(( + "PCIROOT(0)#PCI(0201)#PCI(0000)#USBROOT(0)".into(), + vec![23, 2, 1, 3, 4] + )) + ); + assert_eq!( + parse_location_path(OsStr::new( + "PCIROOT(0)#PCI(0201)#PCI(0000)#USBROOT(1)#USB(16)" + )), + Some(("PCIROOT(0)#PCI(0201)#PCI(0000)#USBROOT(1)".into(), vec![16])) + ); + assert_eq!( + parse_location_path(OsStr::new( + "ACPI(_SB_)#ACPI(PCI0)#ACPI(S11_)#ACPI(S00_)#ACPI(RHUB)#ACPI(HS04)" + )), + None + ); +} diff --git a/src/platform/windows_winusb/util.rs b/src/platform/windows_winusb/util.rs index 5661d39..f4b62d4 100644 --- a/src/platform/windows_winusb/util.rs +++ b/src/platform/windows_winusb/util.rs @@ -167,7 +167,13 @@ impl<'a> Iterator for NulSepListIter<'a> { if let Some(next_nul) = self.0.iter().copied().position(|x| x == 0) { let (i, next) = self.0.split_at(next_nul + 1); self.0 = next; - Some(unsafe { WCStr::from_slice_unchecked(i) }) + + if i.len() <= 1 { + // Empty element (double `\0`) terminates the list + None + } else { + Some(unsafe { WCStr::from_slice_unchecked(i) }) + } } else { None }