Skip to main content

MtpDevice

Struct MtpDevice 

Source
pub struct MtpDevice { /* private fields */ }
Expand description

An MTP device connection.

This is the main entry point for interacting with MTP devices. Use MtpDevice::open_first() to connect to the first available device, or MtpDevice::builder() for more control.

The device is a thin façade over a backend-neutral implementation (the internal MtpBackend trait). Today the only backend is PTP-over-USB (which also drives the virtual and mock transports); a Windows WPD backend is planned. Consumers work against the neutral crate::mtp types throughout.

§Example

use mtp_rs::mtp::MtpDevice;

// Open the first MTP device
let device = MtpDevice::open_first().await?;

println!("Connected to: {} {}",
         device.device_info().manufacturer,
         device.device_info().model);

// Get storages
for storage in device.storages().await? {
    println!("Storage: {} ({} free)",
             storage.info().description,
             storage.info().free_space);
}

Implementations§

Source§

impl MtpDevice

Source

pub fn builder() -> MtpDeviceBuilder

Create a builder for configuring device options.

Source

pub async fn open_first() -> Result<Self, Error>

Open the first available MTP device with default settings.

Source

pub async fn open_by_location(location_id: u64) -> Result<Self, Error>

Open a device at a specific USB location (port) with default settings.

Use list_devices() to get available location IDs.

Source

pub async fn open_by_serial(serial: &str) -> Result<Self, Error>

Open a device by its serial number with default settings.

This identifies a specific physical device regardless of which USB port it’s connected to.

Source

pub async fn reset_by_serial(serial: &str) -> Result<(), Error>

Reset the USB transport state of the device with this serial, without opening a session.

Sends the USB Still Image Class Device Reset request (bRequest=0x66), clears halted bulk endpoints, and drains stale bulk data. This is the USB transport-level reset, not the in-session ResetDevice (0x1010) PTP operation: it works precisely when the device is too confused for PTP traffic, which is when you need it.

§Warning: on Android this can break MTP until the user replugs

Treat this as a last resort, not a recovery step. Sending the reset to a healthy Pixel 9 Pro XL permanently killed its MTP function: Android’s MtpServer lost its endpoint read (ECANCELED, then EPIPE) and never re-armed, while the USB device controller stayed configured. The phone kept enumerating and kept showing up in a device list, and answered nothing. Ten spaced reopens over ~100 s all timed out; only a physical unplug and replug brought it back (verified on a Pixel 9 Pro XL, macOS/nusb + adb logcat, 2026-07-21).

Android is the most common MTP device class, so reach for this only after spaced reopens have already failed, or on a device that’s already unreachable, where you can’t make things much worse. See docs/notes/android-wedges-and-the-reset-kill-switch.md.

§Why this isn’t a method on an open device

It only claims the USB interface and stops there. The regular opens run OpenSession + GetDeviceInfo, which is exactly what a wedged device can’t answer, so a reset hanging off an already-open MtpDevice would be useless in the case it exists for. Drop your device first: holding it keeps the interface claimed, and this call would then fail to claim it. You have to reopen afterwards regardless, since the PTP session is gone.

§Recovering a wedged device

After Error::DeviceReset, when an operation hangs and never returns (the Android signature: no error at all), or when every operation fails with “Transaction ID mismatch” / “expected Response container type”:

  1. Drop the MtpDevice (and any Storage handles).
  2. Wait a few seconds quiet, with no USB traffic at all.
  3. Reopen with idle-spaced retries, several of them.
  4. Only if every reopen failed, and knowing the Android warning above, call this and then repeat steps 2 and 3.

Step 3 is where consumers go wrong: don’t try once and give up, and don’t hammer close/open in a tight loop (that keeps the device busy and re-wedges it into a hard Timeout). Expect the early attempts to fail. A Pixel’s wedge cleared on a fresh open with no reset at all (verified on a Pixel 9 Pro XL, macOS/nusb, 2026-07-20). On a Galaxy S23 Ultra the observed sequence was reset, then a reopen returning Timeout, then one returning SessionAlreadyOpen, then success (verified on SM-S918B, macOS/nusb, 2026-07-20); the control without a reset was never run, so it’s unknown whether spaced reopens alone would have sufficed there.

§Errors

Error::NoDevice when no USB device has that serial, and Error::Unsupported for a virtual device, which is a filesystem with no USB transport to reset.

