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

Add port_number, port_chain for all platform #58

Closed
wants to merge 10 commits into from

Conversation

CCnut
Copy link

@CCnut CCnut commented Jun 4, 2024

Add location paths for USB devices on Windows

src/enumeration.rs Outdated Show resolved Hide resolved
@kevinmehall
Copy link
Owner

kevinmehall commented Jun 11, 2024

I'm wondering what location paths are used for -- what's your use case? This isn't necessarily a problem to add, but I don't want to go so far as caching every device property so wondering where to draw the line. For properties not supported, you can always look them up via the instance ID.

@CCnut
Copy link
Author

CCnut commented Jun 12, 2024

If there are two devices with the same SN/PID_VID/NAME.
When two devices are reconnected, the instance ID cannot distinguish between devices on Windows.
USBDeview
(If the SN is split using the instance ID, the SN of the second device will get the wrong value)

@kevinmehall
Copy link
Owner

Does the existing parent_instance_id (which is the hub) and port_number work to distinguish identical devices?

What would be ideal is a cross-platform way to identify the port, like libusb's libusb_get_port_numbers().

Besides being Windows-specific and not portable, location_paths has the disadvantage of returning multiple strings that you have to deal with somehow. If you really need a particular property that is not supported, the instance ID is exposed so that you can make your own calls to SetupAPI or cfgmgr32. I don't think nusb wants to be an API for all possible Windows device info, though its cfgmgr32 module could be a starting point for writing one.

@CCnut
Copy link
Author

CCnut commented Jun 21, 2024

parent_instance_id has the same problem as instance_id.

Getting location_paths is to get the location of the USB device in the USB device tree, it returns two types of strings, one in the device tree and one in the ACPI tree.

Try enumerating the complete usb device tree to locate the position of the usb device, or use libusb_get_bus_number and libusb_get_port_numbers for similar results.

I will try to implement libusb_get_bus_number and libusb_get_port_numbers first.

@CCnut
Copy link
Author

CCnut commented Jun 27, 2024

The right is the output from libusb.
image

// like listdevs output
fn main() {
    env_logger::init();
    for dev in nusb::list_devices().unwrap() {
        let chain: Vec<String> = dev.port_chain().map(|v| v.to_string()).collect();
        println!(
            "{:04x}:{:04x} (bus {}, device {}) chain: {}",
            dev.vendor_id(),
            dev.product_id(),
            dev.bus_number(),
            dev.device_address(),
            chain.join(".")
        );
    }
}

@CCnut CCnut changed the title windows: add location_paths Add port_number, port_chain for all platform Jun 27, 2024
Copy link
Owner

@kevinmehall kevinmehall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't had a chance to test this or give it a full review, but here are some initial questions.

Comment on lines +254 to +281
// Find Port_#xxxx
// Port_#0002.Hub_#000D
if let Some(location_info) = devinst.get_property::<OsString>(DEVPKEY_Device_LocationInfo) {
let s = location_info.to_string_lossy();
if &s[0..6] == "Port_#" {
if let Ok(n) = s[6..10].parse::<u32>() {
return Some(n);
}
}
}
// Find last #USB(x)
// PCIROOT(B2)#PCI(0300)#PCI(0000)#USBROOT(0)#USB(1)#USB(2)#USBMI(3)
if let Some(location_paths) =
devinst.get_property::<Vec<OsString>>(DEVPKEY_Device_LocationPaths)
{
for location_path in location_paths {
let s = location_path.to_string_lossy();
for b in s.split('#').rev() {
if b.contains("USB(") {
if let Ok(n) = b[5..b.len()].parse::<u32>() {
return Some(n);
}
break;
}
}
}
}
devinst.get_property::<u32>(DEVPKEY_Device_Address)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason all three of these techniques are used? Would be worth a comment explaining under what circumstances we'd hit each of these.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function references the libusb code.
The DEVPKEY_Device_LocationInfo and DEVPKEY_Device_LocationPaths parameters may not return a matching format.

}
}
// Find last #USB(x)
// PCIROOT(B2)#PCI(0300)#PCI(0000)#USBROOT(0)#USB(1)#USB(2)#USBMI(3)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, do these #USB(n) elements directly correspond to the port chain that we could just parse from the location path?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be the case

