diff --git a/Cargo.lock b/Cargo.lock index 20372bd..e5dac52 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -210,6 +210,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "erased-serde" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b" +dependencies = [ + "serde", + "serde_core", + "typeid", +] + [[package]] name = "errno" version = "0.3.14" @@ -239,6 +250,7 @@ dependencies = [ "anyhow", "byteorder", "clap", + "erased-serde", "log", "log4rs", "num_enum", @@ -840,6 +852,12 @@ dependencies = [ "winnow", ] +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typemap-ors" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 882d27b..4de6b3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,4 @@ ptp_cursor = { path = "crates/ptp/cursor" } strum = { version = "0.27.2", features = ["strum_macros"] } strum_macros = "0.27.2" paste = "1.0.15" +erased-serde = "0.4.8" diff --git a/src/camera/devices/mod.rs b/src/camera/devices/mod.rs index 7a80d30..9e53ea5 100644 --- a/src/camera/devices/mod.rs +++ b/src/camera/devices/mod.rs @@ -1,6 +1,11 @@ -use rusb::GlobalContext; +mod x_trans_v; -use super::CameraImpl; +use std::fmt; + +use rusb::GlobalContext; +use serde::Serialize; + +use super::{Camera, CameraImpl}; type ImplFactory

= fn() -> Box>; @@ -12,31 +17,34 @@ pub struct SupportedCamera { pub impl_factory: ImplFactory

, } -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 const SUPPORTED: &[SupportedCamera] = &[x_trans_v::x_t5::FUJIFILM_XT5]; - pub struct $struct_name {} - - impl crate::camera::CameraImpl for $struct_name { - fn supported_camera(&self) -> &'static SupportedCamera { - &$const_name - } - } - }; +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CameraInfoListItem { + pub name: &'static str, + pub usb_id: String, + pub vendor_id: String, + pub product_id: String, } -default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5"); +impl From<&Camera> for CameraInfoListItem { + fn from(camera: &Camera) -> Self { + Self { + 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()), + } + } +} -pub const SUPPORTED: &[SupportedCamera] = &[FUJIFILM_XT5]; +impl fmt::Display for CameraInfoListItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} ({}:{}) (USB ID: {})", + self.name, self.vendor_id, self.product_id, self.usb_id + ) + } +} diff --git a/src/camera/devices/x_trans_v/mod.rs b/src/camera/devices/x_trans_v/mod.rs new file mode 100644 index 0000000..e82aeaa --- /dev/null +++ b/src/camera/devices/x_trans_v/mod.rs @@ -0,0 +1,287 @@ +use std::{ + fmt, + io::{self, Write}, +}; + +use log::error; +use ptp_cursor::PtpSerialize; +use serde::Serialize; + +use crate::{ + camera::ptp::{ + hex::{ + FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace, + FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority, + FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, + FujiImageQuality, FujiImageSize, FujiLensModulationOptimizer, + FujiMonochromaticColorTemperature, FujiMonochromaticColorTint, FujiShadowTone, + FujiSharpness, FujiSmoothSkinEffect, FujiWhiteBalance, FujiWhiteBalanceShift, + FujiWhiteBalanceTemperature, ObjectFormat, UsbMode, + }, + structs::ObjectInfo, + }, + cli::common::film::FilmSimulationOptions, +}; + +pub mod x_t5; + +// TODO: Assuming that all cameras using the same sensor also have the same feature set. +// This might not actually be the case. + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CameraInfo { + pub manufacturer: String, + pub model: String, + pub device_version: String, + pub serial_number: String, + pub mode: UsbMode, + pub battery: u32, +} + +impl fmt::Display for CameraInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Manufacturer: {}", self.manufacturer)?; + writeln!(f, "Model: {}", self.model)?; + writeln!(f, "Version: {}", self.device_version)?; + writeln!(f, "Serial Number: {}", self.serial_number)?; + writeln!(f, "Mode: {}", self.mode)?; + write!(f, "Battery: {}%", self.battery) + } +} + +#[derive(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct SimulationListItem { + pub slot: FujiCustomSetting, + pub name: FujiCustomSettingName, +} + +impl fmt::Display for SimulationListItem { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}: {}", self.slot, self.name) + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct Simulation { + pub name: FujiCustomSettingName, + pub size: FujiImageSize, + pub quality: FujiImageQuality, + #[allow(clippy::struct_field_names)] + pub simulation: FujiFilmSimulation, + pub monochromatic_color_temperature: FujiMonochromaticColorTemperature, + pub monochromatic_color_tint: FujiMonochromaticColorTint, + pub highlight: FujiHighlightTone, + pub shadow: FujiShadowTone, + pub color: FujiColor, + pub sharpness: FujiSharpness, + pub clarity: FujiClarity, + pub noise_reduction: FujiHighISONR, + pub grain: FujiGrainEffect, + pub color_chrome_effect: FujiColorChromeEffect, + pub color_chrome_fx_blue: FujiColorChromeFXBlue, + pub smooth_skin_effect: FujiSmoothSkinEffect, + pub white_balance: FujiWhiteBalance, + pub white_balance_shift_red: FujiWhiteBalanceShift, + pub white_balance_shift_blue: FujiWhiteBalanceShift, + pub white_balance_temperature: FujiWhiteBalanceTemperature, + pub dynamic_range: FujiDynamicRange, + pub dynamic_range_priority: FujiDynamicRangePriority, + pub lens_modulation_optimizer: FujiLensModulationOptimizer, + pub color_space: FujiColorSpace, +} + +impl fmt::Display for Simulation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Name: {}", self.name)?; + writeln!(f, "Size: {}", self.size)?; + writeln!(f, "Quality: {}", self.quality)?; + + writeln!(f, "Simulation: {}", self.simulation)?; + + match self.simulation { + FujiFilmSimulation::Monochrome + | FujiFilmSimulation::MonochromeYe + | FujiFilmSimulation::MonochromeR + | FujiFilmSimulation::MonochromeG + | FujiFilmSimulation::AcrosSTD + | FujiFilmSimulation::AcrosYe + | FujiFilmSimulation::AcrosR + | FujiFilmSimulation::AcrosG => { + writeln!( + f, + "Monochromatic Color Temperature: {}", + self.monochromatic_color_temperature + )?; + writeln!( + f, + "Monochromatic Color Tint: {}", + self.monochromatic_color_tint + )?; + } + _ => {} + } + + if self.dynamic_range_priority == FujiDynamicRangePriority::Off { + writeln!(f, "Highlights: {}", self.highlight)?; + writeln!(f, "Shadows: {}", self.shadow)?; + } + + writeln!(f, "Color: {}", self.color)?; + writeln!(f, "Sharpness: {}", self.sharpness)?; + writeln!(f, "Clarity: {}", self.clarity)?; + writeln!(f, "Noise Reduction: {}", self.noise_reduction)?; + writeln!(f, "Grain: {}", self.grain)?; + writeln!(f, "Color Chrome Effect: {}", self.color_chrome_effect)?; + writeln!(f, "Color Chrome FX Blue: {}", self.color_chrome_fx_blue)?; + writeln!(f, "Smooth Skin Effect: {}", self.smooth_skin_effect)?; + + writeln!(f, "White Balance: {}", self.white_balance)?; + writeln!( + f, + "White Balance Shift (R/B): {} / {}", + self.white_balance_shift_red, self.white_balance_shift_blue + )?; + + if self.white_balance == FujiWhiteBalance::Temperature { + writeln!( + f, + "White Balance Temperature: {}K", + self.white_balance_temperature + )?; + } + + if self.dynamic_range_priority == FujiDynamicRangePriority::Off { + writeln!(f, "Dynamic Range: {}", self.dynamic_range)?; + } + + writeln!(f, "Dynamic Range Priority: {}", self.dynamic_range_priority)?; + + writeln!( + f, + "Lens Modulation Optimizer: {}", + self.lens_modulation_optimizer + )?; + writeln!(f, "Color Space: {}", self.color_space) + } +} + +pub struct FujiBackupObjectInfo { + compressed_size: u32, +} + +impl FujiBackupObjectInfo { + pub fn new(buffer_len: usize) -> anyhow::Result { + Ok(Self { + compressed_size: u32::try_from(buffer_len)?, + }) + } +} + +impl PtpSerialize for FujiBackupObjectInfo { + fn try_into_ptp(&self) -> io::Result> { + let mut buf = Vec::new(); + self.try_write_ptp(&mut buf)?; + Ok(buf) + } + + fn try_write_ptp(&self, buf: &mut Vec) -> io::Result<()> { + let object_info = ObjectInfo { + object_format: ObjectFormat::FujiBackup, + compressed_size: self.compressed_size, + ..Default::default() + }; + + object_info.try_write_ptp(buf)?; + + // TODO: What is this? + buf.write_all(&[0x0u8; 1020])?; + + Ok(()) + } +} + +pub trait XTransV { + fn validate_monochromatic( + final_options: &FilmSimulationOptions, + prev_simulation: FujiFilmSimulation, + ) -> Result { + let mut fail = true; + + if !matches!( + prev_simulation, + FujiFilmSimulation::Monochrome + | FujiFilmSimulation::MonochromeYe + | FujiFilmSimulation::MonochromeR + | FujiFilmSimulation::MonochromeG + | FujiFilmSimulation::AcrosSTD + | FujiFilmSimulation::AcrosYe + | FujiFilmSimulation::AcrosR + | FujiFilmSimulation::AcrosG + ) && (final_options.monochromatic_color_temperature.is_some() + || final_options.monochromatic_color_tint.is_some()) + { + if final_options.monochromatic_color_temperature.is_some() { + error!( + "A B&W film simulation is not selected, refusing to set monochromatic color temperature" + ); + fail = false; + } + + if final_options.monochromatic_color_tint.is_some() { + error!( + "A B&W film simulation is not selected, refusing to set monochromatic color tint" + ); + fail = false; + } + } + + Ok(fail) + } + + fn validate_white_balance_temperature( + final_options: &FilmSimulationOptions, + prev_white_balance: FujiWhiteBalance, + ) -> Result { + if prev_white_balance != FujiWhiteBalance::Temperature + && final_options.white_balance_temperature.is_some() + { + error!("White Balance mode is not set to 'Temperature', refusing to set temperature"); + Ok(false) + } else { + Ok(true) + } + } + + fn validate_exposure( + final_options: &FilmSimulationOptions, + previous_dynamic_range_priority: FujiDynamicRangePriority, + ) -> Result { + let mut fail = true; + + if previous_dynamic_range_priority != FujiDynamicRangePriority::Off + && (final_options.dynamic_range.is_some() + || final_options.highlight.is_some() + || final_options.shadow.is_some()) + { + if final_options.dynamic_range.is_some() { + error!("Dynamic Range Priority is enabled, refusing to set dynamic range"); + fail = false; + } + + if final_options.highlight.is_some() { + error!("Dynamic Range Priority is enabled, refusing to set highlight tone"); + fail = false; + } + + if final_options.shadow.is_some() { + error!("Dynamic Range Priority is enabled, refusing to set shadow tone"); + fail = false; + } + } + + Ok(fail) + } +} diff --git a/src/camera/devices/x_trans_v/x_t5/mod.rs b/src/camera/devices/x_trans_v/x_t5/mod.rs new file mode 100644 index 0000000..5a0aac9 --- /dev/null +++ b/src/camera/devices/x_trans_v/x_t5/mod.rs @@ -0,0 +1,308 @@ +use anyhow::{Ok, anyhow, bail}; +use log::debug; +use ptp_cursor::{PtpDeserialize, PtpSerialize}; +use rusb::GlobalContext; +use strum::IntoEnumIterator; + +use crate::{ + camera::{ + CameraImpl, CameraResult, camera_prop_getter, camera_prop_setter, camera_set_prop_if_some, + devices::{SupportedCamera, x_trans_v::FujiBackupObjectInfo}, + ptp::{ + Ptp, + hex::{ + CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect, + FujiColorChromeFXBlue, FujiColorSpace, FujiCustomSetting, FujiCustomSettingName, + FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect, + FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize, + FujiLensModulationOptimizer, FujiMonochromaticColorTemperature, + FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect, + FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode, + }, + }, + }, + cli::{common::film::FilmSimulationOptions, simulation::SetFilmSimulationOptions}, +}; + +use super::{CameraInfo, Simulation, SimulationListItem, XTransV}; + +pub const FUJIFILM_XT5: SupportedCamera = SupportedCamera { + name: "FUJIFILM XT-5", + vendor: 0x04cb, + product: 0x02fc, + impl_factory: || Box::new(FujifilmXT5 {}), +}; + +pub struct FujifilmXT5 {} + +impl XTransV for FujifilmXT5 {} + +impl FujifilmXT5 { + camera_prop_getter!(get_usb_mode: UsbMode => DevicePropCode::FujiUsbMode); + + fn get_battery_info(&self, ptp: &mut Ptp) -> anyhow::Result { + let data = ptp.get_prop_value(DevicePropCode::FujiBatteryInfo2, self.timeout())?; + + 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) + } + + camera_prop_getter!(get_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName); + camera_prop_getter!(get_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize); + camera_prop_getter!(get_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality); + camera_prop_getter!(get_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange); + camera_prop_getter!(get_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority); + camera_prop_getter!(get_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation); + camera_prop_getter!(get_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature); + camera_prop_getter!(get_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint); + camera_prop_getter!(get_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect); + camera_prop_getter!(get_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance); + camera_prop_getter!(get_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR); + camera_prop_getter!(get_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone); + camera_prop_getter!(get_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone); + camera_prop_getter!(get_color: FujiColor => DevicePropCode::FujiCustomSettingColor); + camera_prop_getter!(get_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness); + camera_prop_getter!(get_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity); + camera_prop_getter!(get_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed); + camera_prop_getter!(get_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue); + camera_prop_getter!(get_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature); + camera_prop_getter!(get_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect); + camera_prop_getter!(get_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue); + camera_prop_getter!(get_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect); + camera_prop_getter!(get_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer); + camera_prop_getter!(get_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace); + + camera_prop_setter!(set_active_custom_setting: FujiCustomSetting => DevicePropCode::FujiCustomSetting); + camera_prop_setter!(set_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName); + camera_prop_setter!(set_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize); + camera_prop_setter!(set_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality); + camera_prop_setter!(set_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange); + camera_prop_setter!(set_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority); + camera_prop_setter!(set_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation); + camera_prop_setter!(set_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature); + camera_prop_setter!(set_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint); + camera_prop_setter!(set_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect); + camera_prop_setter!(set_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance); + camera_prop_setter!(set_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR); + camera_prop_setter!(set_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone); + camera_prop_setter!(set_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone); + camera_prop_setter!(set_color: FujiColor => DevicePropCode::FujiCustomSettingColor); + camera_prop_setter!(set_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness); + camera_prop_setter!(set_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity); + camera_prop_setter!(set_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed); + camera_prop_setter!(set_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue); + camera_prop_setter!(set_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature); + camera_prop_setter!(set_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect); + camera_prop_setter!(set_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue); + camera_prop_setter!(set_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect); + camera_prop_setter!(set_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer); + camera_prop_setter!(set_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace); + + fn validate_simulation_set( + &self, + ptp: &mut Ptp, + options: &FilmSimulationOptions, + ) -> Result<(), anyhow::Error> { + let prev_simulation = if let Some(simulation) = options.simulation { + simulation + } else { + self.get_film_simulation(ptp)? + }; + + let prev_white_balance = if let Some(white_balance) = options.white_balance { + white_balance + } else { + self.get_white_balance(ptp)? + }; + + let prev_dynamic_range_priority = + if let Some(dynamic_range_priority) = options.dynamic_range_priority { + dynamic_range_priority + } else { + self.get_dynamic_range_priority(ptp)? + }; + + if !Self::validate_monochromatic(options, prev_simulation)? + || !Self::validate_white_balance_temperature(options, prev_white_balance)? + || !Self::validate_exposure(options, prev_dynamic_range_priority)? + { + bail!("Incompatible options detected") + } + + Ok(()) + } +} + +impl CameraImpl for FujifilmXT5 { + fn supported_camera(&self) -> &'static SupportedCamera { + &FUJIFILM_XT5 + } + + fn chunk_size(&self) -> usize { + 16128 * 1024 + } + + fn info_get(&self, ptp: &mut Ptp) -> anyhow::Result> { + let info = ptp.get_info(self.timeout())?; + + let mode = self.get_usb_mode(ptp)?; + let battery = self.get_battery_info(ptp)?; + + let repr = CameraInfo { + manufacturer: info.manufacturer.clone(), + model: info.model.clone(), + device_version: info.device_version.clone(), + serial_number: info.serial_number, + mode, + battery, + }; + + let repr = Box::new(repr); + + Ok(repr) + } + + fn simulation_list(&self, ptp: &mut Ptp) -> anyhow::Result>> { + let mut slots = Vec::new(); + + for slot in FujiCustomSetting::iter() { + self.set_active_custom_setting(ptp, &slot)?; + let name = self.get_custom_setting_name(ptp)?; + let repr = SimulationListItem { slot, name }; + let repr: Box = Box::new(repr); + slots.push(repr); + } + + Ok(slots) + } + + fn simulation_get( + &self, + ptp: &mut Ptp, + slot: FujiCustomSetting, + ) -> anyhow::Result> { + self.set_active_custom_setting(ptp, &slot)?; + + let repr = Simulation { + name: self.get_custom_setting_name(ptp)?, + size: self.get_image_size(ptp)?, + quality: self.get_image_quality(ptp)?, + simulation: self.get_film_simulation(ptp)?, + monochromatic_color_temperature: self.get_monochromatic_color_temperature(ptp)?, + monochromatic_color_tint: self.get_monochromatic_color_tint(ptp)?, + highlight: self.get_highlight_tone(ptp)?, + shadow: self.get_shadow_tone(ptp)?, + color: self.get_color(ptp)?, + sharpness: self.get_sharpness(ptp)?, + clarity: self.get_clarity(ptp)?, + noise_reduction: self.get_high_iso_nr(ptp)?, + grain: self.get_grain_effect(ptp)?, + color_chrome_effect: self.get_color_chrome_effect(ptp)?, + color_chrome_fx_blue: self.get_color_chrome_fx_blue(ptp)?, + smooth_skin_effect: self.get_smooth_skin_effect(ptp)?, + white_balance: self.get_white_balance(ptp)?, + white_balance_shift_red: self.get_white_balance_shift_red(ptp)?, + white_balance_shift_blue: self.get_white_balance_shift_blue(ptp)?, + white_balance_temperature: self.get_white_balance_temperature(ptp)?, + dynamic_range: self.get_dynamic_range(ptp)?, + dynamic_range_priority: self.get_dynamic_range_priority(ptp)?, + lens_modulation_optimizer: self.get_lens_modulation_optimizer(ptp)?, + color_space: self.get_color_space(ptp)?, + }; + + let repr = Box::new(repr); + + Ok(repr) + } + + fn simulation_set( + &self, + ptp: &mut Ptp, + slot: FujiCustomSetting, + set_options: &SetFilmSimulationOptions, + options: &FilmSimulationOptions, + ) -> anyhow::Result<()> { + self.set_active_custom_setting(ptp, &slot)?; + + self.validate_simulation_set(ptp, options)?; + + camera_set_prop_if_some!(self, ptp, set_options, + name => set_custom_setting_name, + ); + + camera_set_prop_if_some!(self, ptp, options, + size => set_image_size, + quality => set_image_quality, + simulation => set_film_simulation, + monochromatic_color_temperature => set_monochromatic_color_temperature, + monochromatic_color_tint => set_monochromatic_color_tint, + color => set_color, + sharpness => set_sharpness, + clarity => set_clarity, + noise_reduction => set_high_iso_nr, + grain => set_grain_effect, + color_chrome_effect => set_color_chrome_effect, + color_chrome_fx_blue => set_color_chrome_fx_blue, + smooth_skin_effect => set_smooth_skin_effect, + white_balance => set_white_balance, + white_balance_temperature => set_white_balance_temperature, + white_balance_shift_red => set_white_balance_shift_red, + white_balance_shift_blue => set_white_balance_shift_blue, + dynamic_range_priority => set_dynamic_range_priority, + dynamic_range => set_dynamic_range, + highlight => set_highlight_tone, + shadow => set_shadow_tone, + lens_modulation_optimizer => set_lens_modulation_optimizer, + color_space => set_color_space, + ); + + Ok(()) + } + + fn backup_export(&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 backup_import(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> { + debug!("Sending SendObjectInfo command for backup"); + let object_info = FujiBackupObjectInfo::new(buffer.len())?; + let response = ptp.send( + CommandCode::SendObjectInfo, + &[0x0, 0x0], + Some(&object_info.try_into_ptp()?), + 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(()) + } +} diff --git a/src/camera/mod.rs b/src/camera/mod.rs index 42b60e1..ba7dce4 100644 --- a/src/camera/mod.rs +++ b/src/camera/mod.rs @@ -2,29 +2,19 @@ pub mod devices; pub mod error; pub mod ptp; -use std::time::Duration; +use std::{fmt, time::Duration}; -use anyhow::{anyhow, bail}; -use devices::SupportedCamera; -use log::{debug, error}; -use ptp::{ - Ptp, - hex::{ - CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect, - FujiColorChromeFXBlue, FujiColorSpace, FujiCustomSetting, FujiCustomSettingName, - FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect, - FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize, - FujiLensModulationOptimizer, FujiMonochromaticColorTemperature, FujiMonochromaticColorTint, - FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect, FujiWhiteBalance, - FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, ObjectFormat, UsbMode, - }, - structs::{DeviceInfo, ObjectInfo}, +use crate::{ + cli::common::film::FilmSimulationOptions, cli::simulation::SetFilmSimulationOptions, + usb::find_endpoint, }; -use ptp_cursor::{PtpDeserialize, PtpSerialize}; +use anyhow::bail; +use devices::SupportedCamera; +use erased_serde::serialize_trait_object; +use log::{debug, error}; +use ptp::{Ptp, hex::FujiCustomSetting}; use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE}; -use crate::usb::find_endpoint; - const SESSION: u32 = 1; pub struct Camera { @@ -32,25 +22,23 @@ pub struct Camera { pub ptp: Ptp, } -macro_rules! camera_with_ptp { - ($($fn_name:ident => $ret:ty),* $(,)?) => { - $( - #[allow(dead_code)] - pub fn $fn_name(&mut self) -> anyhow::Result<$ret> { - self.r#impl.$fn_name(&mut self.ptp) - } - )* +macro_rules! camera_to_impl_with_ptp { + ($name:ident -> $ret:ty) => { + pub fn $name(&mut self) -> $ret { + self.r#impl.$name(&mut self.ptp) + } }; - ($($fn_name:ident($($arg:ident : $arg_ty:ty),*) => $ret:ty),* $(,)?) => { - $( - #[allow(dead_code)] - pub fn $fn_name(&mut self, $($arg: $arg_ty),*) -> anyhow::Result<$ret> { - self.r#impl.$fn_name(&mut self.ptp, $($arg),*) - } - )* + ($name:ident($($arg:ident: $arg_ty:ty),*) -> $ret:ty) => { + pub fn $name(&mut self, $($arg: $arg_ty),*) -> $ret { + self.r#impl.$name(&mut self.ptp, $($arg),*) + } }; } +pub trait CameraResult: fmt::Display + erased_serde::Serialize {} +impl CameraResult for T {} +serialize_trait_object!(CameraResult); + impl Camera { pub fn name(&self) -> &'static str { self.r#impl.supported_camera().name @@ -68,69 +56,12 @@ impl Camera { format!("{}.{}", self.ptp.bus, self.ptp.address) } - camera_with_ptp! { - get_info => DeviceInfo, - get_usb_mode => UsbMode, - get_battery_info => u32, - } - - camera_with_ptp! { - export_backup => Vec, - get_active_custom_setting => FujiCustomSetting, - get_custom_setting_name => FujiCustomSettingName, - get_image_size => FujiImageSize, - get_image_quality => FujiImageQuality, - get_dynamic_range => FujiDynamicRange, - get_dynamic_range_priority => FujiDynamicRangePriority, - get_film_simulation => FujiFilmSimulation, - get_monochromatic_color_temperature => FujiMonochromaticColorTemperature, - get_monochromatic_color_tint => FujiMonochromaticColorTint, - get_grain_effect => FujiGrainEffect, - get_white_balance => FujiWhiteBalance, - get_high_iso_nr => FujiHighISONR, - get_highlight_tone => FujiHighlightTone, - get_shadow_tone => FujiShadowTone, - get_color => FujiColor, - get_sharpness => FujiSharpness, - get_clarity => FujiClarity, - get_white_balance_shift_red => FujiWhiteBalanceShift, - get_white_balance_shift_blue => FujiWhiteBalanceShift, - get_white_balance_temperature => FujiWhiteBalanceTemperature, - get_color_chrome_effect => FujiColorChromeEffect, - get_color_chrome_fx_blue => FujiColorChromeFXBlue, - get_smooth_skin_effect => FujiSmoothSkinEffect, - get_lens_modulation_optimizer => FujiLensModulationOptimizer, - get_color_space => FujiColorSpace, - } - - camera_with_ptp! { - import_backup(buffer: &[u8]) => (), - set_active_custom_setting(value: &FujiCustomSetting) => (), - set_custom_setting_name(value: &FujiCustomSettingName) => (), - set_image_size(value: &FujiImageSize) => (), - set_image_quality(value: &FujiImageQuality) => (), - set_dynamic_range(value: &FujiDynamicRange) => (), - set_dynamic_range_priority(value: &FujiDynamicRangePriority) => (), - set_film_simulation(value: &FujiFilmSimulation) => (), - set_monochromatic_color_temperature(value: &FujiMonochromaticColorTemperature) => (), - set_monochromatic_color_tint(value: &FujiMonochromaticColorTint) => (), - set_grain_effect(value: &FujiGrainEffect) => (), - set_white_balance(value: &FujiWhiteBalance) => (), - set_high_iso_nr(value: &FujiHighISONR) => (), - set_highlight_tone(value: &FujiHighlightTone) => (), - set_shadow_tone(value: &FujiShadowTone) => (), - set_color(value: &FujiColor) => (), - set_sharpness(value: &FujiSharpness) => (), - set_clarity(value: &FujiClarity) => (), - set_white_balance_shift_red(value: &FujiWhiteBalanceShift) => (), - set_white_balance_shift_blue(value: &FujiWhiteBalanceShift) => (), - set_white_balance_temperature(value: &FujiWhiteBalanceTemperature) => (), - set_color_chrome_effect(value: &FujiColorChromeEffect) => (), - set_color_chrome_fx_blue(value: &FujiColorChromeFXBlue) => (), - set_smooth_skin_effect(value: &FujiSmoothSkinEffect) => (), - set_lens_modulation_optimizer(value: &FujiLensModulationOptimizer) => (), - set_color_space(value: &FujiColorSpace) => (), - } + camera_to_impl_with_ptp!(info_get() -> anyhow::Result>); + camera_to_impl_with_ptp!(backup_export -> anyhow::Result>); + camera_to_impl_with_ptp!(backup_import(buffer: &[u8]) -> anyhow::Result<()>); + camera_to_impl_with_ptp!(simulation_list -> anyhow::Result>>); + camera_to_impl_with_ptp!(simulation_get(slot: FujiCustomSetting) -> anyhow::Result>); + camera_to_impl_with_ptp!(simulation_set(slot: FujiCustomSetting, set_options: &SetFilmSimulationOptions, options: &FilmSimulationOptions) -> anyhow::Result<()>); } impl Drop for Camera { @@ -213,28 +144,6 @@ impl TryFrom<&rusb::Device> for Camera { } } -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 = ptp.get_prop_value($code, self.timeout())?; - 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()?; - ptp.set_prop_value($code, &bytes, self.timeout())?; - Ok(()) - } - } - )+ - }; -} - pub trait CameraImpl { fn supported_camera(&self) -> &'static SupportedCamera