Source

pub async fn reset_by_location(location_id: u64) -> Result<(), Error>

Reset the USB transport state of the device at this location, without opening a session.

See reset_by_serial for the full contract and the recovery sequence to follow.

Last resort on Android: the reset can break the phone’s MTP function until the user physically replugs. Try spaced reopens first.

Source

pub async fn reset_first() -> Result<(), Error>

Reset the USB transport state of the first available device, without opening a session.

See reset_by_serial for the full contract and the recovery sequence to follow.

Last resort on Android: the reset can break the phone’s MTP function until the user physically replugs. Try spaced reopens first.

Source

pub fn list_devices() -> Result<Vec<MtpDeviceInfo>, Error>

List all available MTP devices without opening them.

Source

pub fn list_devices_with_known( known: &[(u16, u16)], ) -> Result<Vec<MtpDeviceInfo>, Error>

List all available MTP devices, including additional devices identified by the given VID/PID pairs.

Devices matching the provided VID/PID pairs are included in the results even if their USB descriptors don’t match standard MTP class codes. This is useful for legacy or otherwise unusual devices with non-standard USB descriptors that still speak MTP.

§Example
use mtp_rs::mtp::MtpDevice;

let devices = MtpDevice::list_devices_with_known(&[
    (0x045E, 0x0710), // custom VID/PID
])?;
for d in &devices {
    println!("{:04x}:{:04x} {}", d.vendor_id, d.product_id,
             d.product.as_deref().unwrap_or("unknown"));
}
Source

pub fn device_info(&self) -> &DeviceInfo

Get device information (backend-neutral identity).

Source

pub fn capabilities(&self) -> &Capabilities

What this device supports (backend-neutral capabilities).

Replaces the old per-operation accessors. Advertised support can still be wrong on some devices (see the Fujifilm quirk in AGENTS.md), so treat these as a strong hint.

Source

pub fn supports_rename(&self) -> bool

Whether the device supports renaming objects.

Convenience over capabilities().can_rename.

Source

pub fn supports_upload(&self) -> bool

Whether the device supports creating objects (uploads and folders).

Convenience over capabilities().can_upload.

Source

pub async fn storages(&self) -> Result<Vec<Storage>, Error>

Get all storages on the device.

Source

pub async fn storage(&self, id: StorageId) -> Result<Storage, Error>

Get a specific storage by ID.

Source

pub async fn next_event(&self) -> Result<DeviceEvent, Error>

Receive the next event from the device.

This method awaits indefinitely on the underlying event channel until an event arrives or the device disconnects. Always wrap this in tokio::time::timeout (or equivalent) so you can check for shutdown.

§Concurrency

On the USB backend, event reading uses the USB interrupt endpoint, which is independent from the bulk endpoints used by file operations, so it is safe to call next_event() concurrently with other MtpDevice methods.

If you wrap MtpDevice in a shared lock (for example, Arc<Mutex<MtpDevice>>), do not hold that lock while awaiting next_event(): it will block all file operations for the duration of the wait. Instead, clone the MtpDevice (it is cheaply cloneable via Arc internally) and call next_event() on the clone without holding the lock.

§Returns
  • Ok(event) - An event was received from the device
  • Err(Error::Disconnected) - Device was disconnected
  • Err(_) - Other communication error
§Example
use mtp_rs::mtp::{MtpDevice, DeviceEvent};
use mtp_rs::Error;
use tokio::time::{timeout, Duration};

loop {
    match timeout(Duration::from_millis(200), device.next_event()).await {
        Ok(Ok(event)) => {
            match event {
                DeviceEvent::ObjectAdded { handle } => {
                    println!("New object: {:?}", handle);
                }
                DeviceEvent::StoreRemoved { storage_id } => {
                    println!("Storage removed: {:?}", storage_id);
                }
                _ => {}
            }
        }
        Ok(Err(Error::Disconnected)) => break,
        Ok(Err(e)) => {
            eprintln!("Error: {}", e);
            break;
        }
        Err(_elapsed) => continue,  // Timeout, check for shutdown etc.
    }
}
Source

pub async fn close(self) -> Result<(), Error>

Close the connection (best-effort; also happens on drop).

Trait Implementations§

Source§

impl Clone for MtpDevice

Source§

fn clone(&self) -> MtpDevice

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more