Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support reporting usb interface #47

Merged
merged 3 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions examples/list_ports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ fn main() {
" Product: {}",
info.product.as_ref().map_or("", String::as_str)
);
println!(
" Interface: {}",
info.interface
.as_ref()
.map_or("".to_string(), |x| format!("{:02x}", *x))
);
}
SerialPortType::BluetoothPort => {
println!(" Type: Bluetooth");
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,8 @@ pub struct UsbPortInfo {
pub manufacturer: Option<String>,
/// Product name (arbitrary string)
pub product: Option<String>,
/// Interface (id number for multiplexed devices)
pub interface: Option<u8>,
}

/// The physical type of a `SerialPort`
Expand Down
30 changes: 26 additions & 4 deletions src/posix/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,16 @@ fn udev_property_as_string(d: &libudev::Device, key: &str) -> Option<String> {
/// string is comprised of hex digits and the integer value of this will be returned as a u16.
/// If the property value doesn't exist or doesn't contain valid hex digits, then an error
/// will be returned.
/// This function uses a built-in type's `from_str_radix` to implementation to perform the
/// actual conversion.
#[cfg(all(target_os = "linux", not(target_env = "musl"), feature = "libudev"))]
fn udev_hex_property_as_u16(d: &libudev::Device, key: &str) -> Result<u16> {
fn udev_hex_property_as_int<T>(
d: &libudev::Device,
key: &str,
from_str_radix: &dyn Fn(&str, u32) -> std::result::Result<T, std::num::ParseIntError>,
) -> Result<T> {
if let Some(hex_str) = d.property_value(key).and_then(OsStr::to_str) {
if let Ok(num) = u16::from_str_radix(hex_str, 16) {
if let Ok(num) = from_str_radix(hex_str, 16) {
Ok(num)
} else {
Err(Error::new(ErrorKind::Unknown, "value not hex string"))
Expand All @@ -68,13 +74,15 @@ fn port_type(d: &libudev::Device) -> Result<SerialPortType> {
Some("usb") => {
let serial_number = udev_property_as_string(d, "ID_SERIAL_SHORT");
Ok(SerialPortType::UsbPort(UsbPortInfo {
vid: udev_hex_property_as_u16(d, "ID_VENDOR_ID")?,
pid: udev_hex_property_as_u16(d, "ID_MODEL_ID")?,
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: udev_property_as_string(d, "ID_VENDOR_FROM_DATABASE")
.or_else(|| udev_property_as_string(d, "ID_VENDOR")),
product: udev_property_as_string(d, "ID_MODEL_FROM_DATABASE")
.or_else(|| udev_property_as_string(d, "ID_MODEL")),
interface: udev_hex_property_as_int(d, "ID_USB_INTERFACE_NUM", &u8::from_str_radix)
.ok(),
}))
}
Some("pci") => Ok(SerialPortType::PciPort),
Expand Down Expand Up @@ -129,6 +137,12 @@ fn get_int_property(
return None;
}
let num = match cf_number_type {
kCFNumberSInt8Type => {
let mut num: u8 = 0;
let num_ptr: *mut c_void = &mut num as *mut _ as *mut c_void;
CFNumberGetValue(container as CFNumberRef, cf_number_type, num_ptr);
Some(num as u32)
}
kCFNumberSInt16Type => {
let mut num: u16 = 0;
let num_ptr: *mut c_void = &mut num as *mut _ as *mut c_void;
Expand Down Expand Up @@ -196,6 +210,14 @@ fn port_type(service: io_object_t) -> SerialPortType {
serial_number: get_string_property(usb_device, "USB Serial Number"),
manufacturer: get_string_property(usb_device, "USB Vendor Name"),
product: get_string_property(usb_device, "USB Product Name"),
// 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:
//
// https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_driverkit_transport_usb
// https://developer.apple.com/library/archive/documentation/DeviceDrivers/Conceptual/USBBook/USBOverview/USBOverview.html#//apple_ref/doc/uid/TP40002644-BBCEACAJ
interface: get_int_property(usb_device, "bInterfaceNumber", kCFNumberSInt8Type)
.map(|x| x as u8),
})
} else if get_parent_device_by_type(service, bluetooth_device_class_name).is_some() {
SerialPortType::BluetoothPort
Expand Down
99 changes: 71 additions & 28 deletions src/windows/enumerate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,41 @@ fn get_ports_guids() -> Result<Vec<GUID>> {
Ok(guids)
}

/// 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
/// information if the hardware ID can be parsed correctly. The manufacturer
/// and product names cannot be determined from the HWID string, so those are
/// set as None.
///
/// Some HWID examples are:
/// - MicroPython pyboard: USB\VID_F055&PID_9802\385435603432
/// - BlackMagic GDB Server: USB\VID_1D50&PID_6018&MI_00\6&A694CA9&0&0000
/// - BlackMagic UART port: USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0002
/// - FTDI Serial Adapter: FTDIBUS\VID_0403+PID_6001+A702TB52A\0000
fn parse_usb_port_info(hardware_id: &str) -> Option<UsbPortInfo> {
let re = Regex::new(concat!(
r"VID_(?P<vid>[[:xdigit:]]{4})",
r"[&+]PID_(?P<pid>[[:xdigit:]]{4})",
r"(?:[&+]MI_(?P<iid>[[:xdigit:]]{2})){0,1}",
r"([\\+](?P<serial>\w+))?"
))
.unwrap();

let caps = re.captures(hardware_id)?;

Some(UsbPortInfo {
vid: u16::from_str_radix(&caps[1], 16).ok()?,
pid: u16::from_str_radix(&caps[2], 16).ok()?,
serial_number: caps.name("serial").map(|m| m.as_str().to_string()),
manufacturer: None,
product: None,
interface: caps
.name("iid")
.and_then(|m| u8::from_str_radix(m.as_str(), 16).ok()),
})
}

struct PortDevices {
/// Handle to a device information set.
hdi: HDEVINFO,
Expand Down Expand Up @@ -212,34 +247,14 @@ impl PortDevice {

// Determines the port_type for this device, and if it's a USB port populate the various fields.
pub fn port_type(&mut self) -> SerialPortType {
if let Some(hardware_id) = self.instance_id() {
// Some examples of what the hardware_id looks like:
// MicroPython pyboard: USB\VID_F055&PID_9802\385435603432
// BlackMagic GDB Server: USB\VID_1D50&PID_6018&MI_00\6&A694CA9&0&0000
// BlackMagic UART port: USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0002
// FTDI Serial Adapter: FTDIBUS\VID_0403+PID_6001+A702TB52A\0000

let re = Regex::new(concat!(
r"VID_(?P<vid>[[:xdigit:]]{4})",
r"[&+]PID_(?P<pid>[[:xdigit:]]{4})",
r"([\\+](?P<serial>\w+))?"
))
.unwrap();
if let Some(caps) = re.captures(&hardware_id) {
if let Ok(vid) = u16::from_str_radix(&caps[1], 16) {
if let Ok(pid) = u16::from_str_radix(&caps[2], 16) {
return SerialPortType::UsbPort(UsbPortInfo {
vid: vid,
pid: pid,
serial_number: caps.get(4).map(|m| m.as_str().to_string()),
manufacturer: self.property(SPDRP_MFG),
product: self.property(SPDRP_FRIENDLYNAME),
});
}
}
}
}
SerialPortType::Unknown
self.instance_id()
.and_then(|s| parse_usb_port_info(&s))
.map(|mut info| {
info.manufacturer = self.property(SPDRP_MFG);
info.product = self.property(SPDRP_FRIENDLYNAME);
SerialPortType::UsbPort(info)
})
.unwrap_or(SerialPortType::Unknown)
}

// Retrieves a device property and returns it, if it exists. Returns None if the property
Expand Down Expand Up @@ -299,3 +314,31 @@ pub fn available_ports() -> Result<Vec<SerialPortInfo>> {
}
Ok(ports)
}

#[test]
fn test_parsing_usb_port_information() {
let bm_uart_hwid = r"USB\VID_1D50&PID_6018&MI_02\6&A694CA9&0&0000";
let info = parse_usb_port_info(bm_uart_hwid).unwrap();

assert_eq!(info.vid, 0x1D50);
assert_eq!(info.pid, 0x6018);
// FIXME: The 'serial number' as reported by the HWID likely needs some review
assert_eq!(info.serial_number, Some("6".to_string()));
assert_eq!(info.interface, Some(2));

let ftdi_serial_hwid = r"FTDIBUS\VID_0403+PID_6001+A702TB52A\0000";
let info = parse_usb_port_info(ftdi_serial_hwid).unwrap();

assert_eq!(info.vid, 0x0403);
assert_eq!(info.pid, 0x6001);
assert_eq!(info.serial_number, Some("A702TB52A".to_string()));
assert_eq!(info.interface, None);

let pyboard_hwid = r"USB\VID_F055&PID_9802\385435603432";
let info = parse_usb_port_info(pyboard_hwid).unwrap();

assert_eq!(info.vid, 0xF055);
assert_eq!(info.pid, 0x9802);
assert_eq!(info.serial_number, Some("385435603432".to_string()));
assert_eq!(info.interface, None);
}