nusb/src/enumeration.rs
pgorgon-hem f10a5f2c22
Add usb version to device info (#123)
Introduced a new `usb_version` field to represent the encoded USB specification version (`bcdUSB`) in DeviceInfo struct. This change includes updates to data structure, a new accessor method, and enhanced debug formatting to display the `usb_version` field.
2025-04-01 19:32:10 -06:00

696 lines
21 KiB
Rust

#[cfg(target_os = "windows")]
use std::ffi::{OsStr, OsString};
#[cfg(any(target_os = "linux", target_os = "android"))]
use crate::platform::SysfsPath;
use crate::{Device, Error, MaybeFuture};
/// Opaque device identifier
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
pub struct DeviceId(pub(crate) crate::platform::DeviceId);
/// Information about a device that can be obtained without opening it.
///
/// Found in the results of [`crate::list_devices`].
///
/// ### Platform-specific notes
///
/// * Some fields are platform-specific
/// * Linux: `sysfs_path`
/// * Windows: `instance_id`, `parent_instance_id`, `port_number`, `driver`
/// * macOS: `registry_id`, `location_id`
#[derive(Clone)]
pub struct DeviceInfo {
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) path: SysfsPath,
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) busnum: u8,
#[cfg(target_os = "windows")]
pub(crate) instance_id: OsString,
#[cfg(target_os = "windows")]
pub(crate) location_paths: Vec<OsString>,
#[cfg(target_os = "windows")]
pub(crate) parent_instance_id: OsString,
#[cfg(target_os = "windows")]
pub(crate) port_number: u32,
#[cfg(target_os = "windows")]
pub(crate) devinst: crate::platform::DevInst,
#[cfg(target_os = "windows")]
pub(crate) driver: Option<String>,
#[cfg(target_os = "macos")]
pub(crate) registry_id: u64,
#[cfg(target_os = "macos")]
pub(crate) location_id: u32,
pub(crate) bus_id: String,
pub(crate) device_address: u8,
pub(crate) port_chain: Vec<u8>,
pub(crate) vendor_id: u16,
pub(crate) product_id: u16,
pub(crate) device_version: u16,
pub(crate) usb_version: u16,
pub(crate) class: u8,
pub(crate) subclass: u8,
pub(crate) protocol: u8,
pub(crate) max_packet_size_0: u8,
pub(crate) speed: Option<Speed>,
pub(crate) manufacturer_string: Option<String>,
pub(crate) product_string: Option<String>,
pub(crate) serial_number: Option<String>,
pub(crate) interfaces: Vec<InterfaceInfo>,
}
impl DeviceInfo {
/// Opaque identifier for the device.
pub fn id(&self) -> DeviceId {
#[cfg(target_os = "windows")]
{
DeviceId(self.devinst)
}
#[cfg(any(target_os = "linux", target_os = "android"))]
{
DeviceId(crate::platform::DeviceId {
bus: self.busnum,
addr: self.device_address,
})
}
#[cfg(target_os = "macos")]
{
DeviceId(self.registry_id)
}
}
/// *(Linux-only)* Sysfs path for the device.
#[doc(hidden)]
#[deprecated = "use `sysfs_path()` instead"]
#[cfg(target_os = "linux")]
pub fn path(&self) -> &SysfsPath {
&self.path
}
/// *(Linux-only)* Sysfs path for the device.
#[cfg(target_os = "linux")]
pub fn sysfs_path(&self) -> &std::path::Path {
&self.path.0
}
/// *(Linux-only)* Bus number.
///
/// On Linux, the `bus_id` is an integer and this provides the value as `u8`.
#[cfg(any(target_os = "linux", target_os = "android"))]
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 {
&self.parent_instance_id
}
/// *(Windows-only)* Port number
#[cfg(target_os = "windows")]
pub fn port_number(&self) -> u32 {
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> {
self.driver.as_deref()
}
/// *(macOS-only)* IOKit Location ID
#[cfg(target_os = "macos")]
pub fn location_id(&self) -> u32 {
self.location_id
}
/// *(macOS-only)* IOKit [Registry Entry ID](https://developer.apple.com/documentation/iokit/1514719-ioregistryentrygetregistryentryi?language=objc)
#[cfg(target_os = "macos")]
pub fn registry_entry_id(&self) -> u64 {
self.registry_id
}
/// 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.
pub fn device_address(&self) -> u8 {
self.device_address
}
/// The 16-bit number identifying the device's vendor, from the `idVendor` device descriptor field.
#[doc(alias = "idVendor")]
pub fn vendor_id(&self) -> u16 {
self.vendor_id
}
/// The 16-bit number identifying the product, from the `idProduct` device descriptor field.
#[doc(alias = "idProduct")]
pub fn product_id(&self) -> u16 {
self.product_id
}
/// The device version, normally encoded as BCD, from the `bcdDevice` device descriptor field.
#[doc(alias = "bcdDevice")]
pub fn device_version(&self) -> u16 {
self.device_version
}
/// Encoded version of the USB specification, from the `bcdUSB` device descriptor field.
#[doc(alias = "bcdUSB")]
pub fn usb_version(&self) -> u16 {
self.usb_version
}
/// Code identifying the standard device class, from the `bDeviceClass` device descriptor field.
///
/// `0x00`: specified at the interface level.\
/// `0xFF`: vendor-defined.
#[doc(alias = "bDeviceClass")]
pub fn class(&self) -> u8 {
self.class
}
/// Standard subclass, from the `bDeviceSubClass` device descriptor field.
#[doc(alias = "bDeviceSubClass")]
pub fn subclass(&self) -> u8 {
self.subclass
}
/// Standard protocol, from the `bDeviceProtocol` device descriptor field.
#[doc(alias = "bDeviceProtocol")]
pub fn protocol(&self) -> u8 {
self.protocol
}
/// Maximum packet size for endpoint zero.
#[doc(alias = "bMaxPacketSize0")]
pub fn max_packet_size_0(&self) -> u8 {
self.max_packet_size_0
}
/// Connection speed
pub fn speed(&self) -> Option<Speed> {
self.speed
}
/// Manufacturer string, if available without device IO.
///
/// ### Platform-specific notes
/// * Windows: Windows does not cache the manufacturer string, and
/// this will return `None` regardless of whether a descriptor exists.
#[doc(alias = "iManufacturer")]
pub fn manufacturer_string(&self) -> Option<&str> {
self.manufacturer_string.as_deref()
}
/// Product string, if available without device IO.
#[doc(alias = "iProduct")]
pub fn product_string(&self) -> Option<&str> {
self.product_string.as_deref()
}
/// Serial number string, if available without device IO.
#[doc(alias = "iSerial")]
pub fn serial_number(&self) -> Option<&str> {
self.serial_number.as_deref()
}
/// Iterator over the device's interfaces.
///
/// This returns summary information about the interfaces in the device's
/// active configuration for the purposes of matching devices prior to
/// opening them.
///
/// Additional information about interfaces can be found in the
/// configuration descriptor after opening the device by calling
/// [`Device::active_configuration`].
///
/// ### Platform-specific notes:
/// * Windows: this is only available for composite devices bound to the
/// `usbccgp` driver, and will be empty if the entire device is bound to
/// a specific driver.
/// * Windows: When interfaces are grouped by an interface
/// association descriptor, this returns details from the interface
/// association descriptor and does not include each of the associated
/// interfaces.
pub fn interfaces(&self) -> impl Iterator<Item = &InterfaceInfo> {
self.interfaces.iter()
}
/// Open the device
pub fn open(&self) -> impl MaybeFuture<Output = Result<Device, Error>> {
Device::open(self)
}
}
// Not derived so that we can format some fields in hex
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_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(
"device_version",
&format_args!("0x{:04X}", self.device_version),
)
.field("usb_version", &format_args!("0x{:04X}", self.usb_version))
.field("class", &format_args!("0x{:02X}", self.class))
.field("subclass", &format_args!("0x{:02X}", self.subclass))
.field("protocol", &format_args!("0x{:02X}", self.protocol))
.field("max_packet_size_0", &self.max_packet_size_0)
.field("speed", &self.speed)
.field("manufacturer_string", &self.manufacturer_string)
.field("product_string", &self.product_string)
.field("serial_number", &self.serial_number);
#[cfg(target_os = "linux")]
{
s.field("sysfs_path", &self.path);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
{
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);
}
#[cfg(target_os = "macos")]
{
s.field("location_id", &format_args!("0x{:08X}", self.location_id));
s.field(
"registry_entry_id",
&format_args!("0x{:08X}", self.registry_id),
);
}
s.field("interfaces", &self.interfaces);
s.finish()
}
}
/// USB connection speed
#[derive(Copy, Clone, Eq, PartialOrd, Ord, PartialEq, Hash, Debug)]
#[non_exhaustive]
pub enum Speed {
/// Low speed (1.5 Mbit)
Low,
/// Full speed (12 Mbit)
Full,
/// High speed (480 Mbit)
High,
/// Super speed (5000 Mbit)
Super,
/// Super speed (10000 Mbit)
SuperPlus,
}
impl Speed {
#[allow(dead_code)] // not used on all platforms
pub(crate) fn from_str(s: &str) -> Option<Self> {
match s {
"low" | "1.5" => Some(Speed::Low),
"full" | "12" => Some(Speed::Full),
"high" | "480" => Some(Speed::High),
"super" | "5000" => Some(Speed::Super),
"super+" | "10000" => Some(Speed::SuperPlus),
_ => None,
}
}
}
/// Summary information about a device's interface, available before opening a device.
#[derive(Clone)]
pub struct InterfaceInfo {
pub(crate) interface_number: u8,
pub(crate) class: u8,
pub(crate) subclass: u8,
pub(crate) protocol: u8,
pub(crate) interface_string: Option<String>,
}
impl InterfaceInfo {
/// Identifier for the interface from the `bInterfaceNumber` descriptor field.
pub fn interface_number(&self) -> u8 {
self.interface_number
}
/// Code identifying the standard interface class, from the `bInterfaceClass` interface descriptor field.
pub fn class(&self) -> u8 {
self.class
}
/// Standard subclass, from the `bInterfaceSubClass` interface descriptor field.
pub fn subclass(&self) -> u8 {
self.subclass
}
/// Standard protocol, from the `bInterfaceProtocol` interface descriptor field.
pub fn protocol(&self) -> u8 {
self.protocol
}
/// Interface string descriptor value as cached by the OS.
pub fn interface_string(&self) -> Option<&str> {
self.interface_string.as_deref()
}
}
// Not derived so that we can format some fields in hex
impl std::fmt::Debug for InterfaceInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("InterfaceInfo")
.field("interface_number", &self.interface_number)
.field("class", &format_args!("0x{:02X}", self.class))
.field("subclass", &format_args!("0x{:02X}", self.subclass))
.field("protocol", &format_args!("0x{:02X}", self.protocol))
.field("interface_string", &self.interface_string)
.finish()
}
}
/// USB host controller type
#[derive(Copy, Clone, Eq, PartialOrd, Ord, PartialEq, Hash, Debug)]
#[non_exhaustive]
pub enum UsbControllerType {
/// xHCI controller (USB 3.0+)
XHCI,
/// EHCI controller (USB 2.0)
EHCI,
/// OHCI controller (USB 1.1)
OHCI,
/// UHCI controller (USB 1.x) (proprietary interface created by Intel)
UHCI,
/// VHCI controller (virtual internal USB)
VHCI,
}
impl UsbControllerType {
#[allow(dead_code)] // not used on all platforms
pub(crate) fn from_str(s: &str) -> Option<Self> {
let lower_s = s.to_owned().to_ascii_lowercase();
match lower_s
.find("hci")
.filter(|i| *i > 0)
.and_then(|i| lower_s.bytes().nth(i - 1))
{
Some(b'x') => Some(UsbControllerType::XHCI),
Some(b'e') => Some(UsbControllerType::EHCI),
Some(b'o') => Some(UsbControllerType::OHCI),
Some(b'v') => Some(UsbControllerType::VHCI),
Some(b'u') => Some(UsbControllerType::UHCI),
_ => None,
}
}
}
/// Information about a system USB bus.
///
/// Platform-specific fields:
/// * Linux: `path`, `parent_path`, `busnum`, `root_hub`
/// * Windows: `instance_id`, `parent_instance_id`, `location_paths`, `devinst`, `root_hub_description`
/// * macOS: `registry_id`, `location_id`, `name`, `provider_class_name`, `class_name`
pub struct BusInfo {
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) path: SysfsPath,
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) parent_path: SysfsPath,
/// The phony root hub device
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) root_hub: DeviceInfo,
#[cfg(any(target_os = "linux", target_os = "android"))]
pub(crate) busnum: u8,
#[cfg(target_os = "windows")]
pub(crate) instance_id: OsString,
#[cfg(target_os = "windows")]
pub(crate) location_paths: Vec<OsString>,
#[cfg(target_os = "windows")]
pub(crate) devinst: crate::platform::DevInst,
#[cfg(target_os = "windows")]
pub(crate) root_hub_description: String,
#[cfg(target_os = "windows")]
pub(crate) parent_instance_id: OsString,
#[cfg(target_os = "macos")]
pub(crate) registry_id: u64,
#[cfg(target_os = "macos")]
pub(crate) location_id: u32,
#[cfg(target_os = "macos")]
pub(crate) provider_class_name: String,
#[cfg(target_os = "macos")]
pub(crate) class_name: String,
#[cfg(target_os = "macos")]
pub(crate) name: Option<String>,
pub(crate) driver: Option<String>,
/// System ID for the bus
pub(crate) bus_id: String,
/// Detected USB controller type
pub(crate) controller_type: Option<UsbControllerType>,
}
impl BusInfo {
/// *(Linux-only)* Sysfs path for the bus.
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn sysfs_path(&self) -> &std::path::Path {
&self.path.0
}
/// *(Linux-only)* Sysfs path for the parent controller
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn parent_sysfs_path(&self) -> &std::path::Path {
&self.parent_path.0
}
/// *(Linux-only)* Bus number.
///
/// On Linux, the `bus_id` is an integer and this provides the value as `u8`.
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn busnum(&self) -> u8 {
self.busnum
}
/// *(Linux-only)* The root hub [`DeviceInfo`] representing the bus.
#[cfg(any(target_os = "linux", target_os = "android"))]
pub fn root_hub(&self) -> &DeviceInfo {
&self.root_hub
}
/// *(Windows-only)* Instance ID path of this device
#[cfg(target_os = "windows")]
pub fn instance_id(&self) -> &OsStr {
&self.instance_id
}
/// *(Windows-only)* Instance ID path of the parent device
#[cfg(target_os = "windows")]
pub fn parent_instance_id(&self) -> &OsStr {
&self.parent_instance_id
}
/// *(Windows-only)* Location paths property
#[cfg(target_os = "windows")]
pub fn location_paths(&self) -> &[OsString] {
&self.location_paths
}
/// *(Windows-only)* Device Instance ID
#[cfg(target_os = "windows")]
pub fn devinst(&self) -> crate::platform::DevInst {
self.devinst
}
/// *(macOS-only)* IOKit Location ID
#[cfg(target_os = "macos")]
pub fn location_id(&self) -> u32 {
self.location_id
}
/// *(macOS-only)* IOKit [Registry Entry ID](https://developer.apple.com/documentation/iokit/1514719-ioregistryentrygetregistryentryi?language=objc)
#[cfg(target_os = "macos")]
pub fn registry_entry_id(&self) -> u64 {
self.registry_id
}
/// *(macOS-only)* IOKit provider class name
#[cfg(target_os = "macos")]
pub fn provider_class_name(&self) -> &str {
&self.provider_class_name
}
/// *(macOS-only)* IOKit class name
#[cfg(target_os = "macos")]
pub fn class_name(&self) -> &str {
&self.class_name
}
/// *(macOS-only)* Name of the bus
#[cfg(target_os = "macos")]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
/// Driver associated with the bus
pub fn driver(&self) -> Option<&str> {
self.driver.as_deref()
}
/// Identifier for the bus
pub fn bus_id(&self) -> &str {
&self.bus_id
}
/// Detected USB controller type
///
/// None means the controller type could not be determined.
///
/// ### Platform-specific notes
///
/// * Linux: Parsed from driver in use.
/// * macOS: The IOService entry matched.
/// * Windows: Parsed from the numbers following ROOT_HUB in the instance_id.
pub fn controller_type(&self) -> Option<UsbControllerType> {
self.controller_type
}
/// System name of the bus
///
/// ### Platform-specific notes
///
/// * Linux: The root hub product string.
/// * macOS: The [IONameMatched](https://developer.apple.com/documentation/bundleresources/information_property_list/ionamematch) key of the IOService entry.
/// * Windows: Description field of the root hub device. How the bus will appear in Device Manager.
pub fn system_name(&self) -> Option<&str> {
#[cfg(any(target_os = "linux", target_os = "android"))]
{
self.root_hub.product_string()
}
#[cfg(target_os = "windows")]
{
Some(&self.root_hub_description)
}
#[cfg(target_os = "macos")]
{
self.name.as_deref()
}
}
}
impl std::fmt::Debug for BusInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("BusInfo");
#[cfg(any(target_os = "linux", target_os = "android"))]
{
s.field("sysfs_path", &self.path);
s.field("parent_sysfs_path", &self.parent_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);
}
#[cfg(target_os = "macos")]
{
s.field("location_id", &format_args!("0x{:08X}", self.location_id));
s.field(
"registry_entry_id",
&format_args!("0x{:08X}", self.registry_id),
);
s.field("class_name", &self.class_name);
s.field("provider_class_name", &self.provider_class_name);
}
s.field("bus_id", &self.bus_id)
.field("system_name", &self.system_name())
.field("controller_type", &self.controller_type)
.field("driver", &self.driver);
s.finish()
}
}