From 1f26a91dcd5d6416a33e7e222929b966db74aa71 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Wed, 15 Oct 2025 23:35:35 +0100 Subject: [PATCH] feat: custom PTP implementation Signed-off-by: Nikolaos Karaolidis --- Cargo.lock | 12 - Cargo.toml | 1 - src/camera/{devices.rs => devices/mod.rs} | 0 src/camera/error.rs | 13 + src/camera/mod.rs | 382 ++++++++++++++++------ src/camera/ptp/enums.rs | 173 ++++++++++ src/camera/ptp/error.rs | 53 +++ src/camera/ptp/mod.rs | 4 + src/camera/ptp/read.rs | 127 +++++++ src/camera/ptp/structs.rs | 77 +++++ src/cli/common/film.rs | 23 -- src/cli/device/mod.rs | 10 +- src/cli/mod.rs | 12 +- src/cli/render/mod.rs | 4 +- src/cli/simulation/mod.rs | 8 +- src/log.rs | 13 +- src/main.rs | 4 +- 17 files changed, 743 insertions(+), 173 deletions(-) rename src/camera/{devices.rs => devices/mod.rs} (100%) create mode 100644 src/camera/error.rs create mode 100644 src/camera/ptp/enums.rs create mode 100644 src/camera/ptp/error.rs create mode 100644 src/camera/ptp/mod.rs create mode 100644 src/camera/ptp/read.rs create mode 100644 src/camera/ptp/structs.rs diff --git a/Cargo.lock b/Cargo.lock index e031942..39d96c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,7 +239,6 @@ dependencies = [ "anyhow", "byteorder", "clap", - "libptp", "log", "log4rs", "rusb", @@ -339,17 +338,6 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" -[[package]] -name = "libptp" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e6b84822d9579c3adb36bcea61c396dc2596a95ca03a0ffd69636fc85ccc4e2" -dependencies = [ - "byteorder", - "log", - "rusb", -] - [[package]] name = "libusb1-sys" version = "0.7.0" diff --git a/Cargo.toml b/Cargo.toml index b8e8bd3..58ae572 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,6 @@ codegen-units = 1 anyhow = "1.0.100" byteorder = "1.5.0" clap = { version = "4.5.48", features = ["derive", "wrap_help"] } -libptp = "0.6.5" log = "0.4.28" log4rs = "1.4.0" rusb = "0.9.4" diff --git a/src/camera/devices.rs b/src/camera/devices/mod.rs similarity index 100% rename from src/camera/devices.rs rename to src/camera/devices/mod.rs diff --git a/src/camera/error.rs b/src/camera/error.rs new file mode 100644 index 0000000..f55ae32 --- /dev/null +++ b/src/camera/error.rs @@ -0,0 +1,13 @@ +use std::{error::Error, fmt}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct UnsupportedFeatureError; + +impl fmt::Display for UnsupportedFeatureError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "feature is not supported for this device") + } +} + +impl Error for UnsupportedFeatureError {} diff --git a/src/camera/mod.rs b/src/camera/mod.rs index b9a5759..1f1b81c 100644 --- a/src/camera/mod.rs +++ b/src/camera/mod.rs @@ -1,33 +1,38 @@ pub mod devices; +pub mod error; +pub mod ptp; -use std::{error::Error, fmt, io::Cursor, time::Duration}; +use std::{cmp::min, io::Cursor, time::Duration}; use anyhow::{anyhow, bail}; use byteorder::{LittleEndian, WriteBytesExt}; use devices::SupportedCamera; -use libptp::{DeviceInfo, StandardCommandCode}; -use log::{debug, error}; -use rusb::GlobalContext; -use serde::Serialize; - -#[derive(Debug)] -pub struct UnsupportedFeatureError; - -impl fmt::Display for UnsupportedFeatureError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "feature is not supported for this device") - } -} - -impl Error for UnsupportedFeatureError {} +use log::{debug, error, trace}; +use ptp::{ + enums::{CommandCode, ContainerType, PropCode, ResponseCode, UsbMode}, + structs::{ContainerInfo, DeviceInfo}, +}; +use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE}; const SESSION: u32 = 1; -pub struct Camera { +pub struct Usb { bus: u8, address: u8, - ptp: libptp::Camera, - r#impl: Box>, + interface: u8, +} + +pub struct Ptp { + bulk_in: u8, + bulk_out: u8, + handle: rusb::DeviceHandle, + transaction_id: u32, +} + +pub struct Camera { + pub r#impl: Box>, + usb: Usb, + pub ptp: Ptp, } impl Camera { @@ -36,24 +41,69 @@ impl Camera { if let Ok(r#impl) = supported_camera.new_camera(device) { let bus = device.bus_number(); let address = device.address(); - let mut ptp = libptp::Camera::new(device)?; + + let config_desc = device.active_config_descriptor()?; + + let interface_descriptor = config_desc + .interfaces() + .flat_map(|i| i.descriptors()) + .find(|x| x.class_code() == LIBUSB_CLASS_IMAGE) + .ok_or(rusb::Error::NotFound)?; + + let interface = interface_descriptor.interface_number(); + debug!("Found interface {interface}"); + + let usb = Usb { + bus, + address, + interface, + }; + + let handle = device.open()?; + handle.claim_interface(interface)?; + + let bulk_in = Self::find_endpoint( + &interface_descriptor, + rusb::Direction::In, + rusb::TransferType::Bulk, + )?; + let bulk_out = Self::find_endpoint( + &interface_descriptor, + rusb::Direction::Out, + rusb::TransferType::Bulk, + )?; + let transaction_id = 0; + + let mut ptp = Ptp { + bulk_in, + bulk_out, + handle, + transaction_id, + }; debug!("Opening session"); let () = r#impl.open_session(&mut ptp, SESSION)?; debug!("Session opened"); - return Ok(Self { - bus, - address, - ptp, - r#impl, - }); + return Ok(Self { r#impl, usb, ptp }); } } bail!("Device not supported"); } + fn find_endpoint( + interface_descriptor: &rusb::InterfaceDescriptor<'_>, + direction: rusb::Direction, + transfer_type: rusb::TransferType, + ) -> Result { + interface_descriptor + .endpoint_descriptors() + .find(|ep| ep.direction() == direction && ep.transfer_type() == transfer_type) + .map(|x| x.address()) + .ok_or(rusb::Error::NotFound) + } + pub fn name(&self) -> &'static str { self.r#impl.supported_camera().name } @@ -67,7 +117,7 @@ impl Camera { } pub fn connected_usb_id(&self) -> String { - format!("{}.{}", self.bus, self.address) + format!("{}.{}", self.usb.bus, self.usb.address) } fn prop_value_as_scalar(data: &[u8]) -> anyhow::Result { @@ -89,7 +139,7 @@ impl Camera { pub fn get_usb_mode(&mut self) -> anyhow::Result { let data = self .r#impl - .get_prop_value(&mut self.ptp, DevicePropCode::FujiUsbMode); + .get_prop_value(&mut self.ptp, PropCode::FujiUsbMode); let result = Self::prop_value_as_scalar(&data?)?.into(); Ok(result) @@ -98,7 +148,7 @@ impl Camera { pub fn get_battery_info(&mut self) -> anyhow::Result { let data = self .r#impl - .get_prop_value(&mut self.ptp, DevicePropCode::FujiBatteryInfo2); + .get_prop_value(&mut self.ptp, PropCode::FujiBatteryInfo2); let data = data?; debug!("Raw battery data: {data:?}"); @@ -134,41 +184,15 @@ impl Drop for Camera { fn drop(&mut self) { debug!("Closing session"); if let Err(e) = self.r#impl.close_session(&mut self.ptp, SESSION) { - error!("Error closing session: {e}") + error!("Error closing session: {e}"); } debug!("Session closed"); - } -} -#[repr(u32)] -#[derive(Debug, Clone, Copy)] -pub enum DevicePropCode { - FujiUsbMode = 0xd16e, - FujiBatteryInfo2 = 0xD36B, -} - -#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] -pub enum UsbMode { - RawConversion, - Unsupported, -} - -impl From for UsbMode { - fn from(val: u32) -> Self { - match val { - 6 => Self::RawConversion, - _ => Self::Unsupported, + debug!("Releasing interface"); + if let Err(e) = self.ptp.handle.release_interface(self.usb.interface) { + error!("Error releasing interface: {e}"); } - } -} - -impl fmt::Display for UsbMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Self::RawConversion => "USB RAW CONV./BACKUP RESTORE", - Self::Unsupported => "Unsupported USB Mode", - }; - write!(f, "{s}") + debug!("Interface released"); } } @@ -179,81 +203,219 @@ pub trait CameraImpl { None } - fn open_session(&self, ptp: &mut libptp::Camera

