use std::collections::{BTreeMap, HashSet};
use std::path::PathBuf;
use serde::Serialize;
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
pub struct UsbInterface {
pub number: u32,
pub class_code: u8,
pub sub_class: u8,
pub protocol: u8,
pub driver: String,
}
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
pub struct UsbDevice {
pub sysfs_path: PathBuf,
pub bus_port: String,
pub vendor_id: u16,
pub product_id: u16,
pub manufacturer: String,
pub product: String,
pub serial: String,
pub version: String,
pub speed: u32,
pub max_power_ma: u32,
pub device_class: u8,
pub device_sub_class: u8,
pub device_protocol: u8,
pub bus_num: u32,
pub dev_num: u32,
pub rx_lanes: u32,
pub tx_lanes: u32,
pub removable: String,
pub num_interfaces: u32,
pub num_configurations: u32,
pub interfaces: Vec<UsbInterface>,
pub children: Vec<UsbDevice>,
pub is_hub: bool,
pub is_root_hub: bool,
pub raw_attributes: BTreeMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum LinkSpeed {
Unknown,
Low,
Full,
High,
Super,
SuperPlus,
SuperPlus20,
Usb4,
}
impl LinkSpeed {
pub fn label(self) -> &'static str {
match self {
LinkSpeed::Usb4 => "USB4 40 Gbps",
LinkSpeed::SuperPlus20 => "USB4 20 Gbps",
LinkSpeed::SuperPlus => "SuperSpeed+ 10 Gbps",
LinkSpeed::Super => "SuperSpeed 5 Gbps",
LinkSpeed::High => "High Speed 480 Mbps",
LinkSpeed::Full => "Full Speed 12 Mbps",
LinkSpeed::Low => "Low Speed 1.5 Mbps",
LinkSpeed::Unknown => "Unknown speed",
}
}
}
pub fn link_speed_tier(mbps: u32) -> LinkSpeed {
match mbps {
s if s >= 40000 => LinkSpeed::Usb4,
s if s >= 20000 => LinkSpeed::SuperPlus20,
s if s >= 10000 => LinkSpeed::SuperPlus,
s if s >= 5000 => LinkSpeed::Super,
s if s >= 480 => LinkSpeed::High,
s if s >= 12 => LinkSpeed::Full,
s if s >= 2 => LinkSpeed::Low,
_ => LinkSpeed::Unknown,
}
}
impl UsbDevice {
pub fn display_name(&self) -> String {
if !self.product.is_empty() {
self.product.clone()
} else {
format!("{:04x}:{:04x}", self.vendor_id, self.product_id)
}
}
pub fn speed_label(&self) -> &'static str {
speed_label(self.speed)
}
pub fn link_speed_tier(&self) -> LinkSpeed {
link_speed_tier(self.speed)
}
pub fn power_label(&self) -> Option<String> {
if self.max_power_ma == 0 {
return None;
}
Some(if self.max_power_ma >= 1000 {
format!("{:.1} W", self.max_power_ma as f64 / 1000.0)
} else {
format!("{} mA", self.max_power_ma)
})
}
pub fn parent_bus_port(&self) -> Option<String> {
if self.is_root_hub {
return None;
}
if let Some((head, _)) = self.bus_port.rsplit_once('.') {
return Some(head.to_string());
}
if let Some((bus, _)) = self.bus_port.split_once('-') {
return Some(format!("usb{bus}"));
}
None
}
}
pub fn speed_label(speed: u32) -> &'static str {
link_speed_tier(speed).label()
}
pub fn tree_roots(devs: &[UsbDevice]) -> Vec<&UsbDevice> {
let names: HashSet<&str> = devs.iter().map(|d| d.bus_port.as_str()).collect();
devs.iter()
.filter(|d| match d.parent_bus_port() {
None => true,
Some(p) => !names.contains(p.as_str()),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn speed_label_thresholds() {
assert_eq!(speed_label(0), "Unknown speed");
assert_eq!(speed_label(2), "Low Speed 1.5 Mbps");
assert_eq!(speed_label(12), "Full Speed 12 Mbps");
assert_eq!(speed_label(480), "High Speed 480 Mbps");
assert_eq!(speed_label(5000), "SuperSpeed 5 Gbps");
assert_eq!(speed_label(10000), "SuperSpeed+ 10 Gbps");
assert_eq!(speed_label(20000), "USB4 20 Gbps");
assert_eq!(speed_label(40000), "USB4 40 Gbps");
}
#[test]
fn link_speed_tier_buckets() {
assert_eq!(link_speed_tier(0), LinkSpeed::Unknown);
assert_eq!(link_speed_tier(2), LinkSpeed::Low);
assert_eq!(link_speed_tier(12), LinkSpeed::Full);
assert_eq!(link_speed_tier(480), LinkSpeed::High);
assert_eq!(link_speed_tier(5_000), LinkSpeed::Super);
assert_eq!(link_speed_tier(10_000), LinkSpeed::SuperPlus);
assert_eq!(link_speed_tier(20_000), LinkSpeed::SuperPlus20);
assert_eq!(link_speed_tier(40_000), LinkSpeed::Usb4);
}
#[test]
fn link_speed_label_matches_legacy() {
for mbps in [0, 2, 12, 480, 5_000, 10_000, 20_000, 40_000] {
assert_eq!(link_speed_tier(mbps).label(), speed_label(mbps));
}
}
#[test]
fn parent_bus_port_resolves_levels() {
let mk = |bp: &str, root: bool| UsbDevice {
bus_port: bp.into(),
is_root_hub: root,
..Default::default()
};
assert_eq!(mk("5-2.4.1", false).parent_bus_port().as_deref(), Some("5-2.4"));
assert_eq!(mk("1-1", false).parent_bus_port().as_deref(), Some("usb1"));
assert_eq!(mk("usb5", true).parent_bus_port(), None);
}
#[test]
fn tree_roots_includes_root_hubs_and_orphans() {
let root = UsbDevice {
bus_port: "usb1".into(),
is_root_hub: true,
..Default::default()
};
let attached = UsbDevice {
bus_port: "1-1".into(),
..Default::default()
};
let orphan = UsbDevice {
bus_port: "9-9".into(),
..Default::default()
};
let devs = vec![root, attached, orphan];
let roots: Vec<&str> = tree_roots(&devs).iter().map(|d| d.bus_port.as_str()).collect();
assert!(roots.contains(&"usb1"));
assert!(roots.contains(&"9-9"));
assert!(!roots.contains(&"1-1"));
}
#[test]
fn power_label_formats() {
let d = UsbDevice {
max_power_ma: 0,
..Default::default()
};
assert!(d.power_label().is_none());
let d = UsbDevice {
max_power_ma: 100,
..Default::default()
};
assert_eq!(d.power_label().as_deref(), Some("100 mA"));
let d = UsbDevice {
max_power_ma: 1500,
..Default::default()
};
assert_eq!(d.power_label().as_deref(), Some("1.5 W"));
}
#[test]
fn display_name_falls_back_to_vidpid() {
let mut d = UsbDevice {
vendor_id: 0x05AC,
product_id: 0x12A8,
..Default::default()
};
assert_eq!(d.display_name(), "05ac:12a8");
d.product = "iPhone".into();
assert_eq!(d.display_name(), "iPhone");
}
}