; @@ -247,110 +156,80 @@ pub trait CameraImpl { 1024 * 1024 } - fn get_info(&mut self, ptp: &mut Ptp) -> anyhow::Result { - let info = ptp.get_info(self.timeout())?; - Ok(info) + fn info_get(&self, _ptp: &mut Ptp) -> anyhow::Result> { + bail!("This device does not support getting detailed info") } - fn get_usb_mode(&mut self, ptp: &mut Ptp) -> anyhow::Result { - let data = ptp.get_prop_value(DevicePropCode::FujiUsbMode, self.timeout())?; - let result = UsbMode::try_from_ptp(&data)?; - Ok(result) + fn backup_export(&self, _ptp: &mut Ptp) -> anyhow::Result> { + bail!("This device does not support exporting backups") } - fn get_battery_info(&mut self, ptp: &mut Ptp) -> anyhow::Result { - let data = ptp.get_prop_value(DevicePropCode::FujiBatteryInfo2, self.timeout())?; - - 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) + fn backup_import(&self, _ptp: &mut Ptp, _buffer: &[u8]) -> anyhow::Result<()> { + bail!("This device does not support importing backups") } - 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 simulation_list(&self, _ptp: &mut Ptp) -> anyhow::Result>> { + bail!("This device does not support listing simulations") } - fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> { - debug!("Preparing ObjectInfo header for backup"); - - let mut header = Vec::with_capacity(1076); - - let object_info = ObjectInfo { - object_format: ObjectFormat::FujiBackup, - compressed_size: u32::try_from(buffer.len())?, - ..Default::default() - }; - object_info.try_write_ptp(&mut header)?; - // TODO: What is this? - for _ in 0..1020 { - 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(()) + fn simulation_get( + &self, + _ptp: &mut Ptp, + _slot: FujiCustomSetting, + ) -> anyhow::Result> { + bail!("This device does not support getting simulation options") } - camera_impl_custom_settings! { - active_custom_setting: FujiCustomSetting => DevicePropCode::FujiCustomSetting, - custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName, - image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize, - image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality, - dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange, - dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority, - film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation, - monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature, - monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint, - grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect, - white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance, - high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR, - highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone, - shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone, - color: FujiColor => DevicePropCode::FujiCustomSettingColor, - sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness, - clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity, - white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed, - white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue, - white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature, - color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect, - color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue, - smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect, - lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer, - color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace, + fn simulation_set( + &self, + _ptp: &mut Ptp, + _slot: FujiCustomSetting, + _set_options: &SetFilmSimulationOptions, + _options: &FilmSimulationOptions, + ) -> anyhow::Result<()> { + bail!("This device does not support setting simulation options") } } + +macro_rules! camera_prop_getter { + ($name:ident: $type:ty => $code:expr) => { + pub fn $name(&self, ptp: &mut crate::camera::ptp::Ptp) -> anyhow::Result<$type> { + use ptp_cursor::PtpDeserialize; + + let bytes = ptp.get_prop_value($code, self.timeout())?; + let result = <$type>::try_from_ptp(&bytes)?; + Ok(result) + } + }; +} + +macro_rules! camera_prop_setter { + ($name:ident: $type:ty => $code:expr) => { + pub fn $name( + &self, + ptp: &mut crate::camera::ptp::Ptp, + value: &$type, + ) -> anyhow::Result<()> { + use ptp_cursor::PtpSerialize; + + let bytes = value.try_into_ptp()?; + ptp.set_prop_value($code, &bytes, self.timeout())?; + Ok(()) + } + }; +} + +macro_rules! camera_set_prop_if_some { + ($self:ident, $ptp:ident, $options:ident, + $( $field:ident => $setter:ident ),* $(,)? ) => { + $( + if let Some(val) = &$options.$field { + $self.$setter($ptp, val)?; + } + )* + }; +} + +pub(crate) use camera_prop_getter; +pub(crate) use camera_prop_setter; +pub(crate) use camera_set_prop_if_some; diff --git a/src/camera/ptp/structs.rs b/src/camera/ptp/structs.rs index 8bc3f24..72df8fd 100644 --- a/src/camera/ptp/structs.rs +++ b/src/camera/ptp/structs.rs @@ -2,7 +2,6 @@ use ptp_macro::{PtpDeserialize, PtpSerialize}; use super::hex::{CommandCode, ContainerCode, ContainerType, ObjectFormat}; -#[allow(dead_code)] #[derive(Debug, PtpSerialize, PtpDeserialize)] pub struct DeviceInfo { pub version: u16, diff --git a/src/cli/backup/mod.rs b/src/cli/backup/mod.rs index 9511498..fa912eb 100644 --- a/src/cli/backup/mod.rs +++ b/src/cli/backup/mod.rs @@ -24,7 +24,7 @@ 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()?; + let backup = camera.backup_export()?; writer.write_all(&backup)?; Ok(()) @@ -36,7 +36,7 @@ fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> { let mut reader = input.get_reader()?; let mut backup = Vec::new(); reader.read_to_end(&mut backup)?; - camera.import_backup(&backup)?; + camera.backup_import(&backup)?; Ok(()) } diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs index e278fd9..1ab5556 100644 --- a/src/cli/device/mod.rs +++ b/src/cli/device/mod.rs @@ -1,12 +1,6 @@ -use std::fmt; - use clap::Subcommand; -use serde::Serialize; -use crate::{ - camera::{Camera, ptp::hex::UsbMode}, - usb, -}; +use crate::{camera::devices::CameraInfoListItem, usb}; #[derive(Subcommand, Debug, Clone, Copy)] pub enum DeviceCmd { @@ -19,38 +13,8 @@ pub enum DeviceCmd { Info, } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CameraItemRepr { - pub name: &'static str, - pub usb_id: String, - pub vendor_id: String, - pub product_id: String, -} - -impl From<&Camera> for CameraItemRepr { - fn from(camera: &Camera) -> Self { - Self { - 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()), - } - } -} - -impl fmt::Display for CameraItemRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} ({}:{}) (USB ID: {})", - self.name, self.vendor_id, self.product_id, self.usb_id - ) - } -} - fn handle_list(json: bool) -> anyhow::Result<()> { - let cameras: Vec = usb::get_connected_cameras()? + let cameras: Vec = usb::get_connected_cameras()? .iter() .map(std::convert::Into::into) .collect(); @@ -65,7 +29,6 @@ fn handle_list(json: bool) -> anyhow::Result<()> { return Ok(()); } - println!("Connected Cameras:"); for d in cameras { println!("- {d}"); } @@ -73,54 +36,10 @@ fn handle_list(json: bool) -> anyhow::Result<()> { Ok(()) } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CameraRepr { - #[serde(flatten)] - pub device: CameraItemRepr, - - pub manufacturer: String, - pub model: String, - pub device_version: String, - pub serial_number: String, - pub mode: UsbMode, - pub battery: u32, -} - -impl fmt::Display for CameraRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Name: {}", self.device.name)?; - writeln!(f, "USB ID: {}", self.device.usb_id)?; - writeln!( - f, - "Vendor ID: {}, Product ID: {}", - self.device.vendor_id, self.device.product_id - )?; - writeln!(f, "Manufacturer: {}", self.manufacturer)?; - writeln!(f, "Model: {}", self.model)?; - writeln!(f, "Version: {}", self.device_version)?; - writeln!(f, "Serial Number: {}", self.serial_number)?; - writeln!(f, "Mode: {}", self.mode)?; - write!(f, "Battery: {}%", self.battery) - } -} - fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> { let mut camera = usb::get_camera(device_id)?; - let info = camera.get_info()?; - let mode = camera.get_usb_mode()?; - let battery = camera.get_battery_info()?; - - let repr = CameraRepr { - device: (&camera).into(), - manufacturer: info.manufacturer.clone(), - model: info.model.clone(), - device_version: info.device_version.clone(), - serial_number: info.serial_number, - mode, - battery, - }; + let repr = camera.info_get()?; if json { println!("{}", serde_json::to_string_pretty(&repr)?); diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 503fb80..8d297dd 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,6 +1,5 @@ -mod common; - pub mod backup; +pub mod common; pub mod device; pub mod render; pub mod simulation; diff --git a/src/cli/simulation/mod.rs b/src/cli/simulation/mod.rs index d9319cd..0e705cc 100644 --- a/src/cli/simulation/mod.rs +++ b/src/cli/simulation/mod.rs @@ -1,14 +1,5 @@ -use std::fmt; - use crate::{ - camera::ptp::hex::{ - FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace, - FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority, - FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, - FujiImageSize, FujiLensModulationOptimizer, FujiMonochromaticColorTemperature, - FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect, - FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, - }, + camera::ptp::hex::{FujiCustomSetting, FujiCustomSettingName}, usb, }; @@ -17,9 +8,6 @@ use super::common::{ film::FilmSimulationOptions, }; use clap::{Args, Subcommand}; -use log::warn; -use serde::Serialize; -use strum::IntoEnumIterator; #[derive(Subcommand, Debug)] pub enum SimulationCmd { @@ -72,173 +60,27 @@ pub enum SimulationCmd { pub struct SetFilmSimulationOptions { /// The name of the slot #[clap(long)] - name: Option, -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CustomSettingRepr { - pub slot: FujiCustomSetting, - pub name: FujiCustomSettingName, + pub name: Option, } fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> { let mut camera = usb::get_camera(device_id)?; - - let mut slots = Vec::new(); - - for slot in FujiCustomSetting::iter() { - camera.set_active_custom_setting(&slot)?; - let name = camera.get_custom_setting_name()?; - slots.push(CustomSettingRepr { slot, name }); - } + let slots = camera.simulation_list()?; if json { println!("{}", serde_json::to_string_pretty(&slots)?); } else { - println!("Film Simulations:"); - for slot in slots { - println!("- {}: {}", slot.slot, slot.name); + for repr in slots { + println!("- {repr}"); } } Ok(()) } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -pub struct FilmSimulationRepr { - pub name: FujiCustomSettingName, - pub size: FujiImageSize, - pub quality: FujiImageQuality, - pub simulation: FujiFilmSimulation, - pub monochromatic_color_temperature: FujiMonochromaticColorTemperature, - pub monochromatic_color_tint: FujiMonochromaticColorTint, - pub highlight: FujiHighlightTone, - pub shadow: FujiShadowTone, - pub color: FujiColor, - pub sharpness: FujiSharpness, - pub clarity: FujiClarity, - pub noise_reduction: FujiHighISONR, - pub grain: FujiGrainEffect, - pub color_chrome_effect: FujiColorChromeEffect, - pub color_chrome_fx_blue: FujiColorChromeFXBlue, - pub smooth_skin_effect: FujiSmoothSkinEffect, - pub white_balance: FujiWhiteBalance, - pub white_balance_shift_red: FujiWhiteBalanceShift, - pub white_balance_shift_blue: FujiWhiteBalanceShift, - pub white_balance_temperature: FujiWhiteBalanceTemperature, - pub dynamic_range: FujiDynamicRange, - pub dynamic_range_priority: FujiDynamicRangePriority, - pub lens_modulation_optimizer: FujiLensModulationOptimizer, - pub color_space: FujiColorSpace, -} - -impl fmt::Display for FilmSimulationRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Name: {}", self.name)?; - writeln!(f, "Size: {}", self.size)?; - writeln!(f, "Quality: {}", self.quality)?; - - writeln!(f, "Simulation: {}", self.simulation)?; - - match self.simulation { - FujiFilmSimulation::Monochrome - | FujiFilmSimulation::MonochromeYe - | FujiFilmSimulation::MonochromeR - | FujiFilmSimulation::MonochromeG - | FujiFilmSimulation::AcrosSTD - | FujiFilmSimulation::AcrosYe - | FujiFilmSimulation::AcrosR - | FujiFilmSimulation::AcrosG => { - writeln!( - f, - "Monochromatic Color Temperature: {}", - self.monochromatic_color_temperature - )?; - writeln!( - f, - "Monochromatic Color Tint: {}", - self.monochromatic_color_tint - )?; - } - _ => {} - } - - if self.dynamic_range_priority == FujiDynamicRangePriority::Off { - writeln!(f, "Highlights: {}", self.highlight)?; - writeln!(f, "Shadows: {}", self.shadow)?; - } - - writeln!(f, "Color: {}", self.color)?; - writeln!(f, "Sharpness: {}", self.sharpness)?; - writeln!(f, "Clarity: {}", self.clarity)?; - writeln!(f, "Noise Reduction: {}", self.noise_reduction)?; - writeln!(f, "Grain: {}", self.grain)?; - writeln!(f, "Color Chrome Effect: {}", self.color_chrome_effect)?; - writeln!(f, "Color Chrome FX Blue: {}", self.color_chrome_fx_blue)?; - writeln!(f, "Smooth Skin Effect: {}", self.smooth_skin_effect)?; - - writeln!(f, "White Balance: {}", self.white_balance)?; - writeln!( - f, - "White Balance Shift (R/B): {} / {}", - self.white_balance_shift_red, self.white_balance_shift_blue - )?; - - if self.white_balance == FujiWhiteBalance::Temperature { - writeln!( - f, - "White Balance Temperature: {}K", - self.white_balance_temperature - )?; - } - - if self.dynamic_range_priority == FujiDynamicRangePriority::Off { - writeln!(f, "Dynamic Range: {}", self.dynamic_range)?; - } - - writeln!(f, "Dynamic Range Priority: {}", self.dynamic_range_priority)?; - - writeln!( - f, - "Lens Modulation Optimizer: {}", - self.lens_modulation_optimizer - )?; - writeln!(f, "Color Space: {}", self.color_space) - } -} - fn handle_get(json: bool, device_id: Option<&str>, slot: FujiCustomSetting) -> anyhow::Result<()> { let mut camera = usb::get_camera(device_id)?; - camera.set_active_custom_setting(&slot)?; - - let repr = FilmSimulationRepr { - name: camera.get_custom_setting_name()?, - size: camera.get_image_size()?, - quality: camera.get_image_quality()?, - simulation: camera.get_film_simulation()?, - monochromatic_color_temperature: camera.get_monochromatic_color_temperature()?, - monochromatic_color_tint: camera.get_monochromatic_color_tint()?, - highlight: camera.get_highlight_tone()?, - shadow: camera.get_shadow_tone()?, - color: camera.get_color()?, - sharpness: camera.get_sharpness()?, - clarity: camera.get_clarity()?, - noise_reduction: camera.get_high_iso_nr()?, - grain: camera.get_grain_effect()?, - color_chrome_effect: camera.get_color_chrome_effect()?, - color_chrome_fx_blue: camera.get_color_chrome_fx_blue()?, - smooth_skin_effect: camera.get_smooth_skin_effect()?, - white_balance: camera.get_white_balance()?, - white_balance_shift_red: camera.get_white_balance_shift_red()?, - white_balance_shift_blue: camera.get_white_balance_shift_blue()?, - white_balance_temperature: camera.get_white_balance_temperature()?, - dynamic_range: camera.get_dynamic_range()?, - dynamic_range_priority: camera.get_dynamic_range_priority()?, - lens_modulation_optimizer: camera.get_lens_modulation_optimizer()?, - color_space: camera.get_color_space()?, - }; + let repr = camera.simulation_get(slot)?; if json { println!("{}", serde_json::to_string_pretty(&repr)?); @@ -258,176 +100,7 @@ fn handle_set( options: &FilmSimulationOptions, ) -> anyhow::Result<()> { let mut camera = usb::get_camera(device_id)?; - camera.set_active_custom_setting(&slot)?; - - // General - if let Some(name) = &set_options.name { - camera.set_custom_setting_name(name)?; - } - - if let Some(size) = &options.size { - camera.set_image_size(size)?; - } - - if let Some(quality) = &options.quality { - camera.set_image_quality(quality)?; - } - - // Style - if let Some(simulation) = &options.simulation { - camera.set_film_simulation(simulation)?; - } - - if options.monochromatic_color_temperature.is_some() - || options.monochromatic_color_tint.is_some() - { - let simulation = if let Some(simulation) = &options.simulation { - simulation - } else { - &camera.get_film_simulation()? - }; - - let is_bnw = matches!( - *simulation, - FujiFilmSimulation::Monochrome - | FujiFilmSimulation::MonochromeYe - | FujiFilmSimulation::MonochromeR - | FujiFilmSimulation::MonochromeG - | FujiFilmSimulation::AcrosSTD - | FujiFilmSimulation::AcrosYe - | FujiFilmSimulation::AcrosR - | FujiFilmSimulation::AcrosG - ); - - if let Some(monochromatic_color_temperature) = &options.monochromatic_color_temperature { - if is_bnw { - camera.set_monochromatic_color_temperature(monochromatic_color_temperature)?; - } else { - warn!( - "A B&W film simulation is not selected, refusing to set monochromatic color temperature" - ); - } - } - - if let Some(monochromatic_color_tint) = &options.monochromatic_color_tint { - if is_bnw { - camera.set_monochromatic_color_tint(monochromatic_color_tint)?; - } else { - warn!( - "A B&W film simulation is not selected, refusing to set monochromatic color tint" - ); - } - } - } - - if let Some(color) = &options.color { - camera.set_color(color)?; - } - - if let Some(sharpness) = &options.sharpness { - camera.set_sharpness(sharpness)?; - } - - if let Some(clarity) = &options.clarity { - camera.set_clarity(clarity)?; - } - - if let Some(noise_reduction) = &options.noise_reduction { - camera.set_high_iso_nr(noise_reduction)?; - } - - if let Some(grain) = &options.grain { - camera.set_grain_effect(grain)?; - } - - if let Some(color_chrome_effect) = &options.color_chrome_effect { - camera.set_color_chrome_effect(color_chrome_effect)?; - } - - if let Some(color_chrome_fx_blue) = &options.color_chrome_fx_blue { - camera.set_color_chrome_fx_blue(color_chrome_fx_blue)?; - } - - if let Some(smooth_skin_effect) = &options.smooth_skin_effect { - camera.set_smooth_skin_effect(smooth_skin_effect)?; - } - - // White Balance - if let Some(white_balance) = &options.white_balance { - camera.set_white_balance(white_balance)?; - } - - if let Some(temperature) = &options.white_balance_temperature { - let white_balance = if let Some(white_balance) = &options.white_balance { - white_balance - } else { - &camera.get_white_balance()? - }; - - if *white_balance == FujiWhiteBalance::Temperature { - camera.set_white_balance_temperature(temperature)?; - } else { - warn!("White Balance mode is not set to 'Temperature', refusing to set temperature"); - } - } - - if let Some(shift_red) = &options.white_balance_shift_red { - camera.set_white_balance_shift_red(shift_red)?; - } - - if let Some(shift_blue) = &options.white_balance_shift_blue { - camera.set_white_balance_shift_blue(shift_blue)?; - } - - // Exposure - if let Some(dynamic_range_priority) = &options.dynamic_range_priority { - camera.set_dynamic_range_priority(dynamic_range_priority)?; - } - - if options.dynamic_range.is_some() || options.highlight.is_some() || options.shadow.is_some() { - let dynamic_range_priority = - if let Some(dynamic_range_priority) = &options.dynamic_range_priority { - dynamic_range_priority - } else { - &camera.get_dynamic_range_priority()? - }; - - let is_drp_off = *dynamic_range_priority == FujiDynamicRangePriority::Off; - - if let Some(dynamic_range) = &options.dynamic_range { - if is_drp_off { - camera.set_dynamic_range(dynamic_range)?; - } else { - warn!("Dynamic Range Priority is enabled, refusing to set dynamic range"); - } - } - - if let Some(highlights) = &options.highlight { - if is_drp_off { - camera.set_highlight_tone(highlights)?; - } else { - warn!("Dynamic Range Priority is enabled, refusing to set highlight tone"); - } - } - - if let Some(shadows) = &options.shadow { - if is_drp_off { - camera.set_shadow_tone(shadows)?; - } else { - warn!("Dynamic Range Priority is enabled, refusing to set shadow tone"); - } - } - } - - // Extras - if let Some(lens_modulation_optimizer) = &options.lens_modulation_optimizer { - camera.set_lens_modulation_optimizer(lens_modulation_optimizer)?; - } - - if let Some(color_space) = &options.color_space { - camera.set_color_space(color_space)?; - } - + camera.simulation_set(slot, set_options, options)?; Ok(()) }