pub mod devices; pub mod error; pub mod ptp; use std::time::Duration; use anyhow::{anyhow, bail}; use devices::SupportedCamera; use log::{debug, error}; use ptp::{ Ptp, hex::{ CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect, FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode, }, structs::DeviceInfo, }; use ptp_cursor::{PtpDeserialize, PtpSerialize}; use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE}; use crate::usb::find_endpoint; const SESSION: u32 = 1; pub struct Camera { pub r#impl: Box>, pub ptp: Ptp, } macro_rules! camera_custom_settings { ($( $name:ident : $type:ty => $code:expr ),+ $(,)?) => { $( paste::paste! { #[allow(dead_code)] pub fn [](&mut self) -> anyhow::Result<$type> { self.r#impl.[](&mut self.ptp) } #[allow(dead_code)] pub fn [](&mut self, value: &$type) -> anyhow::Result<()> { self.r#impl.[](&mut self.ptp, value) } } )+ }; } impl Camera { 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.ptp.bus, self.ptp.address) } 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 = UsbMode::try_from_ptp(&data)?; Ok(result) } pub fn get_battery_info(&mut self) -> anyhow::Result { let data = self .r#impl .get_prop_value(&mut self.ptp, DevicePropCode::FujiBatteryInfo2)?; debug!("Raw battery data: {data:?}"); let raw_string = String::try_from_ptp(&data)?; debug!("Decoded raw string: {raw_string}"); let percentage: u32 = raw_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) } camera_custom_settings! { active_custom_setting: FujiCustomSetting => DevicePropCode::FujiStillCustomSetting, custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiStillCustomSettingName, image_size: FujiImageSize => DevicePropCode::FujiStillCustomSettingImageSize, image_quality: FujiImageQuality => DevicePropCode::FujiStillCustomSettingImageQuality, dynamic_range: FujiDynamicRange => DevicePropCode::FujiStillCustomSettingDynamicRange, dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiStillCustomSettingDynamicRangePriority, film_simulation: FujiFilmSimulation => DevicePropCode::FujiStillCustomSettingFilmSimulation, grain_effect: FujiGrainEffect => DevicePropCode::FujiStillCustomSettingGrainEffect, white_balance: FujiWhiteBalance => DevicePropCode::FujiStillCustomSettingWhiteBalance, high_iso_nr: FujiHighISONR => DevicePropCode::FujiStillCustomSettingHighISONR, highlight_tone: FujiHighlightTone => DevicePropCode::FujiStillCustomSettingHighlightTone, shadow_tone: FujiShadowTone => DevicePropCode::FujiStillCustomSettingShadowTone, color: FujiColor => DevicePropCode::FujiStillCustomSettingColor, sharpness: FujiSharpness => DevicePropCode::FujiStillCustomSettingSharpness, clarity: FujiClarity => DevicePropCode::FujiStillCustomSettingClarity, white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiStillCustomSettingWhiteBalanceShiftRed, white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiStillCustomSettingWhiteBalanceShiftBlue, white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiStillCustomSettingWhiteBalanceTemperature, color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiStillCustomSettingColorChromeEffect, color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiStillCustomSettingColorChromeFXBlue, smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiStillCustomSettingSmoothSkinEffect, } } 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"); } } impl TryFrom<&rusb::Device> for Camera { type Error = anyhow::Error; fn try_from(device: &rusb::Device) -> anyhow::Result { let descriptor = device.device_descriptor()?; let vendor = descriptor.vendor_id(); let product = descriptor.product_id(); for supported_camera in devices::SUPPORTED { if vendor != supported_camera.vendor || product != supported_camera.product { continue; } let r#impl = (supported_camera.impl_factory)(); let bus = device.bus_number(); let address = device.address(); let config_descriptor = device.active_config_descriptor()?; let interface_descriptor = config_descriptor .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 handle = device.open()?; handle.claim_interface(interface)?; let bulk_in = find_endpoint( &interface_descriptor, rusb::Direction::In, rusb::TransferType::Bulk, )?; let bulk_out = find_endpoint( &interface_descriptor, rusb::Direction::Out, rusb::TransferType::Bulk, )?; let transaction_id = 0; let chunk_size = r#impl.chunk_size(); let mut ptp = Ptp { bus, address, interface, bulk_in, bulk_out, handle, transaction_id, chunk_size, }; debug!("Opening session"); let () = r#impl.open_session(&mut ptp, SESSION)?; debug!("Session opened"); return Ok(Self { r#impl, ptp }); } bail!("Device not supported"); } } macro_rules! camera_impl_custom_settings { ($( $name:ident : $type:ty => $code:expr ),+ $(,)?) => { $( paste::paste! { #[allow(dead_code)] fn [](&self, ptp: &mut Ptp) -> anyhow::Result<$type> { let bytes = self.get_prop_value(ptp, $code)?; let result = <$type>::try_from_ptp(&bytes)?; Ok(result) } #[allow(dead_code)] fn [](&self, ptp: &mut Ptp, value: &$type) -> anyhow::Result<()> { let bytes = value.try_into_ptp()?; self.set_prop_value(ptp, $code, &bytes)?; Ok(()) } } )+ }; } pub trait CameraImpl { fn supported_camera(&self) -> &'static SupportedCamera

; fn timeout(&self) -> Duration { Duration::default() } fn chunk_size(&self) -> usize { 1024 * 1024 } fn open_session(&self, ptp: &mut Ptp, session_id: u32) -> anyhow::Result<()> { debug!("Sending OpenSession command"); _ = ptp.send( CommandCode::OpenSession, &[session_id], None, self.timeout(), )?; Ok(()) } fn close_session(&self, ptp: &mut Ptp, _: u32) -> anyhow::Result<()> { debug!("Sending CloseSession command"); _ = ptp.send(CommandCode::CloseSession, &[], None, self.timeout())?; Ok(()) } fn get_info(&self, ptp: &mut Ptp) -> anyhow::Result { debug!("Sending GetDeviceInfo command"); let response = ptp.send(CommandCode::GetDeviceInfo, &[], None, self.timeout())?; debug!("Received response with {} bytes", response.len()); let info = DeviceInfo::try_from_ptp(&response)?; Ok(info) } fn get_prop_value(&self, ptp: &mut Ptp, prop: DevicePropCode) -> anyhow::Result> { debug!("Sending GetDevicePropValue command for property {prop:?}"); let response = ptp.send( CommandCode::GetDevicePropValue, &[prop.into()], None, self.timeout(), )?; debug!("Received response with {} bytes", response.len()); Ok(response) } fn set_prop_value( &self, ptp: &mut Ptp, prop: DevicePropCode, value: &[u8], ) -> anyhow::Result> { debug!("Sending GetDevicePropValue command for property {prop:?}"); let response = ptp.send( CommandCode::SetDevicePropValue, &[prop.into()], Some(value), self.timeout(), )?; debug!("Received response with {} bytes", response.len()); Ok(response) } fn export_backup(&self, ptp: &mut Ptp) -> anyhow::Result> { const HANDLE: u32 = 0x0; debug!("Sending GetObjectInfo command for backup"); let response = ptp.send(CommandCode::GetObjectInfo, &[HANDLE], None, self.timeout())?; debug!("Received response with {} bytes", response.len()); debug!("Sending GetObject command for backup"); let response = ptp.send(CommandCode::GetObject, &[HANDLE], None, self.timeout())?; debug!("Received response with {} bytes", response.len()); Ok(response) } fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> { debug!("Preparing ObjectInfo header for backup"); let mut header = Vec::with_capacity(1076); 0x0u32.try_write_ptp(&mut header)?; 0x5000u16.try_write_ptp(&mut header)?; 0x0u16.try_write_ptp(&mut header)?; u32::try_from(buffer.len())?.try_write_ptp(&mut header)?; for _ in 0..1064 { 0x0u8.try_write_ptp(&mut header)?; } debug!("Sending SendObjectInfo command for backup"); let response = ptp.send( CommandCode::SendObjectInfo, &[0x0, 0x0], Some(&header), self.timeout(), )?; debug!("Received response with {} bytes", response.len()); debug!("Sending SendObject command for backup"); let response = ptp.send( CommandCode::SendObject, &[0x0], Some(buffer), self.timeout(), )?; debug!("Received response with {} bytes", response.len()); Ok(()) } camera_impl_custom_settings! { active_custom_setting: FujiCustomSetting => DevicePropCode::FujiStillCustomSetting, custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiStillCustomSettingName, image_size: FujiImageSize => DevicePropCode::FujiStillCustomSettingImageSize, image_quality: FujiImageQuality => DevicePropCode::FujiStillCustomSettingImageQuality, dynamic_range: FujiDynamicRange => DevicePropCode::FujiStillCustomSettingDynamicRange, dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiStillCustomSettingDynamicRangePriority, film_simulation: FujiFilmSimulation => DevicePropCode::FujiStillCustomSettingFilmSimulation, grain_effect: FujiGrainEffect => DevicePropCode::FujiStillCustomSettingGrainEffect, white_balance: FujiWhiteBalance => DevicePropCode::FujiStillCustomSettingWhiteBalance, high_iso_nr: FujiHighISONR => DevicePropCode::FujiStillCustomSettingHighISONR, highlight_tone: FujiHighlightTone => DevicePropCode::FujiStillCustomSettingHighlightTone, shadow_tone: FujiShadowTone => DevicePropCode::FujiStillCustomSettingShadowTone, color: FujiColor => DevicePropCode::FujiStillCustomSettingColor, sharpness: FujiSharpness => DevicePropCode::FujiStillCustomSettingSharpness, clarity: FujiClarity => DevicePropCode::FujiStillCustomSettingClarity, white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiStillCustomSettingWhiteBalanceShiftRed, white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiStillCustomSettingWhiteBalanceShiftBlue, white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiStillCustomSettingWhiteBalanceTemperature, color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiStillCustomSettingColorChromeEffect, color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiStillCustomSettingColorChromeFXBlue, smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiStillCustomSettingSmoothSkinEffect, } }