mtp_rs/mtp/device.rs
1//! MtpDevice - the main entry point for MTP operations.
2
3use crate::mtp::backend::usb::UsbBackend;
4use crate::mtp::backend::{Backend, MtpBackend};
5use crate::mtp::{Capabilities, DeviceEvent, DeviceInfo, Error, Storage, StorageId};
6use crate::ptp::PtpSession;
7use crate::transport::{NusbTransport, Transport};
8use std::sync::Arc;
9use std::time::Duration;
10
11/// An MTP device connection.
12///
13/// This is the main entry point for interacting with MTP devices.
14/// Use `MtpDevice::open_first()` to connect to the first available device,
15/// or `MtpDevice::builder()` for more control.
16///
17/// The device is a thin façade over a backend-neutral implementation (the internal `MtpBackend`
18/// trait). Today the only backend is PTP-over-USB (which also drives the virtual and mock
19/// transports); a Windows WPD backend is planned. Consumers work against the neutral
20/// [`crate::mtp`] types throughout.
21///
22/// # Example
23///
24/// ```rust,no_run
25/// use mtp_rs::mtp::MtpDevice;
26///
27/// # async fn example() -> Result<(), mtp_rs::Error> {
28/// // Open the first MTP device
29/// let device = MtpDevice::open_first().await?;
30///
31/// println!("Connected to: {} {}",
32/// device.device_info().manufacturer,
33/// device.device_info().model);
34///
35/// // Get storages
36/// for storage in device.storages().await? {
37/// println!("Storage: {} ({} free)",
38/// storage.info().description,
39/// storage.info().free_space);
40/// }
41/// # Ok(())
42/// # }
43/// ```
44#[derive(Clone)]
45pub struct MtpDevice {
46 pub(crate) backend: Arc<dyn MtpBackend>,
47}
48
49impl MtpDevice {
50 /// Create a builder for configuring device options.
51 pub fn builder() -> MtpDeviceBuilder {
52 MtpDeviceBuilder::new()
53 }
54
55 /// Open the first available MTP device with default settings.
56 pub async fn open_first() -> Result<Self, Error> {
57 Self::builder().open_first().await
58 }
59
60 /// Open a device at a specific USB location (port) with default settings.
61 ///
62 /// Use `list_devices()` to get available location IDs.
63 pub async fn open_by_location(location_id: u64) -> Result<Self, Error> {
64 Self::builder().open_by_location(location_id).await
65 }
66
67 /// Open a device by its serial number with default settings.
68 ///
69 /// This identifies a specific physical device regardless of which USB port
70 /// it's connected to.
71 pub async fn open_by_serial(serial: &str) -> Result<Self, Error> {
72 Self::builder().open_by_serial(serial).await
73 }
74
75 /// Reset the USB transport state of the device with this serial, without
76 /// opening a session.
77 ///
78 /// Sends the USB Still Image Class Device Reset request (`bRequest=0x66`),
79 /// clears halted bulk endpoints, and drains stale bulk data. This is the USB
80 /// **transport-level** reset, not the in-session `ResetDevice` (0x1010) PTP
81 /// operation: it works precisely when the device is too confused for PTP
82 /// traffic, which is when you need it.
83 ///
84 /// # Warning: on Android this can break MTP until the user replugs
85 ///
86 /// **Treat this as a last resort, not a recovery step.** Sending the reset to
87 /// a *healthy* Pixel 9 Pro XL permanently killed its MTP function: Android's
88 /// `MtpServer` lost its endpoint read (`ECANCELED`, then `EPIPE`) and never
89 /// re-armed, while the USB device controller stayed `configured`. The phone
90 /// kept enumerating and kept showing up in a device list, and answered
91 /// nothing. Ten spaced reopens over ~100 s all timed out; only a physical
92 /// unplug and replug brought it back (verified on a Pixel 9 Pro XL,
93 /// macOS/nusb + `adb logcat`, 2026-07-21).
94 ///
95 /// Android is the most common MTP device class, so reach for this only after
96 /// spaced reopens have already failed, or on a device that's *already*
97 /// unreachable, where you can't make things much worse. See
98 /// `docs/notes/android-wedges-and-the-reset-kill-switch.md`.
99 ///
100 /// # Why this isn't a method on an open device
101 ///
102 /// It only claims the USB interface and stops there. The regular opens run
103 /// `OpenSession` + `GetDeviceInfo`, which is exactly what a wedged device
104 /// can't answer, so a reset hanging off an already-open [`MtpDevice`] would
105 /// be useless in the case it exists for. **Drop your device first**: holding
106 /// it keeps the interface claimed, and this call would then fail to claim it.
107 /// You have to reopen afterwards regardless, since the PTP session is gone.
108 ///
109 /// # Recovering a wedged device
110 ///
111 /// After [`Error::DeviceReset`], when an operation hangs and never returns
112 /// (the Android signature: no error at all), or when every operation fails
113 /// with "Transaction ID mismatch" / "expected Response container type":
114 ///
115 /// 1. Drop the [`MtpDevice`] (and any [`Storage`] handles).
116 /// 2. Wait a few seconds **quiet**, with no USB traffic at all.
117 /// 3. Reopen with idle-spaced retries, several of them.
118 /// 4. Only if every reopen failed, and knowing the Android warning above,
119 /// call this and then repeat steps 2 and 3.
120 ///
121 /// Step 3 is where consumers go wrong: don't try once and give up, and don't
122 /// hammer close/open in a tight loop (that keeps the device busy and
123 /// re-wedges it into a hard `Timeout`). Expect the early attempts to fail.
124 /// A Pixel's wedge cleared on a fresh open with no reset at all (verified on
125 /// a Pixel 9 Pro XL, macOS/nusb, 2026-07-20). On a Galaxy S23 Ultra the
126 /// observed sequence was reset, then a reopen returning `Timeout`, then one
127 /// returning `SessionAlreadyOpen`, then success (verified on SM-S918B,
128 /// macOS/nusb, 2026-07-20); the control without a reset was never run, so
129 /// it's unknown whether spaced reopens alone would have sufficed there.
130 ///
131 /// # Errors
132 ///
133 /// [`Error::NoDevice`] when no USB device has that serial, and
134 /// [`Error::Unsupported`] for a virtual device, which is a filesystem with no
135 /// USB transport to reset.
136 pub async fn reset_by_serial(serial: &str) -> Result<(), Error> {
137 Self::builder().reset_by_serial(serial).await
138 }
139
140 /// Reset the USB transport state of the device at this location, without
141 /// opening a session.
142 ///
143 /// See [`reset_by_serial`](Self::reset_by_serial) for the full contract and
144 /// the recovery sequence to follow.
145 ///
146 /// **Last resort on Android**: the reset can break the phone's MTP function
147 /// until the user physically replugs. Try spaced reopens first.
148 pub async fn reset_by_location(location_id: u64) -> Result<(), Error> {
149 Self::builder().reset_by_location(location_id).await
150 }
151
152 /// Reset the USB transport state of the first available device, without
153 /// opening a session.
154 ///
155 /// See [`reset_by_serial`](Self::reset_by_serial) for the full contract and
156 /// the recovery sequence to follow.
157 ///
158 /// **Last resort on Android**: the reset can break the phone's MTP function
159 /// until the user physically replugs. Try spaced reopens first.
160 pub async fn reset_first() -> Result<(), Error> {
161 Self::builder().reset_first().await
162 }
163
164 /// List all available MTP devices without opening them.
165 pub fn list_devices() -> Result<Vec<MtpDeviceInfo>, Error> {
166 Self::list_devices_with_known(&[])
167 }
168
169 /// List all available MTP devices, including additional devices identified
170 /// by the given VID/PID pairs.
171 ///
172 /// Devices matching the provided VID/PID pairs are included in the results
173 /// even if their USB descriptors don't match standard MTP class codes. This
174 /// is useful for legacy or otherwise unusual devices with non-standard USB
175 /// descriptors that still speak MTP.
176 ///
177 /// # Example
178 ///
179 /// ```rust,no_run
180 /// use mtp_rs::mtp::MtpDevice;
181 ///
182 /// let devices = MtpDevice::list_devices_with_known(&[
183 /// (0x045E, 0x0710), // custom VID/PID
184 /// ])?;
185 /// for d in &devices {
186 /// println!("{:04x}:{:04x} {}", d.vendor_id, d.product_id,
187 /// d.product.as_deref().unwrap_or("unknown"));
188 /// }
189 /// # Ok::<(), mtp_rs::Error>(())
190 /// ```
191 pub fn list_devices_with_known(known: &[(u16, u16)]) -> Result<Vec<MtpDeviceInfo>, Error> {
192 let devices = NusbTransport::list_mtp_devices_with_known(known)?;
193 #[allow(unused_mut)]
194 let mut result: Vec<MtpDeviceInfo> =
195 devices.into_iter().map(MtpDeviceInfo::from_usb).collect();
196
197 #[cfg(feature = "virtual-device")]
198 result.extend(crate::transport::virtual_device::registry::list_virtual_devices());
199
200 Ok(result)
201 }
202
203 /// Get device information (backend-neutral identity).
204 #[must_use]
205 pub fn device_info(&self) -> &DeviceInfo {
206 self.backend.device_info()
207 }
208
209 /// What this device supports (backend-neutral capabilities).
210 ///
211 /// Replaces the old per-operation accessors. Advertised support can still be wrong on some
212 /// devices (see the Fujifilm quirk in `AGENTS.md`), so treat these as a strong hint.
213 #[must_use]
214 pub fn capabilities(&self) -> &Capabilities {
215 self.backend.capabilities()
216 }
217
218 /// Whether the device supports renaming objects.
219 ///
220 /// Convenience over [`capabilities()`](Self::capabilities)`.can_rename`.
221 #[must_use]
222 pub fn supports_rename(&self) -> bool {
223 self.backend.capabilities().can_rename
224 }
225
226 /// Whether the device supports creating objects (uploads and folders).
227 ///
228 /// Convenience over [`capabilities()`](Self::capabilities)`.can_upload`.
229 #[must_use]
230 pub fn supports_upload(&self) -> bool {
231 self.backend.capabilities().can_upload
232 }
233
234 /// Get all storages on the device.
235 pub async fn storages(&self) -> Result<Vec<Storage>, Error> {
236 let infos = self.backend.storages().await?;
237 Ok(infos
238 .into_iter()
239 .map(|info| Storage::new(Arc::clone(&self.backend), info.id, info))
240 .collect())
241 }
242
243 /// Get a specific storage by ID.
244 pub async fn storage(&self, id: StorageId) -> Result<Storage, Error> {
245 let info = self.backend.storage_info(id).await?;
246 Ok(Storage::new(Arc::clone(&self.backend), id, info))
247 }
248
249 /// Receive the next event from the device.
250 ///
251 /// This method awaits **indefinitely** on the underlying event channel until an
252 /// event arrives or the device disconnects. Always wrap this in
253 /// `tokio::time::timeout` (or equivalent) so you can check for shutdown.
254 ///
255 /// # Concurrency
256 ///
257 /// On the USB backend, event reading uses the USB interrupt endpoint, which is
258 /// independent from the bulk endpoints used by file operations, so it is safe to
259 /// call `next_event()` concurrently with other `MtpDevice` methods.
260 ///
261 /// If you wrap `MtpDevice` in a shared lock (for example, `Arc<Mutex<MtpDevice>>`),
262 /// do **not** hold that lock while awaiting `next_event()`: it will block all file
263 /// operations for the duration of the wait. Instead, clone the `MtpDevice` (it is
264 /// cheaply cloneable via `Arc` internally) and call `next_event()` on the clone
265 /// without holding the lock.
266 ///
267 /// # Returns
268 ///
269 /// - `Ok(event)` - An event was received from the device
270 /// - `Err(Error::Disconnected)` - Device was disconnected
271 /// - `Err(_)` - Other communication error
272 ///
273 /// # Example
274 ///
275 /// ```rust,no_run
276 /// use mtp_rs::mtp::{MtpDevice, DeviceEvent};
277 /// use mtp_rs::Error;
278 /// use tokio::time::{timeout, Duration};
279 ///
280 /// # async fn example() -> Result<(), Error> {
281 /// # let device = MtpDevice::open_first().await?;
282 /// loop {
283 /// match timeout(Duration::from_millis(200), device.next_event()).await {
284 /// Ok(Ok(event)) => {
285 /// match event {
286 /// DeviceEvent::ObjectAdded { handle } => {
287 /// println!("New object: {:?}", handle);
288 /// }
289 /// DeviceEvent::StoreRemoved { storage_id } => {
290 /// println!("Storage removed: {:?}", storage_id);
291 /// }
292 /// _ => {}
293 /// }
294 /// }
295 /// Ok(Err(Error::Disconnected)) => break,
296 /// Ok(Err(e)) => {
297 /// eprintln!("Error: {}", e);
298 /// break;
299 /// }
300 /// Err(_elapsed) => continue, // Timeout, check for shutdown etc.
301 /// }
302 /// }
303 /// # Ok(())
304 /// # }
305 /// ```
306 pub async fn next_event(&self) -> Result<DeviceEvent, Error> {
307 self.backend.next_event().await
308 }
309
310 /// Close the connection (best-effort; also happens on drop).
311 pub async fn close(self) -> Result<(), Error> {
312 self.backend.close().await
313 }
314}
315
316/// Information about an MTP device (without opening it).
317///
318/// This struct provides device identification at multiple levels:
319///
320/// - **Device identity** (`vendor_id`, `product_id`, `serial_number`): Identifies
321/// a specific physical device. Use this to recognize "John's phone" regardless
322/// of which USB port it's plugged into.
323///
324/// - **Port identity** (`location_id`): Identifies the physical USB port/location.
325/// Use this when you care about "the device on port 3" rather than which
326/// specific device it is. Stable across reconnections to the same port.
327///
328/// - **Display info** (`manufacturer`, `product`): Human-readable strings for
329/// showing device info to users.
330///
331/// # Example
332///
333/// ```rust,no_run
334/// use mtp_rs::mtp::MtpDevice;
335///
336/// let devices = MtpDevice::list_devices()?;
337/// for dev in &devices {
338/// println!("{} {} (serial: {:?})",
339/// dev.manufacturer.as_deref().unwrap_or("Unknown"),
340/// dev.product.as_deref().unwrap_or("Unknown"),
341/// dev.serial_number);
342/// }
343///
344/// // Save location_id to remember "the device on this port"
345/// // Save serial_number to remember "this specific phone"
346/// # Ok::<(), mtp_rs::Error>(())
347/// ```
348///
349/// Marked `#[non_exhaustive]` so future field additions don't break consumers
350/// that pattern-match or destructure. Construct via [`MtpDevice::list_devices`].
351#[derive(Debug, Clone)]
352#[non_exhaustive]
353pub struct MtpDeviceInfo {
354 /// USB vendor ID (assigned by USB-IF to each company).
355 ///
356 /// Examples: Google = `0x18d1`, Samsung = `0x04e8`, Apple = `0x05ac`
357 pub vendor_id: u16,
358
359 /// USB product ID (assigned by vendor to each product model).
360 ///
361 /// Note: The same device may report different product IDs depending on
362 /// its USB mode (MTP, ADB, charging-only, etc.).
363 pub product_id: u16,
364
365 /// Manufacturer name from USB descriptor.
366 ///
367 /// Examples: `"Google"`, `"Samsung"`, `"Apple Inc."`
368 ///
369 /// `None` if the device doesn't report a manufacturer string.
370 pub manufacturer: Option<String>,
371
372 /// Product name from USB descriptor.
373 ///
374 /// Examples: `"Pixel 9 Pro XL"`, `"Galaxy S24"`
375 ///
376 /// `None` if the device doesn't report a product string.
377 pub product: Option<String>,
378
379 /// Serial number uniquely identifying this specific device.
380 ///
381 /// Combined with `vendor_id` and `product_id`, this globally identifies
382 /// a single physical device. Survives reconnection to different ports.
383 ///
384 /// `None` if the device doesn't report a serial number.
385 pub serial_number: Option<String>,
386
387 /// Physical USB location identifier.
388 ///
389 /// Identifies the USB port/path where the device is connected. Stable
390 /// across reconnections to the same physical port, but changes if the
391 /// device is moved to a different port.
392 ///
393 /// Derived cross-platform from the USB bus ID and port chain (topology).
394 pub location_id: u64,
395
396 /// Negotiated USB link speed (slowest of host port, cable, and device).
397 ///
398 /// A USB 3.2 Gen 2 phone connected through a USB 2.0 charging cable
399 /// reports `High` (480 Mbit/s), not the device's capability.
400 ///
401 /// `None` if the OS doesn't report it for this device.
402 pub speed: Option<crate::transport::UsbSpeed>,
403
404 /// Why this USB device was classified as an MTP candidate.
405 pub match_reason: crate::transport::MtpMatchReason,
406}
407
408impl MtpDeviceInfo {
409 /// Build the neutral info from a USB-transport listing entry.
410 pub(crate) fn from_usb(d: crate::transport::UsbDeviceInfo) -> Self {
411 Self {
412 vendor_id: d.vendor_id,
413 product_id: d.product_id,
414 manufacturer: d.manufacturer,
415 product: d.product,
416 serial_number: d.serial_number,
417 location_id: d.location_id,
418 speed: d.speed,
419 match_reason: d.match_reason,
420 }
421 }
422
423 /// Format the device info for display.
424 #[must_use]
425 pub fn display(&self) -> String {
426 let manufacturer = self.manufacturer.as_deref().unwrap_or("Unknown");
427 let product = self.product.as_deref().unwrap_or("Unknown");
428 match &self.serial_number {
429 Some(serial) => format!(
430 "{} {} (serial: {}, location: {:08x})",
431 manufacturer, product, serial, self.location_id
432 ),
433 None => format!(
434 "{} {} (location: {:08x})",
435 manufacturer, product, self.location_id
436 ),
437 }
438 }
439}
440
441/// Builder for MtpDevice configuration.
442pub struct MtpDeviceBuilder {
443 timeout: Duration,
444 known_devices: Vec<(u16, u16)>,
445 backend: Backend,
446}
447
448impl MtpDeviceBuilder {
449 #[must_use]
450 pub fn new() -> Self {
451 Self {
452 timeout: NusbTransport::DEFAULT_TIMEOUT,
453 known_devices: Vec::new(),
454 backend: Backend::default(),
455 }
456 }
457
458 /// Choose which backend to open (default [`Backend::Auto`]).
459 ///
460 /// On Windows, `Auto` prefers WPD (for phones) and falls back to USB; pass [`Backend::Usb`] to
461 /// force PTP-over-USB to a Zadig/WinUSB-bound camera, or [`Backend::Wpd`] to force WPD.
462 #[must_use]
463 pub fn backend(mut self, backend: Backend) -> Self {
464 self.backend = backend;
465 self
466 }
467
468 /// If the configured backend selects WPD (Windows), try to open the first WPD device.
469 ///
470 /// Returns `Ok(None)` when WPD isn't selected, or when `Auto` found no WPD device (so the caller
471 /// falls back to USB). A forced [`Backend::Wpd`], or any non-"no device" WPD error, propagates.
472 async fn try_open_wpd_first(&self) -> Result<Option<MtpDevice>, Error> {
473 #[cfg(windows)]
474 if matches!(self.backend, Backend::Auto | Backend::Wpd) {
475 match crate::mtp::backend::wpd::WpdBackend::open_first().await {
476 Ok(b) => {
477 return Ok(Some(MtpDevice {
478 backend: Arc::new(b),
479 }))
480 }
481 Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
482 return Err(e)
483 }
484 Err(_) => {} // Auto + no WPD device: fall back to USB.
485 }
486 }
487 #[cfg(not(windows))]
488 if self.backend == Backend::Wpd {
489 return Err(Error::Unsupported);
490 }
491 Ok(None)
492 }
493
494 /// As [`try_open_wpd_first`](Self::try_open_wpd_first) but matching a serial number.
495 async fn try_open_wpd_by_serial(&self, serial: &str) -> Result<Option<MtpDevice>, Error> {
496 #[cfg(windows)]
497 if matches!(self.backend, Backend::Auto | Backend::Wpd) {
498 match crate::mtp::backend::wpd::WpdBackend::open_by_serial(serial).await {
499 Ok(b) => {
500 return Ok(Some(MtpDevice {
501 backend: Arc::new(b),
502 }))
503 }
504 Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
505 return Err(e)
506 }
507 Err(_) => {}
508 }
509 }
510 #[cfg(not(windows))]
511 {
512 let _ = serial;
513 if self.backend == Backend::Wpd {
514 return Err(Error::Unsupported);
515 }
516 }
517 Ok(None)
518 }
519
520 /// As [`try_open_wpd_by_serial`](Self::try_open_wpd_by_serial) but for a USB device (VID/PID plus
521 /// the USB descriptor serial), used to correlate an nusb `location_id` to a WPD device.
522 ///
523 /// The nusb and WPD *device* serials can differ (the Pixel's USB-descriptor serial isn't its WPD
524 /// serial), so the WPD side matches on VID/PID and, only when two identical models share it,
525 /// disambiguates by the USB serial resolved from the device tree.
526 async fn try_open_wpd_for_usb(
527 &self,
528 serial: Option<String>,
529 vid: u16,
530 pid: u16,
531 ) -> Result<Option<MtpDevice>, Error> {
532 #[cfg(windows)]
533 if matches!(self.backend, Backend::Auto | Backend::Wpd) {
534 match crate::mtp::backend::wpd::WpdBackend::open_for_usb(serial.clone(), vid, pid).await
535 {
536 Ok(b) => {
537 return Ok(Some(MtpDevice {
538 backend: Arc::new(b),
539 }))
540 }
541 Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
542 return Err(e)
543 }
544 Err(_) => {}
545 }
546 }
547 #[cfg(not(windows))]
548 {
549 let _ = (serial, vid, pid);
550 if self.backend == Backend::Wpd {
551 return Err(Error::Unsupported);
552 }
553 }
554 Ok(None)
555 }
556
557 /// Set bulk transfer timeout (default: 30 seconds).
558 ///
559 /// This timeout applies to file transfers, command responses, and event polling.
560 /// Use longer timeouts for large file operations.
561 #[must_use]
562 pub fn timeout(mut self, timeout: Duration) -> Self {
563 self.timeout = timeout;
564 self
565 }
566
567 /// Include additional devices identified by VID/PID pairs in the open-time
568 /// device scan.
569 ///
570 /// By default, the `open_first` / `open_by_serial` / `open_by_location`
571 /// convenience methods only consider devices whose USB descriptors match
572 /// standard MTP class codes. Pass extra VID/PID pairs here to also accept
573 /// legacy or otherwise unusual devices that speak MTP despite reporting
574 /// non-standard descriptors.
575 ///
576 /// This is the open-side counterpart to [`MtpDevice::list_devices_with_known`]:
577 /// pair them with the same list to enumerate and open the same set of devices.
578 #[must_use]
579 pub fn known_devices(mut self, known: &[(u16, u16)]) -> Self {
580 self.known_devices = known.to_vec();
581 self
582 }
583
584 /// Open the first available device.
585 pub async fn open_first(self) -> Result<MtpDevice, Error> {
586 if let Some(device) = self.try_open_wpd_first().await? {
587 return Ok(device);
588 }
589 let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
590 let device_info = devices
591 .into_iter()
592 .next()
593 .ok_or(crate::PtpError::NoDevice)?;
594 let device = device_info.open().map_err(crate::PtpError::Usb)?;
595 self.open_nusb_device(device).await
596 }
597
598 /// Open a device at a specific USB location (port).
599 ///
600 /// Use `MtpDevice::list_devices()` to get available location IDs.
601 /// Also checks the virtual device registry when the `virtual-device` feature is enabled.
602 ///
603 /// On Windows a phone at the location is bound to the WPD driver and can't be claimed over raw
604 /// USB, so this correlates the location to a WPD device and opens it there (for
605 /// [`Backend::Auto`]/[`Backend::Wpd`]); it falls back to raw USB for WinUSB-bound cameras and on
606 /// other platforms. The correlation is by **VID/PID** (the USB-descriptor serial and the WPD
607 /// serial can differ), so with two attached devices of the *same model* it may open the other
608 /// one — address those by serial instead.
609 pub async fn open_by_location(self, location_id: u64) -> Result<MtpDevice, Error> {
610 #[cfg(feature = "virtual-device")]
611 if let Some(config) =
612 crate::transport::virtual_device::registry::find_virtual_config_by_location(location_id)
613 {
614 return self.open_virtual(config).await;
615 }
616
617 let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
618 let device_info = devices
619 .into_iter()
620 .find(|d| d.location_id == location_id)
621 .ok_or(crate::PtpError::NoDevice)?;
622
623 if let Some(device) = self
624 .try_open_wpd_for_usb(
625 device_info.serial_number.clone(),
626 device_info.vendor_id,
627 device_info.product_id,
628 )
629 .await?
630 {
631 return Ok(device);
632 }
633
634 let device = device_info.open().map_err(crate::PtpError::Usb)?;
635 self.open_nusb_device(device).await
636 }
637
638 /// Open a device by its serial number.
639 ///
640 /// This identifies a specific physical device regardless of which USB port
641 /// it's connected to. Also checks the virtual device registry when the
642 /// `virtual-device` feature is enabled.
643 pub async fn open_by_serial(self, serial: &str) -> Result<MtpDevice, Error> {
644 #[cfg(feature = "virtual-device")]
645 if let Some(config) =
646 crate::transport::virtual_device::registry::find_virtual_config_by_serial(serial)
647 {
648 return self.open_virtual(config).await;
649 }
650
651 if let Some(device) = self.try_open_wpd_by_serial(serial).await? {
652 return Ok(device);
653 }
654
655 let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
656 let device_info = devices
657 .into_iter()
658 .find(|d| d.serial_number.as_deref() == Some(serial))
659 .ok_or(crate::PtpError::NoDevice)?;
660 let device = device_info.open().map_err(crate::PtpError::Usb)?;
661 self.open_nusb_device(device).await
662 }
663
664 /// Open an already-acquired [`nusb::Device`] as an MTP device.
665 ///
666 /// This is an escape hatch for consumers who already hold an `nusb::Device`
667 /// (e.g. from a custom enumeration or hotplug watcher). For most callers,
668 /// prefer [`known_devices`](Self::known_devices) combined with
669 /// `open_by_serial` / `open_by_location`.
670 ///
671 /// The interface scan is permissive: strict MTP-class match first, then
672 /// fallback to any interface with the MTP endpoint layout.
673 ///
674 /// # Example
675 ///
676 /// ```rust,no_run
677 /// use mtp_rs::mtp::MtpDevice;
678 /// use nusb::MaybeFuture;
679 ///
680 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
681 /// let nusb_device = nusb::list_devices()
682 /// .wait()?
683 /// .find(|d: &nusb::DeviceInfo| {
684 /// d.vendor_id() == 0x045E && d.product_id() == 0x0710
685 /// })
686 /// .ok_or(mtp_rs::Error::NoDevice)?
687 /// .open()
688 /// .wait()?;
689 ///
690 /// let device = MtpDevice::builder()
691 /// .open_nusb_device(nusb_device)
692 /// .await?;
693 /// # Ok(())
694 /// # }
695 /// ```
696 pub async fn open_nusb_device(self, device: nusb::Device) -> Result<MtpDevice, Error> {
697 let transport = NusbTransport::open_with_timeout(device, self.timeout).await?;
698 let transport: Arc<dyn Transport> = Arc::new(transport);
699
700 // Open session (use session ID 1)
701 let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
702
703 // Get device info
704 let device_info = session.get_device_info().await?;
705
706 // Quirk for Garmin devices
707 if device_info.manufacturer == "Garmin" {
708 session.set_split_header_data(true);
709 }
710
711 let backend = UsbBackend::new(session, device_info);
712 Ok(MtpDevice {
713 backend: Arc::new(backend),
714 })
715 }
716
717 /// Reset the USB transport of the device with this serial, without opening a
718 /// session. See [`MtpDevice::reset_by_serial`] for the full contract.
719 ///
720 /// **Last resort on Android**: the reset can break the phone's MTP function
721 /// until the user physically replugs. Try spaced reopens first.
722 pub async fn reset_by_serial(self, serial: &str) -> Result<(), Error> {
723 #[cfg(feature = "virtual-device")]
724 if crate::transport::virtual_device::registry::find_virtual_config_by_serial(serial)
725 .is_some()
726 {
727 return Err(Error::Unsupported);
728 }
729
730 let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
731 let device_info = devices
732 .into_iter()
733 .find(|d| d.serial_number.as_deref() == Some(serial))
734 .ok_or(crate::PtpError::NoDevice)?;
735 self.reset_usb_device(device_info).await
736 }
737
738 /// Reset the USB transport of the device at this location, without opening a
739 /// session. See [`MtpDevice::reset_by_serial`] for the full contract.
740 ///
741 /// **Last resort on Android**: the reset can break the phone's MTP function
742 /// until the user physically replugs. Try spaced reopens first.
743 pub async fn reset_by_location(self, location_id: u64) -> Result<(), Error> {
744 #[cfg(feature = "virtual-device")]
745 if crate::transport::virtual_device::registry::find_virtual_config_by_location(location_id)
746 .is_some()
747 {
748 return Err(Error::Unsupported);
749 }
750
751 let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
752 let device_info = devices
753 .into_iter()
754 .find(|d| d.location_id == location_id)
755 .ok_or(crate::PtpError::NoDevice)?;
756 self.reset_usb_device(device_info).await
757 }
758
759 /// Reset the USB transport of the first available device, without opening a
760 /// session. See [`MtpDevice::reset_by_serial`] for the full contract.
761 ///
762 /// **Last resort on Android**: the reset can break the phone's MTP function
763 /// until the user physically replugs. Try spaced reopens first.
764 pub async fn reset_first(self) -> Result<(), Error> {
765 let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
766 let device_info = devices
767 .into_iter()
768 .next()
769 .ok_or(crate::PtpError::NoDevice)?;
770 self.reset_usb_device(device_info).await
771 }
772
773 /// Claim the interface and send the transport reset. Deliberately does NOT
774 /// call [`PtpSession::open`] or `GetDeviceInfo`: claiming is all a wedged
775 /// device can still answer.
776 async fn reset_usb_device(
777 self,
778 device_info: crate::transport::UsbDeviceInfo,
779 ) -> Result<(), Error> {
780 let device = device_info.open().map_err(crate::PtpError::Usb)?;
781 let transport = NusbTransport::open_with_timeout(device, self.timeout).await?;
782 transport.reset_device().await?;
783 Ok(())
784 }
785
786 /// Open a virtual device backed by local filesystem directories.
787 ///
788 /// This creates a virtual MTP device that speaks the full binary protocol but
789 /// operates against local directories instead of USB. Use this for testing MTP
790 /// client code without real hardware.
791 ///
792 /// # Example
793 ///
794 /// ```rust,no_run
795 /// use std::path::PathBuf;
796 /// use mtp_rs::MtpDevice;
797 /// use mtp_rs::transport::virtual_device::config::{VirtualDeviceConfig, VirtualStorageConfig};
798 ///
799 /// # async fn example() -> Result<(), mtp_rs::Error> {
800 /// let device = MtpDevice::builder()
801 /// .open_virtual(VirtualDeviceConfig {
802 /// manufacturer: "Google".into(),
803 /// model: "Virtual Pixel 9".into(),
804 /// serial: "virtual-001".into(),
805 /// storages: vec![VirtualStorageConfig {
806 /// description: "Internal Storage".into(),
807 /// capacity: 64 * 1024 * 1024 * 1024,
808 /// backing_dir: PathBuf::from("/tmp/mtp-test"),
809 /// read_only: false,
810 /// }],
811 /// ..Default::default()
812 /// })
813 /// .await?;
814 /// # Ok(())
815 /// # }
816 /// ```
817 #[cfg(feature = "virtual-device")]
818 pub async fn open_virtual(
819 self,
820 config: crate::transport::virtual_device::config::VirtualDeviceConfig,
821 ) -> Result<MtpDevice, Error> {
822 if config.storages.is_empty() {
823 return Err(Error::invalid_data(
824 "VirtualDeviceConfig requires at least one storage",
825 ));
826 }
827
828 let transport = crate::transport::virtual_device::VirtualTransport::new(config);
829 let transport: Arc<dyn Transport> = Arc::new(transport);
830
831 // Open session (use session ID 1)
832 let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
833
834 // Get device info
835 let device_info = session.get_device_info().await?;
836
837 let backend = UsbBackend::new(session, device_info);
838 Ok(MtpDevice {
839 backend: Arc::new(backend),
840 })
841 }
842}
843
844impl Default for MtpDeviceBuilder {
845 fn default() -> Self {
846 Self::new()
847 }
848}
849
850#[cfg(test)]
851mod tests {
852 use super::*;
853
854 #[test]
855 fn list_devices_returns_ok() {
856 assert!(MtpDevice::list_devices().is_ok());
857 }
858
859 #[tokio::test]
860 async fn resetting_an_absent_device_reports_no_device() {
861 let err = MtpDevice::reset_by_serial("no-such-device-serial")
862 .await
863 .expect_err("no USB device has that serial");
864 assert!(matches!(err, Error::NoDevice), "got {err:?}");
865 }
866
867 #[cfg(feature = "virtual-device")]
868 #[tokio::test]
869 async fn resetting_a_virtual_device_says_it_has_no_transport_to_reset() {
870 let dir = tempfile::tempdir().unwrap();
871 let serial = "reset-virtual-serial";
872 let config = crate::VirtualDeviceConfig {
873 serial: serial.into(),
874 storages: vec![crate::VirtualStorageConfig {
875 description: "Internal Storage".into(),
876 capacity: 1024,
877 backing_dir: dir.path().to_path_buf(),
878 read_only: false,
879 }],
880 ..Default::default()
881 };
882 let info = crate::register_virtual_device(&config);
883
884 // A virtual device is a filesystem, not a USB link: there's no transport
885 // state to reset. Saying so beats a puzzling "no device found" in a
886 // consumer's test suite, which is where this will actually be hit.
887 let err = MtpDevice::reset_by_serial(serial)
888 .await
889 .expect_err("a virtual device has no USB transport");
890 assert!(matches!(err, Error::Unsupported), "got {err:?}");
891
892 crate::unregister_virtual_device(info.location_id);
893 }
894
895 #[test]
896 fn builder_timeout() {
897 // Default value
898 let builder = MtpDeviceBuilder::new();
899 assert_eq!(builder.timeout, NusbTransport::DEFAULT_TIMEOUT);
900
901 // Custom value
902 let custom = MtpDeviceBuilder::new().timeout(Duration::from_secs(45));
903 assert_eq!(custom.timeout, Duration::from_secs(45));
904 }
905
906 #[test]
907 fn device_info_display() {
908 let with_serial = MtpDeviceInfo {
909 vendor_id: 0x04e8,
910 product_id: 0x6860,
911 manufacturer: Some("Samsung".to_string()),
912 product: Some("Galaxy S24".to_string()),
913 serial_number: Some("ABC123".to_string()),
914 location_id: 0x00200000,
915 speed: None,
916 match_reason: crate::transport::MtpMatchReason::StandardClass,
917 };
918 let display = with_serial.display();
919 assert!(display.contains("Samsung") && display.contains("Galaxy S24"));
920 assert!(display.contains("ABC123") && display.contains("00200000"));
921
922 // Without serial
923 let no_serial = MtpDeviceInfo {
924 serial_number: None,
925 ..with_serial.clone()
926 };
927 assert!(!no_serial.display().contains("serial:"));
928
929 // Unknown manufacturer
930 let unknown = MtpDeviceInfo {
931 manufacturer: None,
932 product: None,
933 ..with_serial
934 };
935 assert!(unknown.display().contains("Unknown"));
936 }
937
938 #[cfg(feature = "virtual-device")]
939 #[tokio::test]
940 async fn open_virtual_empty_storages_rejected() {
941 use crate::transport::virtual_device::config::VirtualDeviceConfig;
942
943 let config = VirtualDeviceConfig {
944 serial: "empty-001".into(),
945 // The point of this test: an empty `storages` must be rejected.
946 storages: vec![],
947 ..Default::default()
948 };
949
950 let result = MtpDevice::builder().open_virtual(config).await;
951 match result {
952 Err(err) => assert!(
953 err.to_string().contains("at least one storage"),
954 "unexpected error: {}",
955 err
956 ),
957 Ok(_) => panic!("expected error for empty storages"),
958 }
959 }
960
961 #[tokio::test]
962 #[ignore] // Requires real MTP device
963 async fn real_device_operations() {
964 let device = MtpDevice::open_first().await.unwrap();
965 println!("Connected to: {}", device.device_info().model);
966 for storage in device.storages().await.unwrap() {
967 println!("Storage: {}", storage.info().description);
968 }
969 device.close().await.unwrap();
970 }
971}