Skip to content

Commit

Permalink
Add bus_id and port_chain fields to UsbPortInfo for all platforms
Browse files Browse the repository at this point in the history
Implementation taken and adapted from nusb crate
  • Loading branch information
kindermax committed Nov 30, 2024
1 parent 7e079cc commit bf10cb0
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 24 deletions.
6 changes: 4 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -787,8 +787,10 @@ pub struct UsbPortInfo {
pub manufacturer: Option<String>,
/// Product name (arbitrary string)
pub product: Option<String>,
/// Location (bus_id-a.b string)
pub location: String,
/// Device's bus id
pub bus_id: String,
/// Physycal port hierarchy
pub port_chain: Vec<u8>,
/// The interface index of the USB serial port. This can be either the interface number of
/// the communication interface (as is the case on Windows and Linux) or the data
/// interface (as is the case on macOS), so you should recognize both interface numbers.
Expand Down
59 changes: 42 additions & 17 deletions src/posix/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,25 @@ fn port_type(d: &libudev::Device) -> Result<SerialPortType> {
udev_property_encoded_or_replaced_as_string(d, "ID_MODEL_ENC", "ID_MODEL")
.or_else(|| udev_property_as_string(d, "ID_MODEL_FROM_DATABASE"));

let location = device_location(d);
let [busnum, path] = location.split("-");
let port_chain = path
.filter(|p| p != "0") // root hub should be empty but devpath is 0
.and_then(|p| {
p.split('.')
.map(|v| v.parse::<u8>().ok())
.collect::<Option<Vec<u8>>>()
})
.unwrap_or_default();

Ok(SerialPortType::UsbPort(UsbPortInfo {
vid: udev_hex_property_as_int(d, "ID_VENDOR_ID", &u16::from_str_radix)?,
pid: udev_hex_property_as_int(d, "ID_MODEL_ID", &u16::from_str_radix)?,
serial_number,
manufacturer,
product,
location: device_location(d),
bus_id: format!("{busnum:03}"),
port_chain,
#[cfg(feature = "usbportinfo-interface")]
interface: udev_hex_property_as_int(d, "ID_USB_INTERFACE_NUM", &u8::from_str_radix)
.ok(),
Expand All @@ -172,13 +184,25 @@ fn port_type(d: &libudev::Device) -> Result<SerialPortType> {
"ID_USB_MODEL",
);

let location = device_location(d);
let [busnum, path] = location.split("-");
let port_chain = path
.filter(|p| p != "0") // root hub should be empty but devpath is 0
.and_then(|p| {
p.split('.')
.map(|v| v.parse::<u8>().ok())
.collect::<Option<Vec<u8>>>()
})
.unwrap_or_default();

Ok(SerialPortType::UsbPort(UsbPortInfo {
vid: udev_hex_property_as_int(d, "ID_USB_VENDOR_ID", &u16::from_str_radix)?,
pid: udev_hex_property_as_int(d, "ID_USB_MODEL_ID", &u16::from_str_radix)?,
serial_number: udev_property_as_string(d, "ID_USB_SERIAL_SHORT"),
manufacturer,
product,
location: device_location(d),
bus_id: format!("{busnum:03}"),
port_chain,
#[cfg(feature = "usbportinfo-interface")]
interface: udev_hex_property_as_int(
d,
Expand Down Expand Up @@ -271,7 +295,8 @@ fn parse_modalias(moda: &str) -> Option<UsbPortInfo> {
serial_number: None,
manufacturer: None,
product: None,
location: "".to_string(),
bus_id: "".to_string(),
port_chain: vec![],
// Only attempt to find the interface if the feature is enabled.
#[cfg(feature = "usbportinfo-interface")]
interface: mod_tail.get(pid_start + 4..).and_then(|mod_tail| {
Expand Down Expand Up @@ -366,20 +391,19 @@ fn get_string_property(device_type: io_registry_entry_t, property: &str) -> Resu

/// Parse location_id by extracting bits that represent specific parts of
/// the USB device’s location within the USB topology, such as the bus and
/// port numbers, and then formats them in a hierarchical form like “0-1.2.4”.
fn location_to_string(location_id: u32) -> String {
let mut location_id = location_id;
let bus_id = format!("{:02x}", (location_id >> 24) as u8);
let mut path = format!("{bus_id}-");
while location_id & 0xf00000 != 0 {
let item = (location_id >> 20) & 0xf;
if !path.ends_with("-") {
path.push_str(".");
}
path.push_str(item.to_string().as_str());
location_id <<= 4;
/// port numbers,
/// Returns port chain as a vector of u8 bytes.
fn parse_location_id(id: u32) -> Vec<u8> {
let mut chain = vec![];
let mut shift = id << 8;

while shift != 0 {
let port = shift >> 28;
chain.push(port as u8);
shift = shift << 4;
}
return path;

chain
}

#[cfg(any(target_os = "ios", target_os = "macos"))]
Expand All @@ -400,7 +424,8 @@ fn port_type(service: io_object_t) -> SerialPortType {
serial_number: get_string_property(usb_device, "USB Serial Number").ok(),
manufacturer: get_string_property(usb_device, "USB Vendor Name").ok(),
product: get_string_property(usb_device, "USB Product Name").ok(),
location: location_to_string(location_id),
bus_id: format!("{:02x}", (location_id >> 24) as u8),
port_chain: parse_location_id(location_id),
// Apple developer documentation indicates `bInterfaceNumber` is the supported key for
// looking up the composite usb interface id. `idVendor` and `idProduct` are included in the same tables, so
// we will lookup the interface number using the same method. See:
Expand Down
39 changes: 34 additions & 5 deletions src/windows/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,23 @@ impl<'hwid> HwidMatches<'hwid> {
}
}

fn parse_location_path(s: &str) -> Option<(String, Vec<u8>)> {
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))
}

/// Windows usb port information can be determined by the port's HWID string.
///
/// This function parses the HWID string using regex, and returns the USB port
Expand Down Expand Up @@ -386,6 +403,16 @@ impl PortDevice {
.map(|mut info: UsbPortInfo| {
info.manufacturer = self.property(SPDRP_MFG);
info.product = self.property(SPDRP_FRIENDLYNAME);

let location_paths = self.property(SPDRP_LOCATION_PATHS);

let (bus_id, port_chain) = location_paths
.iter()
.find_map(|p| parse_location_path(p))
.unwrap_or_default();

info.bus_id = bus_id;
info.port_chain = port_chain;
SerialPortType::UsbPort(info)
})
.unwrap_or(SerialPortType::Unknown)
Expand All @@ -409,17 +436,19 @@ impl PortDevice {
)
};

if res == FALSE || value_type != REG_SZ {
if res == FALSE || (value_type != REG_SZ && value_type != REG_MULTI_SZ) {
return None;
}

let mut property_val = from_utf16_lossy_trimmed(&property_buf);
if value_type == REG_MULTI_SZ {
property_val = property_val.split('\0').next().unwrap_or("").to_string();
}

// Using the unicode version of 'SetupDiGetDeviceRegistryProperty' seems to report the
// entire mfg registry string. This typically includes some driver information that we should discard.
// Example string: 'FTDI5.inf,%ftdi%;FTDI'
from_utf16_lossy_trimmed(&property_buf)
.split(';')
.last()
.map(str::to_string)
property_val.split(';').last().map(str::to_string)
}
}

Expand Down

0 comments on commit bf10cb0

Please sign in to comment.