Repository: serialport/serialport-rs (language: rust)

Issue:
Enable differentiation of different devices on a single USB serial port adapter
This issue was migrated from GitLab. The original issue can be found here:
https://gitlab.com/susurrus/serialport-rs/-/issues/81
---
It would be handy if it were possible to differentiate the different ports in a composite USB Serial adapter in some cross platform way, presumably by adding a field to the UsbPortInfo struct. I've attached an `lsusb -v` output for such a device. There seem to be two possible fields that would make this possible. Either the iInterface field (on line 45 and 115) which I believe is generally a human readable string or the bMasterInterface attribute in the CDCACM union (on line 55 and 125) which is a number.

The second method seems to be used to differentiate them by linux as seen below, but iInterface is available through udev.
```
aled@dufous-/dev: ls -l /dev/serial/by-id/
total 0
lrwxrwxrwx 1 root root 13 Jul 14 19:26 usb-Black_Sphere_Technologies_Black_Magic_Probe_DDD5ACC2-if00 -> ../../ttyACM0
lrwxrwxrwx 1 root root 13 Jul 14 19:26 usb-Black_Sphere_Technologies_Black_Magic_Probe_DDD5ACC2-if02 -> ../../ttyACM1
```

It looks like the library actually [already queries](https://gitlab.com/susurrus/serialport-rs/-/blob/master/src/windows/enumerate.rs#L144) the if number on Windows, although it is not exposed to the user. Funnily enough the example in the comment is exactly my use case, funny how life does that.

[example.txt](/uploads/e558152159de13c1f2e2eb5aed8f869d/example.txt)

If anything I said in regards to anything USB doesn't make any sense, I apologize in advance, USB is still kind of a blackbox to me. I can probably work on a pull request on the posix side of things, but I don't own a Windows box so I have no real way of testing windows stuff


Discussion / hints:

Fix patch:
diff --git a/examples/list_ports.rs b/examples/list_ports.rs
index 193bb432..0fe98f2b 100644
--- a/examples/list_ports.rs
+++ b/examples/list_ports.rs
@@ -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");
diff --git a/src/lib.rs b/src/lib.rs
index 32f24af4..efb32f52 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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`
diff --git a/src/posix/enumerate.rs b/src/posix/enumerate.rs
index 039a348e..ada5978e 100644
--- a/src/posix/enumerate.rs
+++ b/src/posix/enumerate.rs
@@ -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"))
@@ -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),
@@ -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;
@@ -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
diff --git a/src/windows/enumerate.rs b/src/windows/enumerate.rs
index 1ebd6eb0..84c44169 100644
--- a/src/windows/enumerate.rs
+++ b/src/windows/enumerate.rs
@@ -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,
@@ -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
@@ -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);
+}


Test patch: