Skip to main content

mtp_rs/ptp/
device.rs

1//! Low-level PTP device API.
2
3use crate::ptp::{
4    container_type, CommandContainer, ContainerType, DataContainer, DeviceInfo, OperationCode,
5    PtpSession, ResponseCode, ResponseContainer,
6};
7use crate::transport::{NusbTransport, Transport};
8use crate::Error;
9use std::sync::Arc;
10use std::time::Duration;
11
12/// A low-level PTP device connection.
13///
14/// Use this for camera support or when you need raw PTP operations.
15/// For typical MTP usage with Android devices, prefer `MtpDevice` instead.
16pub struct PtpDevice {
17    transport: Arc<NusbTransport>,
18}
19
20impl PtpDevice {
21    /// Open a PTP device at a specific USB location (port).
22    pub async fn open_by_location(location_id: u64) -> Result<Self, Error> {
23        Self::open_by_location_with_timeout(location_id, NusbTransport::DEFAULT_TIMEOUT).await
24    }
25
26    /// Open by location with custom timeout.
27    pub async fn open_by_location_with_timeout(
28        location_id: u64,
29        timeout: Duration,
30    ) -> Result<Self, Error> {
31        let devices = NusbTransport::list_mtp_devices()?;
32        let device_info = devices
33            .into_iter()
34            .find(|d| d.location_id == location_id)
35            .ok_or(Error::NoDevice)?;
36        Self::open_device(device_info, timeout).await
37    }
38
39    /// Open a PTP device by its serial number.
40    pub async fn open_by_serial(serial: &str) -> Result<Self, Error> {
41        Self::open_by_serial_with_timeout(serial, NusbTransport::DEFAULT_TIMEOUT).await
42    }
43
44    /// Open by serial with custom timeout.
45    pub async fn open_by_serial_with_timeout(
46        serial: &str,
47        timeout: Duration,
48    ) -> Result<Self, Error> {
49        let devices = NusbTransport::list_mtp_devices()?;
50        let device_info = devices
51            .into_iter()
52            .find(|d| d.serial_number.as_deref() == Some(serial))
53            .ok_or(Error::NoDevice)?;
54        Self::open_device(device_info, timeout).await
55    }
56
57    /// Open the first available PTP device.
58    pub async fn open_first() -> Result<Self, Error> {
59        Self::open_first_with_timeout(NusbTransport::DEFAULT_TIMEOUT).await
60    }
61
62    /// Open the first available device with custom timeout.
63    pub async fn open_first_with_timeout(timeout: Duration) -> Result<Self, Error> {
64        let devices = NusbTransport::list_mtp_devices()?;
65        let device_info = devices.into_iter().next().ok_or(Error::NoDevice)?;
66        Self::open_device(device_info, timeout).await
67    }
68
69    async fn open_device(
70        device_info: crate::transport::UsbDeviceInfo,
71        timeout: Duration,
72    ) -> Result<Self, Error> {
73        let device = device_info.open().map_err(Error::Usb)?;
74        let transport = NusbTransport::open_with_timeout(device, timeout).await?;
75        Ok(Self {
76            transport: Arc::new(transport),
77        })
78    }
79
80    /// Get device info without opening a session.
81    ///
82    /// This is the only operation that can be performed without a session.
83    pub async fn get_device_info(&self) -> Result<DeviceInfo, Error> {
84        // Build GetDeviceInfo command (transaction ID 0 for session-less)
85        let cmd = CommandContainer {
86            code: OperationCode::GetDeviceInfo,
87            transaction_id: 0,
88            params: vec![],
89        };
90        self.transport.send_bulk(&cmd.to_bytes()).await?;
91
92        // Receive data
93        let mut data_payload = Vec::new();
94        loop {
95            let bytes = self.transport.receive_bulk(64 * 1024).await?;
96            if bytes.is_empty() {
97                return Err(Error::invalid_data("Empty response"));
98            }
99
100            let ct = container_type(&bytes)?;
101            match ct {
102                ContainerType::Data => {
103                    let container = DataContainer::from_bytes(&bytes)?;
104                    data_payload.extend_from_slice(&container.payload);
105                }
106                ContainerType::Response => {
107                    let response = ResponseContainer::from_bytes(&bytes)?;
108                    if response.code != ResponseCode::Ok {
109                        return Err(Error::Protocol {
110                            code: response.code,
111                            operation: OperationCode::GetDeviceInfo,
112                        });
113                    }
114                    break;
115                }
116                _ => {
117                    return Err(Error::invalid_data(format!(
118                        "Unexpected container type: {:?}",
119                        ct
120                    )));
121                }
122            }
123        }
124
125        DeviceInfo::from_bytes(&data_payload)
126    }
127
128    /// Open a PTP session.
129    ///
130    /// Most operations require a session to be open first.
131    pub async fn open_session(&self) -> Result<PtpSession, Error> {
132        self.open_session_with_id(1).await
133    }
134
135    /// Open a session with a specific session ID.
136    pub async fn open_session_with_id(&self, session_id: u32) -> Result<PtpSession, Error> {
137        let transport: Arc<dyn Transport> = self.transport.clone();
138        PtpSession::open(transport, session_id).await
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145
146    #[tokio::test]
147    #[ignore] // Requires real device
148    async fn test_open_first() {
149        let device = PtpDevice::open_first().await.unwrap();
150        let info = device.get_device_info().await.unwrap();
151        println!("Model: {}", info.model);
152    }
153
154    #[tokio::test]
155    #[ignore] // Requires real device
156    async fn test_open_session() {
157        let device = PtpDevice::open_first().await.unwrap();
158        let session = device.open_session().await.unwrap();
159
160        let info = session.get_device_info().await.unwrap();
161        println!("Model: {}", info.model);
162
163        session.close().await.unwrap();
164    }
165}