From 943f22c0741de1042bcac0b1dec82488f1cab061 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Wed, 15 Oct 2025 20:04:16 +0100 Subject: [PATCH] chore: refactor impl Signed-off-by: Nikolaos Karaolidis --- src/camera/devices.rs | 63 +++++++ src/camera/mod.rs | 283 +++++++++++++++++++++++++++++++ src/cli/backup/mod.rs | 20 +-- src/cli/common/file.rs | 4 +- src/cli/device/mod.rs | 43 +++-- src/hardware/mod.rs | 375 ----------------------------------------- src/log.rs | 2 +- src/main.rs | 4 +- src/usb/mod.rs | 33 +--- 9 files changed, 388 insertions(+), 439 deletions(-) create mode 100644 src/camera/devices.rs create mode 100644 src/camera/mod.rs delete mode 100644 src/hardware/mod.rs diff --git a/src/camera/devices.rs b/src/camera/devices.rs new file mode 100644 index 0000000..4513ff4 --- /dev/null +++ b/src/camera/devices.rs @@ -0,0 +1,63 @@ +use anyhow::bail; +use rusb::GlobalContext; + +use super::CameraImpl; + +type ImplFactory

= fn() -> Box>; + +#[derive(Debug, Clone, Copy)] +pub struct SupportedCamera { + pub name: &'static str, + pub vendor: u16, + pub product: u16, + pub impl_factory: ImplFactory

, +} + +impl SupportedCamera

{ + pub fn new_camera(&self, device: &rusb::Device

) -> anyhow::Result>> { + let descriptor = device.device_descriptor()?; + + let matches = + descriptor.vendor_id() == self.vendor && descriptor.product_id() == self.product; + + if !matches { + bail!( + "Device with vendor {:04x} and product {:04x} does not match {}", + descriptor.vendor_id(), + descriptor.product_id(), + self.name + ); + } + + Ok((self.impl_factory)()) + } +} + +macro_rules! default_camera_impl { + ( + $const_name:ident, + $struct_name:ident, + $vendor:expr, + $product:expr, + $display_name:expr + ) => { + pub const $const_name: SupportedCamera = SupportedCamera { + name: $display_name, + vendor: $vendor, + product: $product, + impl_factory: || Box::new($struct_name {}), + }; + + pub struct $struct_name {} + + impl crate::camera::CameraImpl for $struct_name { + fn supported_camera(&self) -> &'static SupportedCamera { + &$const_name + } + } + }; +} + +default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5"); + +pub const SUPPORTED: &[SupportedCamera] = &[FUJIFILM_XT5]; diff --git a/src/camera/mod.rs b/src/camera/mod.rs new file mode 100644 index 0000000..b9a5759 --- /dev/null +++ b/src/camera/mod.rs @@ -0,0 +1,283 @@ +pub mod devices; + +use std::{error::Error, fmt, 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 {} + +const SESSION: u32 = 1; + +pub struct Camera { + bus: u8, + address: u8, + ptp: libptp::Camera, + r#impl: Box>, +} + +impl Camera { + pub fn from_device(device: &rusb::Device) -> anyhow::Result { + for supported_camera in devices::SUPPORTED { + 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)?; + + debug!("Opening session"); + let () = r#impl.open_session(&mut ptp, SESSION)?; + debug!("Session opened"); + + return Ok(Self { + bus, + address, + ptp, + r#impl, + }); + } + } + + bail!("Device not supported"); + } + + pub fn name(&self) -> &'static str { + self.r#impl.supported_camera().name + } + + pub fn vendor_id(&self) -> u16 { + self.r#impl.supported_camera().vendor + } + + pub fn product_id(&self) -> u16 { + self.r#impl.supported_camera().product + } + + pub fn connected_usb_id(&self) -> String { + format!("{}.{}", self.bus, self.address) + } + + fn prop_value_as_scalar(data: &[u8]) -> anyhow::Result { + let data = match data.len() { + 1 => anyhow::Ok(u32::from(data[0])), + 2 => anyhow::Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))), + 4 => anyhow::Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])), + n => bail!("Cannot parse {n} bytes as scalar"), + }?; + + Ok(data) + } + + pub fn get_info(&mut self) -> anyhow::Result { + let info = self.r#impl.get_info(&mut self.ptp)?; + Ok(info) + } + + pub fn get_usb_mode(&mut self) -> anyhow::Result { + let data = self + .r#impl + .get_prop_value(&mut self.ptp, DevicePropCode::FujiUsbMode); + + let result = Self::prop_value_as_scalar(&data?)?.into(); + Ok(result) + } + + pub fn get_battery_info(&mut self) -> anyhow::Result { + let data = self + .r#impl + .get_prop_value(&mut self.ptp, DevicePropCode::FujiBatteryInfo2); + + let data = data?; + debug!("Raw battery data: {data:?}"); + + let utf16: Vec = data[1..] + .chunks(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .take_while(|&c| c != 0) + .collect(); + + let utf8_string = String::from_utf16(&utf16)?; + debug!("Decoded UTF-16 string: {utf8_string}"); + + let percentage: u32 = utf8_string + .split(',') + .next() + .ok_or_else(|| anyhow!("Failed to parse battery percentage"))? + .parse()?; + + Ok(percentage) + } + + pub fn export_backup(&mut self) -> anyhow::Result> { + self.r#impl.export_backup(&mut self.ptp) + } + + pub fn import_backup(&mut self, backup: &[u8]) -> anyhow::Result<()> { + self.r#impl.import_backup(&mut self.ptp, backup) + } +} + +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}") + } + 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, + } + } +} + +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}") + } +} + +pub trait CameraImpl { + fn supported_camera(&self) -> &'static SupportedCamera

; + + fn timeout(&self) -> Option { + None + } + + fn open_session(&self, ptp: &mut libptp::Camera

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

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

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

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

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

, buffer: &[u8]) -> anyhow::Result<()> { + debug!("Preparing ObjectInfo header for backup"); + + let mut obj_info = vec![0u8; 1012]; + + let mut cursor = Cursor::new(&mut obj_info[..]); + cursor.write_u32::(0x0)?; + cursor.write_u16::(0x5000)?; + cursor.write_u16::(0x0)?; + cursor.write_u32::(u32::try_from(buffer.len())?)?; + + debug!("Sending SendObjectInfo command for backup"); + let response = ptp.command( + libptp::StandardCommandCode::SendObjectInfo, + &[0x0, 0x0], + Some(&obj_info), + self.timeout(), + )?; + debug!("Received response with {} bytes", response.len()); + + debug!("Sending SendObject command for backup"); + let response = ptp.command( + libptp::StandardCommandCode::SendObject, + &[0x0], + Some(buffer), + self.timeout(), + )?; + debug!("Received response with {} bytes", response.len()); + + Ok(()) + } +} diff --git a/src/cli/backup/mod.rs b/src/cli/backup/mod.rs index 741310c..9511498 100644 --- a/src/cli/backup/mod.rs +++ b/src/cli/backup/mod.rs @@ -20,30 +20,28 @@ pub enum BackupCmd { }, } -fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> { - let camera = usb::get_camera(device_id)?; - let mut ptp = camera.ptp_session()?; +fn handle_export(device_id: Option<&str>, output: &Output) -> anyhow::Result<()> { + let mut camera = usb::get_camera(device_id)?; let mut writer = output.get_writer()?; - let backup = camera.export_backup(&mut ptp)?; + let backup = camera.export_backup()?; writer.write_all(&backup)?; Ok(()) } -fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> { - let camera = usb::get_camera(device_id)?; - let mut ptp = camera.ptp_session()?; +fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> { + let mut camera = usb::get_camera(device_id)?; let mut reader = input.get_reader()?; - let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer)?; - camera.import_backup(&mut ptp, &buffer)?; + let mut backup = Vec::new(); + reader.read_to_end(&mut backup)?; + camera.import_backup(&backup)?; Ok(()) } -pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> { +pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> anyhow::Result<()> { match cmd { BackupCmd::Export { output_file } => handle_export(device_id, &output_file), BackupCmd::Import { input_file } => handle_import(device_id, &input_file), diff --git a/src/cli/common/file.rs b/src/cli/common/file.rs index 3b11bb3..b502aec 100644 --- a/src/cli/common/file.rs +++ b/src/cli/common/file.rs @@ -18,7 +18,7 @@ impl FromStr for Input { } impl Input { - pub fn get_reader(&self) -> Result, anyhow::Error> { + pub fn get_reader(&self) -> anyhow::Result> { match self { Self::Stdin => Ok(Box::new(io::stdin())), Self::Path(path) => Ok(Box::new(File::open(path)?)), @@ -44,7 +44,7 @@ impl FromStr for Output { } impl Output { - pub fn get_writer(&self) -> Result, anyhow::Error> { + pub fn get_writer(&self) -> anyhow::Result> { match self { Self::Stdout => Ok(Box::new(io::stdout())), Self::Path(path) => Ok(Box::new(File::create(path)?)), diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs index 2c9a0bc..a669066 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::{ - hardware::{CameraImpl, UsbMode}, + camera::{Camera, UsbMode}, usb, }; @@ -21,19 +21,19 @@ pub enum DeviceCmd { #[derive(Serialize)] pub struct CameraItemRepr { - pub name: String, - pub id: String, + pub name: &'static str, + pub usb_id: String, pub vendor_id: String, pub product_id: String, } -impl From<&Box> for CameraItemRepr { - fn from(camera: &Box) -> Self { +impl From<&Camera> for CameraItemRepr { + fn from(camera: &Camera) -> Self { Self { - id: camera.usb_id(), - name: camera.id().name.to_string(), - vendor_id: format!("0x{:04x}", camera.id().vendor), - product_id: format!("0x{:04x}", camera.id().product), + name: camera.name(), + usb_id: camera.connected_usb_id(), + vendor_id: format!("0x{:04x}", camera.vendor_id()), + product_id: format!("0x{:04x}", camera.product_id()), } } } @@ -42,14 +42,14 @@ impl fmt::Display for CameraItemRepr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, - "{} ({}:{}) (ID: {})", - self.name, self.vendor_id, self.product_id, self.id + "{} ({}:{}) (USB ID: {})", + self.name, self.vendor_id, self.product_id, self.usb_id ) } } -fn handle_list(json: bool) -> Result<(), anyhow::Error> { - let cameras: Vec = usb::get_connected_camers()? +fn handle_list(json: bool) -> anyhow::Result<()> { + let cameras: Vec = usb::get_connected_cameras()? .iter() .map(std::convert::Into::into) .collect(); @@ -88,7 +88,7 @@ pub struct CameraRepr { impl fmt::Display for CameraRepr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Name: {}", self.device.name)?; - writeln!(f, "ID: {}", self.device.id)?; + writeln!(f, "USB ID: {}", self.device.usb_id)?; writeln!( f, "Vendor ID: {}, Product ID: {}", @@ -103,15 +103,12 @@ impl fmt::Display for CameraRepr { } } -fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> { - let camera = usb::get_camera(device_id)?; - let mut ptp = camera.ptp(); +fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> { + let mut camera = usb::get_camera(device_id)?; - let info = camera.get_info(&mut ptp)?; - - let mut ptp = camera.open_session(ptp)?; - let mode = camera.get_usb_mode(&mut ptp)?; - let battery = camera.get_battery_info(&mut ptp)?; + let info = camera.get_info()?; + let mode = camera.get_usb_mode()?; + let battery = camera.get_battery_info()?; let repr = CameraRepr { device: (&camera).into(), @@ -132,7 +129,7 @@ fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> Ok(()) } -pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> { +pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> anyhow::Result<()> { match cmd { DeviceCmd::List => handle_list(json), DeviceCmd::Info => handle_info(json, device_id), diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs deleted file mode 100644 index 1b8d16d..0000000 --- a/src/hardware/mod.rs +++ /dev/null @@ -1,375 +0,0 @@ -use std::{ - fmt, - io::Cursor, - ops::{Deref, DerefMut}, - time::Duration, -}; - -use anyhow::bail; -use byteorder::{LittleEndian, WriteBytesExt}; -use libptp::{DeviceInfo, StandardCommandCode}; -use log::{debug, error}; -use rusb::{DeviceDescriptor, GlobalContext}; -use serde::Serialize; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CameraId { - pub name: &'static str, - pub vendor: u16, - pub product: u16, -} - -type CameraFactory = fn(rusb::Device) -> Result, anyhow::Error>; - -pub struct SupportedCamera { - pub id: CameraId, - pub factory: CameraFactory, -} - -pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera { - id: FUJIFILM_XT5, - factory: |d| FujifilmXT5::new_boxed(&d), -}]; - -impl SupportedCamera { - pub fn matches_descriptor(&self, descriptor: &DeviceDescriptor) -> bool { - descriptor.vendor_id() == self.id.vendor && descriptor.product_id() == self.id.product - } -} - -pub const TIMEOUT: Duration = Duration::from_millis(500); - -#[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, - } - } -} - -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}") - } -} - -pub struct Ptp { - ptp: libptp::Camera, -} - -impl Deref for Ptp { - type Target = libptp::Camera; - - fn deref(&self) -> &Self::Target { - &self.ptp - } -} - -impl DerefMut for Ptp { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ptp - } -} - -impl From> for Ptp { - fn from(ptp: libptp::Camera) -> Self { - Self { ptp } - } -} - -type SessionCloseFn = - Box) -> Result<(), anyhow::Error>>; - -pub struct PtpSession { - ptp: libptp::Camera, - session_id: u32, - close_fn: Option, -} - -impl Deref for PtpSession { - type Target = libptp::Camera; - - fn deref(&self) -> &Self::Target { - &self.ptp - } -} - -impl DerefMut for PtpSession { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ptp - } -} - -impl Drop for PtpSession { - fn drop(&mut self) { - if let Some(close_fn) = self.close_fn.take() { - if let Err(e) = close_fn(self.session_id, &mut self.ptp) { - error!("Error closing session {}: {}", self.session_id, e); - } - } - } -} - -pub trait CameraImpl { - fn id(&self) -> &'static CameraId; - - fn device(&self) -> &rusb::Device; - - fn usb_id(&self) -> String { - let bus = self.device().bus_number(); - let address = self.device().address(); - format!("{bus}.{address}") - } - - fn ptp(&self) -> Ptp; - - fn ptp_session(&self) -> Result; - - fn get_info(&self, ptp: &mut Ptp) -> Result { - debug!("Sending GetDeviceInfo command"); - let response = ptp.command(StandardCommandCode::GetDeviceInfo, &[], None, Some(TIMEOUT))?; - debug!("Received response with {} bytes", response.len()); - - let info = DeviceInfo::decode(&response)?; - Ok(info) - } - - fn next_session_id(&self) -> u32; - - fn open_session(&self, ptp: Ptp) -> Result { - let session_id = self.next_session_id(); - let mut ptp = ptp.ptp; - - debug!("Opening session with id {session_id}"); - ptp.command( - StandardCommandCode::OpenSession, - &[session_id], - None, - Some(TIMEOUT), - )?; - debug!("Session {session_id} open"); - - let close_fn: Option = Some(Box::new(move |_, ptp| { - debug!("Closing session with id {session_id}"); - ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?; - debug!("Session {session_id} closed"); - Ok(()) - })); - - Ok(PtpSession { - ptp, - session_id, - close_fn, - }) - } - - fn get_prop_value_raw( - &self, - ptp: &mut PtpSession, - prop: DevicePropCode, - ) -> Result, anyhow::Error> { - debug!("Getting property {prop:?}"); - - let response = ptp.command( - StandardCommandCode::GetDevicePropValue, - &[prop as u32], - None, - Some(TIMEOUT), - )?; - - debug!("Received response with {} bytes", response.len()); - - Ok(response) - } - - fn get_prop_value_scalar( - &self, - ptp: &mut PtpSession, - prop: DevicePropCode, - ) -> Result { - let data = self.get_prop_value_raw(ptp, prop)?; - - match data.len() { - 1 => Ok(u32::from(data[0])), - 2 => Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))), - 4 => Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])), - n => bail!("Cannot parse property {prop:?} as scalar: {n} bytes"), - } - } - - fn get_usb_mode(&self, ptp: &mut PtpSession) -> Result { - let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?; - Ok(result.into()) - } - - fn get_battery_info(&self, ptp: &mut PtpSession) -> Result { - let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?; - debug!("Raw battery data: {data:?}"); - - if data.len() < 3 { - bail!("Battery info payload too short"); - } - - let utf16: Vec = data[1..] - .chunks(2) - .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) - .take_while(|&c| c != 0) - .collect(); - - debug!("Decoded UTF-16 units: {utf16:?}"); - - let utf8_string = String::from_utf16(&utf16)?; - debug!("Decoded UTF-16 string: {utf8_string}"); - - let percentage: u32 = utf8_string - .split(',') - .next() - .ok_or_else(|| anyhow::anyhow!("Failed to parse battery percentage"))? - .parse()?; - - Ok(percentage) - } - - fn export_backup(&self, ptp: &mut PtpSession) -> Result, anyhow::Error> { - const HANDLE: u32 = 0x0; - - debug!("Getting object info for backup"); - - let info = ptp.command( - StandardCommandCode::GetObjectInfo, - &[HANDLE], - None, - Some(TIMEOUT), - )?; - - debug!("Got object info, {} bytes", info.len()); - - debug!("Downloading backup object"); - - let object = ptp.command( - StandardCommandCode::GetObject, - &[HANDLE], - None, - Some(TIMEOUT), - )?; - - debug!("Downloaded backup object ({} bytes)", object.len()); - - Ok(object) - } - - fn import_backup(&self, ptp: &mut PtpSession, buffer: &[u8]) -> Result<(), anyhow::Error> { - todo!("This is currently broken"); - - debug!("Preparing ObjectInfo header for backup"); - - let mut obj_info = vec![0u8; 1088]; - - let mut cursor = Cursor::new(&mut obj_info[..]); - cursor.write_u32::(0x0)?; - cursor.write_u16::(0x5000)?; - cursor.write_u16::(0x0)?; - cursor.write_u32::(u32::try_from(buffer.len())?)?; - - debug!("Sending ObjectInfo for backup"); - - ptp.command( - libptp::StandardCommandCode::SendObjectInfo, - &[0x0, 0x0], - Some(&obj_info), - Some(TIMEOUT), - )?; - - debug!("Sending backup payload ({} bytes)", buffer.len()); - - ptp.command( - libptp::StandardCommandCode::SendObject, - &[], - Some(buffer), - Some(TIMEOUT), - )?; - - Ok(()) - } -} - -macro_rules! default_camera_impl { - ( - $const_name:ident, - $struct_name:ident, - $vendor:expr, - $product:expr, - $display_name:expr - ) => { - pub const $const_name: CameraId = CameraId { - name: $display_name, - vendor: $vendor, - product: $product, - }; - - pub struct $struct_name { - device: rusb::Device, - session_counter: std::sync::atomic::AtomicU32, - } - - impl $struct_name { - pub fn new_boxed( - rusb_device: &rusb::Device, - ) -> Result, anyhow::Error> { - let session_counter = std::sync::atomic::AtomicU32::new(1); - - let handle = rusb_device.open()?; - let device = handle.device(); - - Ok(Box::new(Self { - device, - session_counter, - })) - } - } - - impl CameraImpl for $struct_name { - fn id(&self) -> &'static CameraId { - &$const_name - } - - fn device(&self) -> &rusb::Device { - &self.device - } - - fn ptp(&self) -> Ptp { - libptp::Camera::new(&self.device).unwrap().into() - } - - fn ptp_session(&self) -> Result { - let ptp = self.ptp(); - self.open_session(ptp) - } - - fn next_session_id(&self) -> u32 { - self.session_counter - .fetch_add(1, std::sync::atomic::Ordering::SeqCst) - } - } - }; -} - -default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5"); diff --git a/src/log.rs b/src/log.rs index 396e3ac..786710e 100644 --- a/src/log.rs +++ b/src/log.rs @@ -6,7 +6,7 @@ use log4rs::{ encode::pattern::PatternEncoder, }; -pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> { +pub fn init(quiet: bool, verbose: bool) -> anyhow::Result<()> { let level = if quiet { LevelFilter::Warn } else if verbose { diff --git a/src/main.rs b/src/main.rs index 62e449c..35d1d39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,11 +5,11 @@ use clap::Parser; use cli::Commands; mod cli; -mod hardware; +mod camera; mod log; mod usb; -fn main() -> Result<(), anyhow::Error> { +fn main() -> anyhow::Result<()> { let cli = cli::Cli::parse(); log::init(cli.quiet, cli.verbose)?; diff --git a/src/usb/mod.rs b/src/usb/mod.rs index 51ea467..13b6b51 100644 --- a/src/usb/mod.rs +++ b/src/usb/mod.rs @@ -1,28 +1,20 @@ use anyhow::{anyhow, bail}; -use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS}; +use crate::camera::Camera; -pub fn get_connected_camers() -> Result>, anyhow::Error> { +pub fn get_connected_cameras() -> anyhow::Result> { let mut connected_cameras = Vec::new(); for device in rusb::devices()?.iter() { - let Ok(descriptor) = device.device_descriptor() else { - continue; - }; - - for camera in SUPPORTED_CAMERAS { - if camera.matches_descriptor(&descriptor) { - let camera = (camera.factory)(device)?; - connected_cameras.push(camera); - break; - } + if let Ok(camera) = Camera::from_device(&device) { + connected_cameras.push(camera); } } Ok(connected_cameras) } -pub fn get_connected_camera_by_id(id: &str) -> Result, anyhow::Error> { +pub fn get_connected_camera_by_id(id: &str) -> anyhow::Result { let parts: Vec<&str> = id.split('.').collect(); if parts.len() != 2 { bail!("Invalid device id format: {id}"); @@ -33,26 +25,17 @@ pub fn get_connected_camera_by_id(id: &str) -> Result, anyho for device in rusb::devices()?.iter() { if device.bus_number() == bus && device.address() == address { - let descriptor = device.device_descriptor()?; - - for camera in SUPPORTED_CAMERAS { - if camera.matches_descriptor(&descriptor) { - let camera = (camera.factory)(device)?; - return Ok(camera); - } - } - - bail!("Device found at {id} but is not supported"); + return Camera::from_device(&device); } } bail!("No device found with id: {id}"); } -pub fn get_camera(device_id: Option<&str>) -> Result, anyhow::Error> { +pub fn get_camera(device_id: Option<&str>) -> anyhow::Result { match device_id { Some(id) => get_connected_camera_by_id(id), - None => get_connected_camers()? + None => get_connected_cameras()? .into_iter() .next() .ok_or_else(|| anyhow!("No supported devices connected.")),