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
impl MtpDevice
Sourcepub fn builder() -> MtpDeviceBuilder
pub fn builder() -> MtpDeviceBuilder
Create a builder for configuring device options.
Sourcepub async fn open_first() -> Result<Self, Error>
pub async fn open_first() -> Result<Self, Error>
Open the first available MTP device with default settings.
Sourcepub async fn open_by_location(location_id: u64) -> Result<Self, Error>
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.
Sourcepub async fn open_by_serial(serial: &str) -> Result<Self, Error>
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.
Sourcepub async fn reset_by_serial(serial: &str) -> Result<(), Error>
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”:
- Drop the
MtpDevice(and anyStoragehandles). - Wait a few seconds quiet, with no USB traffic at all.
- Reopen with idle-spaced retries, several of them.
- 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.
Sourcepub async fn reset_by_location(location_id: u64) -> Result<(), Error>
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.
Sourcepub async fn reset_first() -> Result<(), Error>
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.
Sourcepub fn list_devices() -> Result<Vec<MtpDeviceInfo>, Error>
pub fn list_devices() -> Result<Vec<MtpDeviceInfo>, Error>
List all available MTP devices without opening them.
Sourcepub fn list_devices_with_known(
known: &[(u16, u16)],
) -> Result<Vec<MtpDeviceInfo>, Error>
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"));
}Sourcepub fn device_info(&self) -> &DeviceInfo
pub fn device_info(&self) -> &DeviceInfo
Get device information (backend-neutral identity).
Sourcepub fn capabilities(&self) -> &Capabilities
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.
Sourcepub fn supports_rename(&self) -> bool
pub fn supports_rename(&self) -> bool
Whether the device supports renaming objects.
Convenience over capabilities().can_rename.
Sourcepub fn supports_upload(&self) -> bool
pub fn supports_upload(&self) -> bool
Whether the device supports creating objects (uploads and folders).
Convenience over capabilities().can_upload.
Sourcepub async fn storage(&self, id: StorageId) -> Result<Storage, Error>
pub async fn storage(&self, id: StorageId) -> Result<Storage, Error>
Get a specific storage by ID.
Sourcepub async fn next_event(&self) -> Result<DeviceEvent, Error>
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 deviceErr(Error::Disconnected)- Device was disconnectedErr(_)- 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.
}
}