use crate::mtp::backend::usb::UsbBackend;
use crate::mtp::backend::{Backend, MtpBackend};
use crate::mtp::{Capabilities, DeviceEvent, DeviceInfo, Error, Storage, StorageId};
use crate::ptp::PtpSession;
use crate::transport::{NusbTransport, Transport};
use std::sync::Arc;
use std::time::Duration;
#[derive(Clone)]
pub struct MtpDevice {
pub(crate) backend: Arc<dyn MtpBackend>,
}
impl MtpDevice {
pub fn builder() -> MtpDeviceBuilder {
MtpDeviceBuilder::new()
}
pub async fn open_first() -> Result<Self, Error> {
Self::builder().open_first().await
}
pub async fn open_by_location(location_id: u64) -> Result<Self, Error> {
Self::builder().open_by_location(location_id).await
}
pub async fn open_by_serial(serial: &str) -> Result<Self, Error> {
Self::builder().open_by_serial(serial).await
}
pub fn list_devices() -> Result<Vec<MtpDeviceInfo>, Error> {
Self::list_devices_with_known(&[])
}
pub fn list_devices_with_known(known: &[(u16, u16)]) -> Result<Vec<MtpDeviceInfo>, Error> {
let devices = NusbTransport::list_mtp_devices_with_known(known)?;
#[allow(unused_mut)]
let mut result: Vec<MtpDeviceInfo> = devices
.into_iter()
.map(|d| MtpDeviceInfo {
vendor_id: d.vendor_id,
product_id: d.product_id,
manufacturer: d.manufacturer,
product: d.product,
serial_number: d.serial_number,
location_id: d.location_id,
speed: d.speed,
match_reason: d.match_reason,
})
.collect();
#[cfg(feature = "virtual-device")]
result.extend(crate::transport::virtual_device::registry::list_virtual_devices());
Ok(result)
}
#[must_use]
pub fn device_info(&self) -> &DeviceInfo {
self.backend.device_info()
}
#[must_use]
pub fn capabilities(&self) -> &Capabilities {
self.backend.capabilities()
}
#[must_use]
pub fn supports_rename(&self) -> bool {
self.backend.capabilities().can_rename
}
#[must_use]
pub fn supports_upload(&self) -> bool {
self.backend.capabilities().can_upload
}
pub async fn storages(&self) -> Result<Vec<Storage>, Error> {
let infos = self.backend.storages().await?;
Ok(infos
.into_iter()
.map(|info| Storage::new(Arc::clone(&self.backend), info.id, info))
.collect())
}
pub async fn storage(&self, id: StorageId) -> Result<Storage, Error> {
let info = self.backend.storage_info(id).await?;
Ok(Storage::new(Arc::clone(&self.backend), id, info))
}
pub async fn next_event(&self) -> Result<DeviceEvent, Error> {
self.backend.next_event().await
}
pub async fn close(self) -> Result<(), Error> {
self.backend.close().await
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct MtpDeviceInfo {
pub vendor_id: u16,
pub product_id: u16,
pub manufacturer: Option<String>,
pub product: Option<String>,
pub serial_number: Option<String>,
pub location_id: u64,
pub speed: Option<crate::transport::UsbSpeed>,
pub match_reason: crate::transport::MtpMatchReason,
}
impl MtpDeviceInfo {
#[must_use]
pub fn display(&self) -> String {
let manufacturer = self.manufacturer.as_deref().unwrap_or("Unknown");
let product = self.product.as_deref().unwrap_or("Unknown");
match &self.serial_number {
Some(serial) => format!(
"{} {} (serial: {}, location: {:08x})",
manufacturer, product, serial, self.location_id
),
None => format!(
"{} {} (location: {:08x})",
manufacturer, product, self.location_id
),
}
}
}
pub struct MtpDeviceBuilder {
timeout: Duration,
known_devices: Vec<(u16, u16)>,
backend: Backend,
}
impl MtpDeviceBuilder {
#[must_use]
pub fn new() -> Self {
Self {
timeout: NusbTransport::DEFAULT_TIMEOUT,
known_devices: Vec::new(),
backend: Backend::default(),
}
}
#[must_use]
pub fn backend(mut self, backend: Backend) -> Self {
self.backend = backend;
self
}
async fn try_open_wpd_first(&self) -> Result<Option<MtpDevice>, Error> {
#[cfg(windows)]
if matches!(self.backend, Backend::Auto | Backend::Wpd) {
match crate::mtp::backend::wpd::WpdBackend::open_first().await {
Ok(b) => {
return Ok(Some(MtpDevice {
backend: Arc::new(b),
}))
}
Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
return Err(e)
}
Err(_) => {} }
}
#[cfg(not(windows))]
if self.backend == Backend::Wpd {
return Err(Error::Unsupported);
}
Ok(None)
}
async fn try_open_wpd_by_serial(&self, serial: &str) -> Result<Option<MtpDevice>, Error> {
#[cfg(windows)]
if matches!(self.backend, Backend::Auto | Backend::Wpd) {
match crate::mtp::backend::wpd::WpdBackend::open_by_serial(serial).await {
Ok(b) => {
return Ok(Some(MtpDevice {
backend: Arc::new(b),
}))
}
Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
return Err(e)
}
Err(_) => {}
}
}
#[cfg(not(windows))]
{
let _ = serial;
if self.backend == Backend::Wpd {
return Err(Error::Unsupported);
}
}
Ok(None)
}
async fn try_open_wpd_for_usb(
&self,
serial: Option<String>,
vid: u16,
pid: u16,
) -> Result<Option<MtpDevice>, Error> {
#[cfg(windows)]
if matches!(self.backend, Backend::Auto | Backend::Wpd) {
match crate::mtp::backend::wpd::WpdBackend::open_for_usb(serial.clone(), vid, pid).await
{
Ok(b) => {
return Ok(Some(MtpDevice {
backend: Arc::new(b),
}))
}
Err(e) if self.backend == Backend::Wpd || !matches!(e, Error::NoDevice) => {
return Err(e)
}
Err(_) => {}
}
}
#[cfg(not(windows))]
{
let _ = (serial, vid, pid);
if self.backend == Backend::Wpd {
return Err(Error::Unsupported);
}
}
Ok(None)
}
#[must_use]
pub fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
#[must_use]
pub fn known_devices(mut self, known: &[(u16, u16)]) -> Self {
self.known_devices = known.to_vec();
self
}
pub async fn open_first(self) -> Result<MtpDevice, Error> {
if let Some(device) = self.try_open_wpd_first().await? {
return Ok(device);
}
let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
let device_info = devices
.into_iter()
.next()
.ok_or(crate::PtpError::NoDevice)?;
let device = device_info.open().map_err(crate::PtpError::Usb)?;
self.open_nusb_device(device).await
}
pub async fn open_by_location(self, location_id: u64) -> Result<MtpDevice, Error> {
#[cfg(feature = "virtual-device")]
if let Some(config) =
crate::transport::virtual_device::registry::find_virtual_config_by_location(location_id)
{
return self.open_virtual(config).await;
}
let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
let device_info = devices
.into_iter()
.find(|d| d.location_id == location_id)
.ok_or(crate::PtpError::NoDevice)?;
if let Some(device) = self
.try_open_wpd_for_usb(
device_info.serial_number.clone(),
device_info.vendor_id,
device_info.product_id,
)
.await?
{
return Ok(device);
}
let device = device_info.open().map_err(crate::PtpError::Usb)?;
self.open_nusb_device(device).await
}
pub async fn open_by_serial(self, serial: &str) -> Result<MtpDevice, Error> {
#[cfg(feature = "virtual-device")]
if let Some(config) =
crate::transport::virtual_device::registry::find_virtual_config_by_serial(serial)
{
return self.open_virtual(config).await;
}
if let Some(device) = self.try_open_wpd_by_serial(serial).await? {
return Ok(device);
}
let devices = NusbTransport::list_mtp_devices_with_known(&self.known_devices)?;
let device_info = devices
.into_iter()
.find(|d| d.serial_number.as_deref() == Some(serial))
.ok_or(crate::PtpError::NoDevice)?;
let device = device_info.open().map_err(crate::PtpError::Usb)?;
self.open_nusb_device(device).await
}
pub async fn open_nusb_device(self, device: nusb::Device) -> Result<MtpDevice, Error> {
let transport = NusbTransport::open_with_timeout(device, self.timeout).await?;
let transport: Arc<dyn Transport> = Arc::new(transport);
let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
let device_info = session.get_device_info().await?;
if device_info.manufacturer == "Garmin" {
session.set_split_header_data(true);
}
let backend = UsbBackend::new(session, device_info);
Ok(MtpDevice {
backend: Arc::new(backend),
})
}
#[cfg(feature = "virtual-device")]
pub async fn open_virtual(
self,
config: crate::transport::virtual_device::config::VirtualDeviceConfig,
) -> Result<MtpDevice, Error> {
if config.storages.is_empty() {
return Err(Error::invalid_data(
"VirtualDeviceConfig requires at least one storage",
));
}
let transport = crate::transport::virtual_device::VirtualTransport::new(config);
let transport: Arc<dyn Transport> = Arc::new(transport);
let session = Arc::new(PtpSession::open(transport.clone(), 1).await?);
let device_info = session.get_device_info().await?;
let backend = UsbBackend::new(session, device_info);
Ok(MtpDevice {
backend: Arc::new(backend),
})
}
}
impl Default for MtpDeviceBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn list_devices_returns_ok() {
assert!(MtpDevice::list_devices().is_ok());
}
#[test]
fn builder_timeout() {
let builder = MtpDeviceBuilder::new();
assert_eq!(builder.timeout, NusbTransport::DEFAULT_TIMEOUT);
let custom = MtpDeviceBuilder::new().timeout(Duration::from_secs(45));
assert_eq!(custom.timeout, Duration::from_secs(45));
}
#[test]
fn device_info_display() {
let with_serial = MtpDeviceInfo {
vendor_id: 0x04e8,
product_id: 0x6860,
manufacturer: Some("Samsung".to_string()),
product: Some("Galaxy S24".to_string()),
serial_number: Some("ABC123".to_string()),
location_id: 0x00200000,
speed: None,
match_reason: crate::transport::MtpMatchReason::StandardClass,
};
let display = with_serial.display();
assert!(display.contains("Samsung") && display.contains("Galaxy S24"));
assert!(display.contains("ABC123") && display.contains("00200000"));
let no_serial = MtpDeviceInfo {
serial_number: None,
..with_serial.clone()
};
assert!(!no_serial.display().contains("serial:"));
let unknown = MtpDeviceInfo {
manufacturer: None,
product: None,
..with_serial
};
assert!(unknown.display().contains("Unknown"));
}
#[cfg(feature = "virtual-device")]
#[tokio::test]
async fn open_virtual_empty_storages_rejected() {
use crate::transport::virtual_device::config::VirtualDeviceConfig;
let config = VirtualDeviceConfig {
manufacturer: "TestCorp".into(),
model: "Empty".into(),
serial: "empty-001".into(),
storages: vec![],
supports_rename: false,
event_poll_interval: Duration::ZERO,
watch_backing_dirs: false,
};
let result = MtpDevice::builder().open_virtual(config).await;
match result {
Err(err) => assert!(
err.to_string().contains("at least one storage"),
"unexpected error: {}",
err
),
Ok(_) => panic!("expected error for empty storages"),
}
}
#[tokio::test]
#[ignore] async fn real_device_operations() {
let device = MtpDevice::open_first().await.unwrap();
println!("Connected to: {}", device.device_info().model);
for storage in device.storages().await.unwrap() {
println!("Storage: {}", storage.info().description);
}
device.close().await.unwrap();
}
}