.collect();
Ok(devs.into_iter())
}

pub fn probe_device(devinst: DevInst) -> Option<DeviceInfo> {
pub fn enum_root_hub(rootinst: DevInst) -> impl Iterator<Item = (DevInst, Vec<u32>)> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would there be a way to do this starting from a device and looking up its parent rather than switching to start at a root hub? Besides the increased complexity, one argument against this is that with hotplug attach events, we're given a single DevInst and would like to be able to find all of its DeviceInfo properties without walking the whole tree.


fn get_port_number(device: &IoService) -> Option<u32> {
get_integer_property::<u32>(device, "PortNum").or_else(|| {
if let Ok(parent) = get_parent(device) {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the parent something other than a hub here? How is the line below getting the port for a specific child device when the property is on the parent?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function also references the libusb code.

};

// Ignore the same sessionID
if session_id
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you observe it reporting a device as its own parent or find documentation indicating that, or is this just a precaution to prevent infinite-looping?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parent device may return the same sessionID, which needs to be ignored until a new sessionID is obtained or the obtaining fails.

Copy link
Owner

@kevinmehall kevinmehall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is far too complicated for the functionality added. Libusb does a lot of crazy stuff that is no longer relevant; please don't copy what libusb does without understanding why it does it. Since much of this code is practically unreachable, it's also untested.

For example, this adds an alternative way to look up port numbers on macOS if portNum is missing -- it turns out that libusb had this because macOS versions 10.11.0 - 10.11.2 lacked the portNum property, but macOS 10.11.2 is 9 years old. Meanwhile, the locationId is just a bitwise encoding of the port path we're looking for, no iokit tree traversal needed.

Similarly on Windows, this adds two additional methods of finding the port number. The Windows backend already relies heavily on the port number, because it's what's passed to the hub ioctls to get descriptors. We've been using DEVPKEY_Device_Address exclusively for that with no problems so far.

I've gone ahead and implemented a simpler version as #71. Need to figure out what to do with the bus number though, and may end up using some of this Windows code after all.

Comment on lines +101 to +109
let bus_devs = cfgmgr32::list_interfaces(GUID_DEVINTERFACE_USB_HOST_CONTROLLER, None)
.iter()
.flat_map(|i| get_device_interface_property::<WCString>(i, DEVPKEY_Device_InstanceId))
.flat_map(|d| DevInst::from_instance_id(&d))
.flat_map(|d| d.children())
.map(|d| d.instance_id().to_string())
.enumerate()
.map(|v| (v.1, (v.0 + 1) as u8))
.collect::<HashMap<String, u8>>();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existing bus number using DEVPKEY_Device_BusNumber is indeed wrong (#70) but as mentioned in that issue I'm considering just exposing the bus ID as a string rather than trying to map it to a small integer.

}
}
// Find last #USB(x)
// PCIROOT(B2)#PCI(0300)#PCI(0000)#USBROOT(0)#USB(1)#USB(2)#USBMI(3)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be the case

@CCnut
Copy link
Author

CCnut commented Aug 4, 2024

The usb bus number on Windows is related to GUID_DEVINTERFACE_USB_HOST_CONTROLLER.

Here's what I've tried so far to get the bus number.

Use a prefix of DEVPKEY_Device_LocationPaths such as PCIROOT(B2)#PCI(0300)#PCI(0000) to compare with the device returned by GUID_DEVINTERFACE_USB_HOST_CONTROLLER.
Alternatively, use the root hub's DEVPKEY_Device_PhysicalDeviceLocation, which are in reverse order of the bus number.

@kevinmehall
Copy link
Owner

Superseded by #71, but thanks for your work on this in helping figure out what was needed here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants