use std::collections::HashMap;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;
use futures::future::BoxFuture;
use futures::{FutureExt, Stream, StreamExt};
use crate::mtp::{Error, MtpDeviceInfo};
use crate::transport::NusbTransport;
pub const DEFAULT_SETTLE_DELAY: Duration = Duration::from_millis(500);
#[derive(Debug, Clone)]
pub enum HotplugEvent {
Arrived(MtpDeviceInfo),
Left(MtpDeviceInfo),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct DeviceKey {
location_id: u64,
vendor_id: u16,
product_id: u16,
serial_number: Option<String>,
}
impl DeviceKey {
fn of(info: &MtpDeviceInfo) -> Self {
Self {
location_id: info.location_id,
vendor_id: info.vendor_id,
product_id: info.product_id,
serial_number: info.serial_number.clone(),
}
}
}
fn diff(
known: &mut HashMap<DeviceKey, MtpDeviceInfo>,
current: Vec<MtpDeviceInfo>,
) -> Vec<HotplugEvent> {
let current: HashMap<DeviceKey, MtpDeviceInfo> = current
.into_iter()
.map(|i| (DeviceKey::of(&i), i))
.collect();
let mut events: Vec<HotplugEvent> = known
.iter()
.filter(|(key, _)| !current.contains_key(key))
.map(|(_, info)| HotplugEvent::Left(info.clone()))
.collect();
events.extend(
current
.iter()
.filter(|(key, _)| !known.contains_key(key))
.map(|(_, info)| HotplugEvent::Arrived(info.clone())),
);
*known = current;
events
}
#[derive(Debug, Clone)]
pub struct DeviceWatchBuilder {
known_devices: Vec<(u16, u16)>,
settle_delay: Duration,
}
impl Default for DeviceWatchBuilder {
fn default() -> Self {
Self {
known_devices: Vec::new(),
settle_delay: DEFAULT_SETTLE_DELAY,
}
}
}
impl DeviceWatchBuilder {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn known_devices(mut self, known: &[(u16, u16)]) -> Self {
self.known_devices = known.to_vec();
self
}
#[must_use]
pub fn settle_delay(mut self, delay: Duration) -> Self {
self.settle_delay = delay;
self
}
pub fn watch(self) -> Result<DeviceWatch, Error> {
let usb = nusb::watch_devices().map_err(crate::PtpError::Usb)?;
Ok(DeviceWatch {
usb,
known: HashMap::new(),
known_devices: self.known_devices,
settle_delay: self.settle_delay,
pending: Vec::new(),
settling: None,
started: false,
})
}
}
pub struct DeviceWatch {
usb: nusb::hotplug::HotplugWatch,
known: HashMap<DeviceKey, MtpDeviceInfo>,
known_devices: Vec<(u16, u16)>,
settle_delay: Duration,
pending: Vec<HotplugEvent>,
settling: Option<BoxFuture<'static, ()>>,
started: bool,
}
impl std::fmt::Debug for DeviceWatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DeviceWatch")
.field("known", &self.known.len())
.field("settle_delay", &self.settle_delay)
.field("pending", &self.pending.len())
.finish_non_exhaustive()
}
}
impl DeviceWatch {
fn enumerate(&mut self) {
match NusbTransport::list_mtp_devices_with_known(&self.known_devices) {
Ok(devices) => {
let current = devices.into_iter().map(MtpDeviceInfo::from_usb).collect();
self.pending.extend(diff(&mut self.known, current));
}
Err(e) => {
diag_debug!(
"hotplug enumeration failed, keeping last known device set: {}",
e
);
}
}
}
}
impl Stream for DeviceWatch {
type Item = HotplugEvent;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
loop {
if !this.pending.is_empty() {
return Poll::Ready(Some(this.pending.remove(0)));
}
if !this.started {
this.started = true;
this.enumerate();
continue;
}
if let Some(settle) = this.settling.as_mut() {
match settle.poll_unpin(cx) {
Poll::Ready(()) => {
this.settling = None;
this.enumerate();
continue;
}
Poll::Pending => return Poll::Pending,
}
}
match this.usb.poll_next_unpin(cx) {
Poll::Ready(Some(_)) => {
this.settling = Some(sleep(this.settle_delay).boxed());
continue;
}
Poll::Ready(None) => return Poll::Ready(None),
Poll::Pending => return Poll::Pending,
}
}
}
}
async fn sleep(duration: Duration) {
if !duration.is_zero() {
futures_timer::Delay::new(duration).await;
}
}
pub fn watch_devices() -> Result<DeviceWatch, Error> {
DeviceWatchBuilder::new().watch()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::transport::MtpMatchReason;
fn info(location_id: u64, serial: Option<&str>) -> MtpDeviceInfo {
MtpDeviceInfo {
vendor_id: 0x18d1,
product_id: 0x4ee1,
manufacturer: Some("Google".into()),
product: Some("Pixel 9 Pro XL".into()),
serial_number: serial.map(String::from),
location_id,
speed: None,
match_reason: MtpMatchReason::StandardClass,
}
}
fn serials(events: &[HotplugEvent]) -> Vec<(&'static str, Option<String>)> {
events
.iter()
.map(|e| match e {
HotplugEvent::Arrived(i) => ("arrived", i.serial_number.clone()),
HotplugEvent::Left(i) => ("left", i.serial_number.clone()),
})
.collect()
}
#[test]
fn first_enumeration_reports_every_connected_device_as_arrived() {
let mut known = HashMap::new();
let events = diff(&mut known, vec![info(1, Some("a")), info(2, Some("b"))]);
assert_eq!(events.len(), 2);
assert!(events.iter().all(|e| matches!(e, HotplugEvent::Arrived(_))));
assert_eq!(known.len(), 2);
}
#[test]
fn unchanged_device_set_produces_no_events() {
let mut known = HashMap::new();
diff(&mut known, vec![info(1, Some("a"))]);
let events = diff(&mut known, vec![info(1, Some("a"))]);
assert!(events.is_empty());
}
#[test]
fn unplugged_device_is_reported_with_the_info_last_seen() {
let mut known = HashMap::new();
diff(&mut known, vec![info(1, Some("a")), info(2, Some("b"))]);
let events = diff(&mut known, vec![info(1, Some("a"))]);
assert_eq!(events.len(), 1);
match &events[0] {
HotplugEvent::Left(i) => {
assert_eq!(i.serial_number.as_deref(), Some("b"));
assert_eq!(i.product.as_deref(), Some("Pixel 9 Pro XL"));
}
other => panic!("expected Left, got {other:?}"),
}
assert_eq!(known.len(), 1);
}
#[test]
fn device_swapped_on_the_same_port_reports_left_before_arrived() {
let mut known = HashMap::new();
diff(&mut known, vec![info(1, Some("a"))]);
let events = diff(&mut known, vec![info(1, Some("b"))]);
assert_eq!(
serials(&events),
vec![
("left", Some("a".to_string())),
("arrived", Some("b".to_string()))
]
);
}
#[test]
fn device_re_enumerating_into_file_transfer_mode_reports_left_then_arrived() {
let mut known = HashMap::new();
let mut charging = info(1, Some("a"));
charging.product_id = 0x4ee7; diff(&mut known, vec![charging]);
let events = diff(&mut known, vec![info(1, Some("a"))]);
assert_eq!(
serials(&events),
vec![
("left", Some("a".to_string())),
("arrived", Some("a".to_string()))
]
);
}
#[test]
fn devices_without_serials_are_distinguished_by_port() {
let mut known = HashMap::new();
let events = diff(&mut known, vec![info(1, None), info(2, None)]);
assert_eq!(events.len(), 2);
assert_eq!(known.len(), 2);
let events = diff(&mut known, vec![info(2, None)]);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], HotplugEvent::Left(_)));
}
#[test]
fn all_devices_gone_reports_every_one_as_left() {
let mut known = HashMap::new();
diff(&mut known, vec![info(1, Some("a")), info(2, Some("b"))]);
let events = diff(&mut known, vec![]);
assert_eq!(events.len(), 2);
assert!(events.iter().all(|e| matches!(e, HotplugEvent::Left(_))));
assert!(known.is_empty());
}
}