, session_id: u32) -> anyhow::Result<()> { + fn chunk_size(&self) -> usize { + 1024 * 1024 + } + + fn send( + &self, + ptp: &mut Ptp, + code: CommandCode, + params: Option<&[u32]>, + data: Option<&[u8]>, + transaction: bool, + ) -> anyhow::Result> { + let transaction_id = if transaction { + Some(ptp.transaction_id) + } else { + None + }; + + let params = params.unwrap_or_default(); + + let mut payload = Vec::with_capacity(params.len() * 4); + for p in params { + payload.write_u32::(*p).ok(); + } + + trace!( + "Sending PTP command: {:?}, transaction: {:?}, parameters ({} bytes): {:x?}", + code, + transaction_id, + payload.len(), + payload, + ); + self.write(ptp, ContainerType::Command, code, &payload, transaction_id)?; + + if let Some(data) = data { + trace!("Sending PTP data: {} bytes", data.len()); + self.write(ptp, ContainerType::Data, code, data, transaction_id)?; + } + + let mut data_payload = Vec::new(); + loop { + let (container, payload) = self.read(ptp)?; + match container.kind { + ContainerType::Data => { + trace!("Data received: {} bytes", payload.len()); + data_payload = payload; + } + ContainerType::Response => { + trace!("Response received: code {:?}", container.code); + let code = ResponseCode::try_from(container.code)?; + if code != ResponseCode::Ok { + bail!(ptp::error::Error::Response(container.code)); + } + trace!( + "Command {:?} completed successfully with data payload of {} bytes", + code, + data_payload.len(), + ); + return Ok(data_payload); + } + _ => { + debug!("Ignoring unexpected container type: {:?}", container.kind); + } + } + } + } + + fn write( + &self, + ptp: &mut Ptp, + kind: ContainerType, + code: CommandCode, + payload: &[u8], + // Fuji, for the love of God don't ever write code again. + transaction_id: Option, + ) -> anyhow::Result<()> { + // Look at what you made me do. Fuck. + let header_len = ContainerInfo::SIZE + - if transaction_id.is_none() { + size_of::() + } else { + 0 + }; + + let first_chunk_len = min(payload.len(), self.chunk_size() - header_len); + let total_len = u32::try_from(payload.len() + header_len)?; + + let mut buffer = Vec::with_capacity(first_chunk_len + header_len); + buffer.write_u32::(total_len)?; + buffer.write_u16::(kind as u16)?; + buffer.write_u16::(code as u16)?; + if let Some(transaction_id) = transaction_id { + buffer.write_u32::(transaction_id)?; + } + + buffer.extend_from_slice(&payload[..first_chunk_len]); + + trace!( + "Writing PTP {kind:?} container, code: {code:?}, transaction: {transaction_id:?}, first_chunk: {first_chunk_len} bytes", + ); + + let timeout = self.timeout().unwrap_or_default(); + ptp.handle.write_bulk(ptp.bulk_out, &buffer, timeout)?; + + for chunk in payload[first_chunk_len..].chunks(self.chunk_size()) { + trace!("Writing additional chunk ({} bytes)", chunk.len(),); + ptp.handle.write_bulk(ptp.bulk_out, chunk, timeout)?; + } + + trace!( + "Write completed for code {:?}, total payload of {} bytes", + code, + payload.len() + ); + Ok(()) + } + + fn read(&self, ptp: &mut Ptp) -> anyhow::Result<(ContainerInfo, Vec)> { + let timeout = self.timeout().unwrap_or_default(); + + let mut stack_buf = [0u8; 8 * 1024]; + let n = ptp.handle.read_bulk(ptp.bulk_in, &mut stack_buf, timeout)?; + let buf = &stack_buf[..n]; + + trace!("Read {n} bytes from bulk_in"); + + let container_info = ContainerInfo::parse(buf)?; + if container_info.payload_len == 0 { + trace!("No payload in container"); + return Ok((container_info, Vec::new())); + } + + let payload_len = container_info.payload_len as usize; + let mut payload = Vec::with_capacity(payload_len); + if buf.len() > ContainerInfo::SIZE { + payload.extend_from_slice(&buf[ContainerInfo::SIZE..]); + } + + while payload.len() < payload_len { + let remaining = payload_len - payload.len(); + let mut chunk = vec![0u8; min(remaining, self.chunk_size())]; + let n = ptp.handle.read_bulk(ptp.bulk_in, &mut chunk, timeout)?; + trace!("Read additional chunk ({n} bytes)"); + if n == 0 { + break; + } + payload.extend_from_slice(&chunk[..n]); + } + + trace!( + "Finished reading container, total payload of {} bytes", + payload.len(), + ); + + Ok((container_info, payload)) + } + + fn open_session(&self, ptp: &mut Ptp, session_id: u32) -> anyhow::Result<()> { debug!("Sending OpenSession command"); - _ = ptp.command( - StandardCommandCode::OpenSession, - &[session_id], + _ = self.send( + ptp, + CommandCode::OpenSession, + Some(&[session_id]), None, - self.timeout(), + true, )?; Ok(()) } - fn close_session(&self, ptp: &mut libptp::Camera

, _: u32) -> anyhow::Result<()> { + fn close_session(&self, ptp: &mut Ptp, _: u32) -> anyhow::Result<()> { debug!("Sending CloseSession command"); - let _ = ptp.command(StandardCommandCode::CloseSession, &[], None, self.timeout())?; + _ = self.send(ptp, CommandCode::CloseSession, None, None, true)?; Ok(()) } - fn get_info(&self, ptp: &mut libptp::Camera

) -> anyhow::Result { + fn get_info(&self, ptp: &mut Ptp) -> anyhow::Result { debug!("Sending GetDeviceInfo command"); - let response = ptp.command( - StandardCommandCode::GetDeviceInfo, - &[], - None, - self.timeout(), - )?; + let response = self.send(ptp, CommandCode::GetDeviceInfo, None, None, true)?; debug!("Received response with {} bytes", response.len()); - let info = DeviceInfo::decode(&response)?; + let info = DeviceInfo::try_from(response.as_slice())?; Ok(info) } - fn get_prop_value( - &self, - ptp: &mut libptp::Camera

, - prop: DevicePropCode, - ) -> anyhow::Result> { + fn get_prop_value(&self, ptp: &mut Ptp, prop: PropCode) -> anyhow::Result> { debug!("Sending GetDevicePropValue command for property {prop:?}"); - let response = ptp.command( - StandardCommandCode::GetDevicePropValue, - &[prop as u32], + let response = self.send( + ptp, + CommandCode::GetDevicePropValue, + Some(&[prop as u32]), None, - self.timeout(), + true, )?; debug!("Received response with {} bytes", response.len()); Ok(response) } - fn export_backup(&self, ptp: &mut libptp::Camera

) -> anyhow::Result> { + fn export_backup(&self, ptp: &mut Ptp) -> anyhow::Result> { const HANDLE: u32 = 0x0; debug!("Sending GetObjectInfo command for backup"); - let response = ptp.command( - StandardCommandCode::GetObjectInfo, - &[HANDLE], - None, - self.timeout(), - )?; + let response = self.send(ptp, CommandCode::GetObjectInfo, Some(&[HANDLE]), None, true)?; debug!("Received response with {} bytes", response.len()); debug!("Sending GetObject command for backup"); - let response = ptp.command( - StandardCommandCode::GetObject, - &[HANDLE], - None, - self.timeout(), - )?; + let response = self.send(ptp, CommandCode::GetObject, Some(&[HANDLE]), None, true)?; debug!("Received response with {} bytes", response.len()); Ok(response) } - fn import_backup(&self, ptp: &mut libptp::Camera

, buffer: &[u8]) -> anyhow::Result<()> { + fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> { debug!("Preparing ObjectInfo header for backup"); - - let mut obj_info = vec![0u8; 1012]; - + let mut obj_info = vec![0u8; 1088]; let mut cursor = Cursor::new(&mut obj_info[..]); cursor.write_u32::(0x0)?; cursor.write_u16::(0x5000)?; @@ -261,20 +423,22 @@ pub trait CameraImpl { cursor.write_u32::(u32::try_from(buffer.len())?)?; debug!("Sending SendObjectInfo command for backup"); - let response = ptp.command( - libptp::StandardCommandCode::SendObjectInfo, - &[0x0, 0x0], + let response = self.send( + ptp, + CommandCode::SendObjectInfo, + Some(&[0x0, 0x0]), Some(&obj_info), - self.timeout(), + true, )?; debug!("Received response with {} bytes", response.len()); debug!("Sending SendObject command for backup"); - let response = ptp.command( - libptp::StandardCommandCode::SendObject, - &[0x0], + let response = self.send( + ptp, + CommandCode::SendObject, + Some(&[0x0]), Some(buffer), - self.timeout(), + false, )?; debug!("Received response with {} bytes", response.len()); diff --git a/src/camera/ptp/enums.rs b/src/camera/ptp/enums.rs new file mode 100644 index 0000000..f2d6f41 --- /dev/null +++ b/src/camera/ptp/enums.rs @@ -0,0 +1,173 @@ +use std::fmt; + +use anyhow::bail; +use serde::Serialize; + +#[repr(u16)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CommandCode { + GetDeviceInfo = 0x1001, + OpenSession = 0x1002, + CloseSession = 0x1003, + GetObjectInfo = 0x1008, + GetObject = 0x1009, + SendObjectInfo = 0x100C, + SendObject = 0x100D, + GetDevicePropValue = 0x1015, + SetDevicePropValue = 0x1016, +} + +impl TryFrom for CommandCode { + type Error = anyhow::Error; + + fn try_from(value: u16) -> Result { + match value { + 0x1001 => Ok(Self::GetDeviceInfo), + 0x1002 => Ok(Self::OpenSession), + 0x1003 => Ok(Self::CloseSession), + 0x1008 => Ok(Self::GetObjectInfo), + 0x1009 => Ok(Self::GetObject), + 0x100C => Ok(Self::SendObjectInfo), + 0x100D => Ok(Self::SendObject), + 0x1015 => Ok(Self::GetDevicePropValue), + 0x1016 => Ok(Self::SetDevicePropValue), + v => bail!("Unknown command code '{v}'"), + } + } +} + +#[repr(u16)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResponseCode { + Undefined = 0x2000, + Ok = 0x2001, + GeneralError = 0x2002, + SessionNotOpen = 0x2003, + InvalidTransactionId = 0x2004, + OperationNotSupported = 0x2005, + ParameterNotSupported = 0x2006, + IncompleteTransfer = 0x2007, + InvalidStorageId = 0x2008, + InvalidObjectHandle = 0x2009, + DevicePropNotSupported = 0x200A, + InvalidObjectFormatCode = 0x200B, + StoreFull = 0x200C, + ObjectWriteProtected = 0x200D, + StoreReadOnly = 0x200E, + AccessDenied = 0x200F, + NoThumbnailPresent = 0x2010, + SelfTestFailed = 0x2011, + PartialDeletion = 0x2012, + StoreNotAvailable = 0x2013, + SpecificationByFormatUnsupported = 0x2014, + NoValidObjectInfo = 0x2015, + InvalidCodeFormat = 0x2016, + UnknownVendorCode = 0x2017, + CaptureAlreadyTerminated = 0x2018, + DeviceBusy = 0x2019, + InvalidParentObject = 0x201A, + InvalidDevicePropFormat = 0x201B, + InvalidDevicePropValue = 0x201C, + InvalidParameter = 0x201D, + SessionAlreadyOpen = 0x201E, + TransactionCancelled = 0x201F, + SpecificationOfDestinationUnsupported = 0x2020, +} + +impl std::convert::TryFrom for ResponseCode { + type Error = anyhow::Error; + + fn try_from(value: u16) -> Result { + match value { + 0x2000 => Ok(Self::Undefined), + 0x2001 => Ok(Self::Ok), + 0x2002 => Ok(Self::GeneralError), + 0x2003 => Ok(Self::SessionNotOpen), + 0x2004 => Ok(Self::InvalidTransactionId), + 0x2005 => Ok(Self::OperationNotSupported), + 0x2006 => Ok(Self::ParameterNotSupported), + 0x2007 => Ok(Self::IncompleteTransfer), + 0x2008 => Ok(Self::InvalidStorageId), + 0x2009 => Ok(Self::InvalidObjectHandle), + 0x200A => Ok(Self::DevicePropNotSupported), + 0x200B => Ok(Self::InvalidObjectFormatCode), + 0x200C => Ok(Self::StoreFull), + 0x200D => Ok(Self::ObjectWriteProtected), + 0x200E => Ok(Self::StoreReadOnly), + 0x200F => Ok(Self::AccessDenied), + 0x2010 => Ok(Self::NoThumbnailPresent), + 0x2011 => Ok(Self::SelfTestFailed), + 0x2012 => Ok(Self::PartialDeletion), + 0x2013 => Ok(Self::StoreNotAvailable), + 0x2014 => Ok(Self::SpecificationByFormatUnsupported), + 0x2015 => Ok(Self::NoValidObjectInfo), + 0x2016 => Ok(Self::InvalidCodeFormat), + 0x2017 => Ok(Self::UnknownVendorCode), + 0x2018 => Ok(Self::CaptureAlreadyTerminated), + 0x2019 => Ok(Self::DeviceBusy), + 0x201A => Ok(Self::InvalidParentObject), + 0x201B => Ok(Self::InvalidDevicePropFormat), + 0x201C => Ok(Self::InvalidDevicePropValue), + 0x201D => Ok(Self::InvalidParameter), + 0x201E => Ok(Self::SessionAlreadyOpen), + 0x201F => Ok(Self::TransactionCancelled), + 0x2020 => Ok(Self::SpecificationOfDestinationUnsupported), + v => bail!("Unknown response code '{v}'"), + } + } +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PropCode { + FujiUsbMode = 0xd16e, + FujiBatteryInfo2 = 0xD36B, +} + +#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] +pub enum UsbMode { + RawConversion, + Unsupported, +} + +impl From for UsbMode { + fn from(val: u32) -> Self { + match val { + 6 => Self::RawConversion, + _ => Self::Unsupported, + } + } +} + +impl fmt::Display for UsbMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::RawConversion => "USB RAW CONV./BACKUP RESTORE", + Self::Unsupported => "Unsupported USB Mode", + }; + write!(f, "{s}") + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u16)] +pub enum ContainerType { + Command = 1, + Data = 2, + Response = 3, + Event = 4, +} + +impl TryFrom for ContainerType { + type Error = anyhow::Error; + + fn try_from(value: u16) -> Result { + match value { + 1 => Ok(Self::Command), + 2 => Ok(Self::Data), + 3 => Ok(Self::Response), + 4 => Ok(Self::Event), + v => bail!("Invalid message type '{v}'"), + } + } +} diff --git a/src/camera/ptp/error.rs b/src/camera/ptp/error.rs new file mode 100644 index 0000000..363d396 --- /dev/null +++ b/src/camera/ptp/error.rs @@ -0,0 +1,53 @@ +use std::{fmt, io}; + +use crate::camera::ptp::enums::ResponseCode; + +#[derive(Debug)] +pub enum Error { + Response(u16), + Malformed(String), + Usb(rusb::Error), + Io(io::Error), +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::Response(r) => { + let name = ResponseCode::try_from(r) + .map_or_else(|_| "Unknown".to_string(), |c| format!("{c:?}")); + write!(f, "{name} (0x{r:04x})") + } + Self::Usb(ref e) => write!(f, "USB error: {e}"), + Self::Io(ref e) => write!(f, "IO error: {e}"), + Self::Malformed(ref e) => write!(f, "{e}"), + } + } +} + +impl ::std::error::Error for Error { + fn cause(&self) -> Option<&dyn (::std::error::Error)> { + match *self { + Self::Usb(ref e) => Some(e), + Self::Io(ref e) => Some(e), + _ => None, + } + } +} + +impl From for Error { + fn from(e: rusb::Error) -> Self { + Self::Usb(e) + } +} + +impl From for Error { + fn from(e: io::Error) -> Self { + match e.kind() { + io::ErrorKind::UnexpectedEof => { + Self::Malformed("Unexpected end of message".to_string()) + } + _ => Self::Io(e), + } + } +} diff --git a/src/camera/ptp/mod.rs b/src/camera/ptp/mod.rs new file mode 100644 index 0000000..61172d6 --- /dev/null +++ b/src/camera/ptp/mod.rs @@ -0,0 +1,4 @@ +pub mod enums; +pub mod error; +pub mod read; +pub mod structs; diff --git a/src/camera/ptp/read.rs b/src/camera/ptp/read.rs new file mode 100644 index 0000000..91fb729 --- /dev/null +++ b/src/camera/ptp/read.rs @@ -0,0 +1,127 @@ +#![allow(dead_code)] +#![allow(clippy::redundant_closure_for_method_calls)] + +use std::io::Cursor; + +use anyhow::bail; +use byteorder::{LittleEndian, ReadBytesExt}; + +pub trait Read: ReadBytesExt { + fn read_ptp_u8(&mut self) -> anyhow::Result { + Ok(self.read_u8()?) + } + + fn read_ptp_i8(&mut self) -> anyhow::Result { + Ok(self.read_i8()?) + } + + fn read_ptp_u16(&mut self) -> anyhow::Result { + Ok(self.read_u16::()?) + } + + fn read_ptp_i16(&mut self) -> anyhow::Result { + Ok(self.read_i16::()?) + } + + fn read_ptp_u32(&mut self) -> anyhow::Result { + Ok(self.read_u32::()?) + } + + fn read_ptp_i32(&mut self) -> anyhow::Result { + Ok(self.read_i32::()?) + } + + fn read_ptp_u64(&mut self) -> anyhow::Result { + Ok(self.read_u64::()?) + } + + fn read_ptp_i64(&mut self) -> anyhow::Result { + Ok(self.read_i64::()?) + } + + fn read_ptp_u128(&mut self) -> anyhow::Result { + Ok(self.read_u128::()?) + } + + fn read_ptp_i128(&mut self) -> anyhow::Result { + Ok(self.read_i128::()?) + } + + fn read_ptp_vec anyhow::Result>( + &mut self, + func: U, + ) -> anyhow::Result> { + let len = self.read_u32::()? as usize; + (0..len).map(|_| func(self)).collect() + } + + fn read_ptp_u8_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u8()) + } + + fn read_ptp_i8_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i8()) + } + + fn read_ptp_u16_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u16()) + } + + fn read_ptp_i16_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i16()) + } + + fn read_ptp_u32_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u32()) + } + + fn read_ptp_i32_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i32()) + } + + fn read_ptp_u64_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u64()) + } + + fn read_ptp_i64_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i64()) + } + + fn read_ptp_u128_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u128()) + } + + fn read_ptp_i128_vec(&mut self) -> anyhow::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i128()) + } + + fn read_ptp_str(&mut self) -> anyhow::Result { + let len = self.read_u8()?; + if len > 0 { + let data: Vec = (0..(len - 1)) + .map(|_| self.read_u16::()) + .collect::>()?; + self.read_u16::()?; + Ok(String::from_utf16(&data)?) + } else { + Ok(String::new()) + } + } + + fn expect_end(&mut self) -> anyhow::Result<()>; +} + +impl> Read for Cursor { + fn expect_end(&mut self) -> anyhow::Result<()> { + let len = self.get_ref().as_ref().len(); + if len as u64 != self.position() { + bail!(super::error::Error::Malformed(format!( + "Response {} bytes, expected {} bytes", + len, + self.position() + ))) + } + + Ok(()) + } +} diff --git a/src/camera/ptp/structs.rs b/src/camera/ptp/structs.rs new file mode 100644 index 0000000..8e166f7 --- /dev/null +++ b/src/camera/ptp/structs.rs @@ -0,0 +1,77 @@ +use std::io::Cursor; + +use byteorder::{LittleEndian, ReadBytesExt}; + +use super::{enums::ContainerType, read::Read}; + +#[allow(dead_code)] +#[derive(Debug)] +pub struct DeviceInfo { + pub version: u16, + pub vendor_ex_id: u32, + pub vendor_ex_version: u16, + pub vendor_extension_desc: String, + pub functional_mode: u16, + pub operations_supported: Vec, + pub events_supported: Vec, + pub device_properties_supported: Vec, + pub capture_formats: Vec, + pub image_formats: Vec, + pub manufacturer: String, + pub model: String, + pub device_version: String, + pub serial_number: String, +} + +impl TryFrom<&[u8]> for DeviceInfo { + type Error = anyhow::Error; + + fn try_from(buf: &[u8]) -> Result { + let mut cur = Cursor::new(buf); + + Ok(Self { + version: cur.read_ptp_u16()?, + vendor_ex_id: cur.read_ptp_u32()?, + vendor_ex_version: cur.read_ptp_u16()?, + vendor_extension_desc: cur.read_ptp_str()?, + functional_mode: cur.read_ptp_u16()?, + operations_supported: cur.read_ptp_u16_vec()?, + events_supported: cur.read_ptp_u16_vec()?, + device_properties_supported: cur.read_ptp_u16_vec()?, + capture_formats: cur.read_ptp_u16_vec()?, + image_formats: cur.read_ptp_u16_vec()?, + manufacturer: cur.read_ptp_str()?, + model: cur.read_ptp_str()?, + device_version: cur.read_ptp_str()?, + serial_number: cur.read_ptp_str()?, + }) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct ContainerInfo { + pub kind: ContainerType, + pub payload_len: u32, + pub code: u16, +} + +impl ContainerInfo { + pub const SIZE: usize = + size_of::() + size_of::() + size_of::() + size_of::(); +} + +impl ContainerInfo { + pub fn parse(mut r: R) -> anyhow::Result { + let payload_len = r.read_u32::()? - Self::SIZE as u32; + let kind = r.read_u16::()?; + let kind = ContainerType::try_from(kind)?; + let code = r.read_u16::()?; + let _transaction_id = r.read_u32::()?; + + Ok(Self { + kind, + payload_len, + code, + }) + } +} diff --git a/src/cli/common/film.rs b/src/cli/common/film.rs index c266a73..2562576 100644 --- a/src/cli/common/film.rs +++ b/src/cli/common/film.rs @@ -1,27 +1,4 @@ -use anyhow::bail; use clap::Args; -#[derive(Debug, Clone)] -pub enum SimulationSelector { - Slot(u8), - Name(String), -} - -impl std::str::FromStr for SimulationSelector { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - if let Ok(slot) = s.parse::() { - return Ok(Self::Slot(slot)); - } - - if s.is_empty() { - bail!("Simulation name cannot be empty") - } - - Ok(Self::Name(s.to_string())) - } -} - #[derive(Args, Debug)] pub struct FilmSimulationOptions {} diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs index a669066..f4d952e 100644 --- a/src/cli/device/mod.rs +++ b/src/cli/device/mod.rs @@ -4,7 +4,7 @@ use clap::Subcommand; use serde::Serialize; use crate::{ - camera::{Camera, UsbMode}, + camera::{Camera, ptp::enums::UsbMode}, usb, }; @@ -112,10 +112,10 @@ fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> { let repr = CameraRepr { device: (&camera).into(), - manufacturer: info.Manufacturer.clone(), - model: info.Model.clone(), - device_version: info.DeviceVersion.clone(), - serial_number: info.SerialNumber, + manufacturer: info.manufacturer.clone(), + model: info.model.clone(), + device_version: info.device_version.clone(), + serial_number: info.serial_number, mode, battery, }; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 80e574d..503fb80 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -5,7 +5,7 @@ pub mod device; pub mod render; pub mod simulation; -use clap::{Parser, Subcommand}; +use clap::{ArgAction, Parser, Subcommand}; use backup::BackupCmd; use device::DeviceCmd; @@ -23,13 +23,9 @@ pub struct Cli { #[arg(long, short = 'j', global = true)] pub json: bool, - /// Only log warnings and errors - #[arg(long, short = 'q', global = true, conflicts_with = "verbose")] - pub quiet: bool, - - /// Log extra debugging information - #[arg(long, short = 'v', global = true, conflicts_with = "quiet")] - pub verbose: bool, + /// Log extra debugging information (multiple instances increase verbosity) + #[arg(long, short = 'v', action = ArgAction::Count, global = true)] + pub verbose: u8, /// Manually specify target device #[arg(long, short = 'd', global = true)] diff --git a/src/cli/render/mod.rs b/src/cli/render/mod.rs index e2db2f4..1709c52 100644 --- a/src/cli/render/mod.rs +++ b/src/cli/render/mod.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use super::common::{ file::{Input, Output}, - film::{FilmSimulationOptions, SimulationSelector}, + film::FilmSimulationOptions, }; use clap::Args; @@ -10,7 +10,7 @@ use clap::Args; pub struct RenderCmd { /// Simulation number or name #[arg(long, conflicts_with = "simulation_file")] - simulation: Option, + simulation: Option, /// Path to exported simulation #[arg(long, conflicts_with = "simulation")] diff --git a/src/cli/simulation/mod.rs b/src/cli/simulation/mod.rs index b3323b8..edf0741 100644 --- a/src/cli/simulation/mod.rs +++ b/src/cli/simulation/mod.rs @@ -1,6 +1,6 @@ use super::common::{ file::{Input, Output}, - film::{FilmSimulationOptions, SimulationSelector}, + film::FilmSimulationOptions, }; use clap::Subcommand; @@ -14,14 +14,14 @@ pub enum SimulationCmd { #[command(alias = "g")] Get { /// Simulation number or name - simulation: SimulationSelector, + simulation: u8, }, /// Set simulation parameters #[command(alias = "s")] Set { /// Simulation number or name - simulation: SimulationSelector, + simulation: u8, #[command(flatten)] film_simulation_options: FilmSimulationOptions, @@ -31,7 +31,7 @@ pub enum SimulationCmd { #[command(alias = "e")] Export { /// Simulation number or name - simulation: SimulationSelector, + simulation: u8, /// Output file (use '-' to write to stdout) output_file: Output, diff --git a/src/log.rs b/src/log.rs index 786710e..400d5a9 100644 --- a/src/log.rs +++ b/src/log.rs @@ -6,13 +6,12 @@ use log4rs::{ encode::pattern::PatternEncoder, }; -pub fn init(quiet: bool, verbose: bool) -> anyhow::Result<()> { - let level = if quiet { - LevelFilter::Warn - } else if verbose { - LevelFilter::Debug - } else { - LevelFilter::Info +pub fn init(verbose: u8) -> anyhow::Result<()> { + let level = match verbose { + 0 => LevelFilter::Warn, + 1 => LevelFilter::Info, + 2 => LevelFilter::Debug, + _ => LevelFilter::Trace, }; let encoder = Box::new(PatternEncoder::new("{d} {h({l})} {M}::{L} - {m}{n}")); diff --git a/src/main.rs b/src/main.rs index 35d1d39..94f5795 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,15 +4,15 @@ use clap::Parser; use cli::Commands; -mod cli; mod camera; +mod cli; mod log; mod usb; fn main() -> anyhow::Result<()> { let cli = cli::Cli::parse(); - log::init(cli.quiet, cli.verbose)?; + log::init(cli.verbose)?; let device_id = cli.device.as_deref();