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..753add8 100644 --- a/src/camera/devices/mod.rs +++ b/src/camera/devices/mod.rs @@ -1,42 +1,225 @@ -use rusb::GlobalContext; +pub mod x_trans_v; -use super::CameraImpl; +use std::{ + fmt, + io::{self, Write}, +}; -type ImplFactory

= fn() -> Box>; +use anyhow::anyhow; +use log::debug; +use ptp_cursor::{PtpDeserialize, PtpSerialize}; +use serde::Serialize; +use strum::IntoEnumIterator; -#[derive(Debug, Clone, Copy)] -pub struct SupportedCamera { - pub name: &'static str, - pub vendor: u16, - pub product: u16, - pub impl_factory: ImplFactory

, -} +use crate::{ + camera::ptp::hex::CommandCode, + cli::{common::film::FilmSimulationOptions, simulation::SetFilmSimulationOptions}, +}; -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 {}), +use super::{ + CameraResult, SupportedCamera, + ptp::{ + Ptp, + hex::{DevicePropCode, FujiCustomSetting, ObjectFormat, UsbMode}, + structs::ObjectInfo, + }, +}; + +pub trait DeviceImpl { + fn camera_definition(&self) -> &'static SupportedCamera

; + + fn chunk_size(&self) -> usize { + // Default conservative estimate. + 1024 * 1024 + } + + fn custom_settings_slots(&self) -> Vec { + FujiCustomSetting::iter().collect() + } + + fn info_get(&self, ptp: &mut Ptp) -> anyhow::Result> { + let info = ptp.get_info()?; + + let bytes = ptp.get_prop_value(DevicePropCode::FujiUsbMode)?; + let mode = UsbMode::try_from_ptp(&bytes)?; + + let bytes = ptp.get_prop_value(DevicePropCode::FujiBatteryInfo2)?; + debug!("Raw battery data: {bytes:?}"); + + let battery_string = String::try_from_ptp(&bytes)?; + debug!("Decoded raw string: {battery_string}"); + + let battery: u32 = battery_string + .split(',') + .next() + .ok_or_else(|| anyhow!("Failed to parse battery percentage"))? + .parse()?; + + let repr = CameraInfo { + manufacturer: info.manufacturer.clone(), + model: info.model.clone(), + device_version: info.device_version.clone(), + serial_number: info.serial_number, + mode, + battery, }; - pub struct $struct_name {} + let repr = Box::new(repr); - impl crate::camera::CameraImpl for $struct_name { - fn supported_camera(&self) -> &'static SupportedCamera { - &$const_name - } + Ok(repr) + } + + 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)?; + debug!("Received response with {} bytes", response.len()); + + debug!("Sending GetObject command for backup"); + let response = ptp.send(CommandCode::GetObject, &[HANDLE], None)?; + 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()?), + )?; + debug!("Received response with {} bytes", response.len()); + + debug!("Sending SendObject command for backup"); + let response = ptp.send(CommandCode::SendObject, &[0x0], Some(buffer))?; + debug!("Received response with {} bytes", response.len()); + + Ok(()) + } +} + +// TODO: Naively assuming that all cameras support getting basic info. +#[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) + } +} + +// TODO: Naively assuming that all cameras support backup/restore +// using the same structs. +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 SensorImpl { + fn simulation_list( + &self, + ptp: &mut Ptp, + device: &dyn DeviceImpl

, + ) -> anyhow::Result>>; + + fn simulation_get( + &self, + ptp: &mut Ptp, + device: &dyn DeviceImpl

, + slot: FujiCustomSetting, + ) -> anyhow::Result>; + + fn simulation_set( + &self, + ptp: &mut Ptp, + device: &dyn DeviceImpl

, + slot: FujiCustomSetting, + set_options: &SetFilmSimulationOptions, + options: &FilmSimulationOptions, + ) -> anyhow::Result<()>; +} + +macro_rules! prop_getter { + ($name:ident: $type:ty => $code:expr) => { + fn $name(&self, ptp: &mut crate::camera::ptp::Ptp) -> anyhow::Result<$type> { + use ptp_cursor::PtpDeserialize; + + let bytes = ptp.get_prop_value($code)?; + let result = <$type>::try_from_ptp(&bytes)?; + Ok(result) } }; } -default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5"); +macro_rules! prop_setter { + ($name:ident: $type:ty => $code:expr) => { + fn $name(&self, ptp: &mut crate::camera::ptp::Ptp, value: &$type) -> anyhow::Result<()> { + use ptp_cursor::PtpSerialize; -pub const SUPPORTED: &[SupportedCamera] = &[FUJIFILM_XT5]; + let bytes = value.try_into_ptp()?; + ptp.set_prop_value($code, &bytes)?; + Ok(()) + } + }; +} + +macro_rules! 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 prop_getter; +pub(crate) use prop_setter; +pub(crate) use set_prop_if_some; 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..117bdf8 --- /dev/null +++ b/src/camera/devices/x_trans_v/mod.rs @@ -0,0 +1,535 @@ +pub mod x_t5; + +use std::{ + fmt, + io::{self, Cursor, Write}, +}; + +use anyhow::bail; +use log::error; +use ptp_cursor::{PtpDeserialize, PtpSerialize}; +use ptp_macro::{PtpDeserialize, PtpSerialize}; +use serde::Serialize; + +use crate::{ + camera::{ + CameraResult, + devices::set_prop_if_some, + ptp::{ + Ptp, + hex::{ + 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, + }, + }, + }, + cli::{common::film::FilmSimulationOptions, simulation::SetFilmSimulationOptions}, +}; + +use super::{DeviceImpl, SensorImpl, prop_getter, prop_setter}; + +pub struct XTransV; + +impl XTransV { + prop_getter!(get_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName); + prop_getter!(get_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize); + prop_getter!(get_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality); + prop_getter!(get_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange); + prop_getter!(get_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority); + prop_getter!(get_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation); + prop_getter!(get_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature); + prop_getter!(get_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint); + prop_getter!(get_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect); + prop_getter!(get_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance); + prop_getter!(get_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR); + prop_getter!(get_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone); + prop_getter!(get_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone); + prop_getter!(get_color: FujiColor => DevicePropCode::FujiCustomSettingColor); + prop_getter!(get_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness); + prop_getter!(get_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity); + prop_getter!(get_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed); + prop_getter!(get_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue); + prop_getter!(get_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature); + prop_getter!(get_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect); + prop_getter!(get_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue); + prop_getter!(get_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect); + prop_getter!(get_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer); + prop_getter!(get_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace); + + prop_setter!(set_active_custom_setting: FujiCustomSetting => DevicePropCode::FujiCustomSetting); + prop_setter!(set_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName); + prop_setter!(set_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize); + prop_setter!(set_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality); + prop_setter!(set_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange); + prop_setter!(set_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority); + prop_setter!(set_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation); + prop_setter!(set_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature); + prop_setter!(set_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint); + prop_setter!(set_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect); + prop_setter!(set_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance); + prop_setter!(set_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR); + prop_setter!(set_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone); + prop_setter!(set_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone); + prop_setter!(set_color: FujiColor => DevicePropCode::FujiCustomSettingColor); + prop_setter!(set_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness); + prop_setter!(set_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity); + prop_setter!(set_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed); + prop_setter!(set_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue); + prop_setter!(set_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature); + prop_setter!(set_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect); + prop_setter!(set_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue); + prop_setter!(set_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect); + prop_setter!(set_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer); + prop_setter!(set_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace); + + fn validate_monochromatic( + final_options: &FilmSimulationOptions, + prev_simulation: FujiFilmSimulation, + ) -> bool { + 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; + } + } + + fail + } + + fn validate_white_balance_temperature( + final_options: &FilmSimulationOptions, + prev_white_balance: FujiWhiteBalance, + ) -> bool { + 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"); + false + } else { + true + } + } + + fn validate_exposure( + final_options: &FilmSimulationOptions, + previous_dynamic_range_priority: FujiDynamicRangePriority, + ) -> bool { + 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; + } + } + + fail + } + + 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 SensorImpl

for XTransV { + fn simulation_list( + &self, + ptp: &mut Ptp, + device: &dyn DeviceImpl

, + ) -> anyhow::Result>> { + let mut slots = Vec::new(); + + for slot in device.custom_settings_slots() { + 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, + device: &dyn DeviceImpl

, + slot: FujiCustomSetting, + ) -> anyhow::Result> { + if !device.custom_settings_slots().contains(&slot) { + bail!("Unsupported custom setting slot '{slot}'") + } + + 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, + device: &dyn DeviceImpl

, + slot: FujiCustomSetting, + set_options: &SetFilmSimulationOptions, + options: &FilmSimulationOptions, + ) -> anyhow::Result<()> { + if !device.custom_settings_slots().contains(&slot) { + bail!("Unsupported custom setting slot '{slot}'") + } + + self.set_active_custom_setting(ptp, &slot)?; + + self.validate_simulation_set(ptp, options)?; + + set_prop_if_some!(self, ptp, set_options, + name => set_custom_setting_name, + ); + + 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(()) + } +} + +// TODO: Naively assuming that all cameras using the same sensor +// also have the same simulation feature set. +#[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) + } +} + +#[derive(Debug, PtpSerialize, PtpDeserialize)] +pub struct FujiConversionProfileContents { + // TODO: What is this, and why is it always 0x2? + pub unknown_0: i32, + pub file_type: u32, + pub size: u32, + pub quality: u32, + pub exposure_offset: i32, + pub dynamic_range: u32, + pub dynamic_range_priority: u32, + pub simulation: u32, + pub grain: u32, + pub color_chrome_effect: u32, + pub white_balance_as_shot: u32, + pub white_balance: u32, + pub white_balance_shift_red: i32, + pub white_balance_shift_blue: i32, + pub white_balance_temperature: i32, + pub highlight: i32, + pub shadow: i32, + pub color: i32, + pub sharpness: i32, + pub noise_reduction: u32, + pub lens_modulation_optimizer: u32, + pub color_space: u32, + pub monochromatic_color_temperature: i32, + pub smooth_skin_effect: u32, + pub color_chrome_fx_blue: u32, + pub monochromatic_color_tint: i32, + pub clarity: i32, + pub teleconverter: u32, +} + +#[derive(Debug)] +pub struct FujiConversionProfile { + pub contents: FujiConversionProfileContents, +} + +impl FujiConversionProfile { + const EXPECTED_N_PROPS: i16 = 29; + const EXPECTED_PROFILE_CODE: &str = "FF17950"; + const PADDING: usize = 0x1EE; +} + +impl PtpDeserialize for FujiConversionProfile { + fn try_from_ptp(buf: &[u8]) -> io::Result { + let mut cur = Cursor::new(buf); + let value = Self::try_read_ptp(&mut cur)?; + Ok(value) + } + + fn try_read_ptp(cur: &mut R) -> io::Result { + let n_props = ::try_read_ptp(cur)?; + if n_props != Self::EXPECTED_N_PROPS { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!("Expected {} props, got {n_props}", Self::EXPECTED_N_PROPS), + )); + } + + let profile_code = String::try_read_ptp(cur)?; + if profile_code != Self::EXPECTED_PROFILE_CODE { + return Err(io::Error::new( + io::ErrorKind::InvalidData, + format!( + "Expected profile code '{}', got '{profile_code}'", + Self::EXPECTED_PROFILE_CODE + ), + )); + } + + let mut padding = [0u8; Self::PADDING]; + cur.read_exact(&mut padding)?; + + let contents = FujiConversionProfileContents::try_read_ptp(cur)?; + Ok(Self { contents }) + } +} + +impl PtpSerialize for FujiConversionProfile { + 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<()> { + Self::EXPECTED_N_PROPS.try_write_ptp(buf)?; + Self::EXPECTED_PROFILE_CODE.try_write_ptp(buf)?; + + let padding = [0u8; Self::PADDING]; + buf.write_all(&padding)?; + + self.contents.try_write_ptp(buf)?; + Ok(()) + } +} 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..2cc3b21 --- /dev/null +++ b/src/camera/devices/x_trans_v/x_t5/mod.rs @@ -0,0 +1,26 @@ +use rusb::GlobalContext; + +use crate::camera::{DeviceImpl, devices::SupportedCamera}; + +use super::XTransV; + +pub const FUJIFILM_XT5: SupportedCamera = SupportedCamera { + name: "FUJIFILM XT-5", + vendor: 0x04cb, + product: 0x02fc, + device_factory: || Box::new(FujifilmXT5 {}), + sensor_factory: || Box::new(XTransV {}), +}; + +pub struct FujifilmXT5 {} + +impl DeviceImpl for FujifilmXT5 { + fn camera_definition(&self) -> &'static SupportedCamera { + &FUJIFILM_XT5 + } + + fn chunk_size(&self) -> usize { + // 15.75 * 1024^2 + 16128 * 1024 + } +} diff --git a/src/camera/mod.rs b/src/camera/mod.rs index 42b60e1..b814363 100644 --- a/src/camera/mod.rs +++ b/src/camera/mod.rs @@ -2,141 +2,91 @@ pub mod devices; pub mod error; pub mod ptp; -use std::time::Duration; +use std::fmt; -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::{DeviceImpl, SensorImpl, x_trans_v}; +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; +use serde::Serialize; const SESSION: u32 = 1; pub struct Camera { - pub r#impl: Box>, + pub device: Box>, + pub sensor: Box>, 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_device { + ($name:ident -> $ret:ty) => { + pub fn $name(&mut self) -> $ret { + self.device.$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.device.$name(&mut self.ptp, $($arg),*) + } }; } +macro_rules! camera_to_sensor { + ($name:ident -> $ret:ty) => { + pub fn $name(&mut self) -> $ret { + self.sensor.$name(&mut self.ptp, &self.device) + } + }; + + ($name:ident($($arg:ident: $arg_ty:ty),*) -> $ret:ty) => { + pub fn $name(&mut self, $($arg: $arg_ty),*) -> $ret { + self.sensor.$name(&mut self.ptp, &*self.device, $($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 + self.device.camera_definition().name } pub fn vendor_id(&self) -> u16 { - self.r#impl.supported_camera().vendor + self.device.camera_definition().vendor } pub fn product_id(&self) -> u16 { - self.r#impl.supported_camera().product + self.device.camera_definition().product } pub fn connected_usb_id(&self) -> String { format!("{}.{}", self.ptp.bus, self.ptp.address) } - camera_with_ptp! { - get_info => DeviceInfo, - get_usb_mode => UsbMode, - get_battery_info => u32, - } + camera_to_device!(info_get() -> anyhow::Result>); - 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_to_device!(backup_export -> anyhow::Result>); + camera_to_device!(backup_import(buffer: &[u8]) -> anyhow::Result<()>); - 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_sensor!(simulation_list() -> anyhow::Result>>); + camera_to_sensor!(simulation_get(slot: FujiCustomSetting) -> anyhow::Result>); + camera_to_sensor!(simulation_set(slot: FujiCustomSetting, set_options: &SetFilmSimulationOptions, options: &FilmSimulationOptions) -> anyhow::Result<()>); } impl Drop for Camera { fn drop(&mut self) { debug!("Closing session"); - if let Err(e) = self.ptp.close_session(SESSION, self.r#impl.timeout()) { + if let Err(e) = self.ptp.close_session(SESSION) { error!("Error closing session: {e}"); } debug!("Session closed"); @@ -152,12 +102,13 @@ impl TryFrom<&rusb::Device> for Camera { let vendor = descriptor.vendor_id(); let product = descriptor.product_id(); - for supported_camera in devices::SUPPORTED { + for supported_camera in SUPPORTED { if vendor != supported_camera.vendor || product != supported_camera.product { continue; } - let r#impl = (supported_camera.impl_factory)(); + let device_impl = (supported_camera.device_factory)(); + let sensor_impl = (supported_camera.sensor_factory)(); let bus = device.bus_number(); let address = device.address(); @@ -189,7 +140,7 @@ impl TryFrom<&rusb::Device> for Camera { let transaction_id = 0; - let chunk_size = r#impl.chunk_size(); + let chunk_size = device_impl.chunk_size(); let mut ptp = Ptp { bus, @@ -203,154 +154,60 @@ impl TryFrom<&rusb::Device> for Camera { }; debug!("Opening session"); - let () = ptp.open_session(SESSION, r#impl.timeout())?; + let () = ptp.open_session(SESSION)?; debug!("Session opened"); - return Ok(Self { r#impl, ptp }); + return Ok(Self { + ptp, + device: device_impl, + sensor: sensor_impl, + }); } 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 = ptp.get_prop_value($code, self.timeout())?; - let result = <$type>::try_from_ptp(&bytes)?; - Ok(result) - } +type DeviceFactory

= fn() -> Box>; +type SensorFactory

= fn() -> Box>; - #[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(()) - } - } - )+ - }; +#[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, } -pub trait CameraImpl { - fn supported_camera(&self) -> &'static SupportedCamera

; - - fn timeout(&self) -> Duration { - Duration::default() - } - - fn chunk_size(&self) -> usize { - // Conservative estimate. Could go up to 15.75 * 1024^2 on the X-T5 but only gained 200ms. - 1024 * 1024 - } - - fn get_info(&mut self, ptp: &mut Ptp) -> anyhow::Result { - let info = ptp.get_info(self.timeout())?; - Ok(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 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 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); - - 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)?; +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()), } - - 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::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, } } + +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 + ) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct SupportedCamera { + pub name: &'static str, + pub vendor: u16, + pub product: u16, + pub device_factory: DeviceFactory

, + pub sensor_factory: SensorFactory

, +} + +pub const SUPPORTED: &[SupportedCamera] = &[x_trans_v::x_t5::FUJIFILM_XT5]; diff --git a/src/camera/ptp/hex.rs b/src/camera/ptp/hex.rs index fc1db47..a147027 100644 --- a/src/camera/ptp/hex.rs +++ b/src/camera/ptp/hex.rs @@ -18,8 +18,10 @@ pub enum CommandCode { GetDeviceInfo = 0x1001, OpenSession = 0x1002, CloseSession = 0x1003, + GetObjectHandles = 0x1007, GetObjectInfo = 0x1008, GetObject = 0x1009, + DeleteObject = 0x100B, SendObjectInfo = 0x100C, SendObject = 0x100D, GetDevicePropValue = 0x1015, @@ -309,6 +311,8 @@ pub enum FujiDynamicRange { HDR100 = 0x64, HDR200 = 0xc8, HDR400 = 0x190, + // TODO: Limit this when setting film sim + HDR800 = 0x320, } #[repr(u16)] @@ -327,6 +331,8 @@ pub enum FujiDynamicRange { )] pub enum FujiDynamicRangePriority { Auto = 0x8000, + // TODO: Limit this, used in conjuction with HDR800 + Plus = 0x3, Strong = 0x2, Weak = 0x1, Off = 0x0, @@ -369,6 +375,27 @@ pub enum FujiFilmSimulation { RealaAce = 0x14, } +#[repr(u16)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + IntoPrimitive, + TryFromPrimitive, + PtpSerialize, + PtpDeserialize, + EnumIter, +)] +pub enum FujiFileType { + Jpeg = 0x7, + Heif = 0x12, + Tiff8 = 0x9, + Tiff10 = 0xb, +} + #[repr(u16)] #[derive( Debug, @@ -388,7 +415,10 @@ pub enum FujiGrainEffect { WeakLarge = 0x4, StrongSmall = 0x3, WeakSmall = 0x2, - Off = 0x6, + // TODO: God knows what the fuck is happening here. + // If I do Set 0x1 and immediately Get, camera returns 0x6 or 0x7. + #[num_enum(alternatives = [0x6, 0x7])] + Off = 0x1, } #[repr(u16)] @@ -451,6 +481,25 @@ pub enum FujiSmoothSkinEffect { Off = 0x1, } +#[repr(u16)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + IntoPrimitive, + TryFromPrimitive, + PtpSerialize, + PtpDeserialize, + EnumIter, +)] +pub enum FujiWhiteBalanceAsShot { + False = 0x2, + True = 0x1, +} + #[repr(u16)] #[derive( Debug, @@ -466,7 +515,6 @@ pub enum FujiSmoothSkinEffect { EnumIter, )] pub enum FujiWhiteBalance { - AsShot = 0x1, WhitePriority = 0x8020, Auto = 0x2, AmbiencePriority = 0x8021, @@ -538,6 +586,25 @@ pub enum FujiColorSpace { AdobeRGB = 0x1, } +#[repr(u16)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + IntoPrimitive, + TryFromPrimitive, + PtpSerialize, + PtpDeserialize, + EnumIter, +)] +pub enum FujiTeleconverter { + Off = 0x2, + On = 0x1, +} + macro_rules! fuji_i16 { ($name:ident, $min:expr, $max:expr, $step:expr, $scale:literal) => { #[derive(Debug, Clone, Copy, PartialEq, Eq, PtpSerialize, PtpDeserialize)] @@ -589,6 +656,12 @@ macro_rules! fuji_i16 { Ok(Self(value)) } } + + impl std::convert::From<$name> for i16 { + fn from(value: $name) -> i16 { + *value.deref() + } + } }; } @@ -602,6 +675,30 @@ fuji_i16!(FujiColor, -4.0, 4.0, 1.0, 10i16); fuji_i16!(FujiSharpness, -4.0, 4.0, 1.0, 10i16); fuji_i16!(FujiClarity, -5.0, 5.0, 1.0, 10i16); +#[repr(i16)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, EnumIter)] +pub enum FujiExposureOffset { + Plus3 = 3000, + Plus2_7 = 2667, + Plus2_3 = 2333, + Plus2 = 2000, + Plus1_7 = 1667, + Plus1_3 = 1333, + Plus1 = 1000, + Plus0_7 = 667, + Plus0_3 = 333, + Zero = 0, + Minus0_3 = -333, + Minus0_7 = -667, + Minus1 = -1000, + Minus1_3 = -1333, + Minus1_7 = -1667, + Minus2 = -2000, + Minus2_3 = -2333, + Minus2_7 = -2667, + Minus3 = -3000, +} + #[repr(u16)] #[derive( Debug, Clone, Copy, Serialize, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpDeserialize, diff --git a/src/camera/ptp/mod.rs b/src/camera/ptp/mod.rs index 7d0ace6..aaeebc6 100644 --- a/src/camera/ptp/mod.rs +++ b/src/camera/ptp/mod.rs @@ -28,50 +28,40 @@ impl Ptp { code: CommandCode, params: &[u32], data: Option<&[u8]>, - timeout: Duration, ) -> anyhow::Result> { let transaction_id = self.transaction_id; - self.send_header(code, params, transaction_id, timeout)?; + self.send_header(code, params, transaction_id)?; if let Some(data) = data { - self.write(ContainerType::Data, code, data, transaction_id, timeout)?; + self.write(ContainerType::Data, code, data, transaction_id)?; } - let response = self.receive_response(timeout); + let response = self.receive_response(); self.transaction_id += 1; response } - pub fn open_session(&mut self, session_id: u32, timeout: Duration) -> anyhow::Result<()> { + pub fn open_session(&mut self, session_id: u32) -> anyhow::Result<()> { debug!("Sending OpenSession command"); - self.send(CommandCode::OpenSession, &[session_id], None, timeout)?; + self.send(CommandCode::OpenSession, &[session_id], None)?; Ok(()) } - pub fn close_session(&mut self, _: u32, timeout: Duration) -> anyhow::Result<()> { + pub fn close_session(&mut self, _: u32) -> anyhow::Result<()> { debug!("Sending CloseSession command"); - self.send(CommandCode::CloseSession, &[], None, timeout)?; + self.send(CommandCode::CloseSession, &[], None)?; Ok(()) } - pub fn get_info(&mut self, timeout: Duration) -> anyhow::Result { + pub fn get_info(&mut self) -> anyhow::Result { debug!("Sending GetDeviceInfo command"); - let response = self.send(CommandCode::GetDeviceInfo, &[], None, timeout)?; + let response = self.send(CommandCode::GetDeviceInfo, &[], None)?; debug!("Received response with {} bytes", response.len()); let info = DeviceInfo::try_from_ptp(&response)?; Ok(info) } - pub fn get_prop_value( - &mut self, - prop: DevicePropCode, - timeout: Duration, - ) -> anyhow::Result> { + pub fn get_prop_value(&mut self, prop: DevicePropCode) -> anyhow::Result> { debug!("Sending GetDevicePropValue command for property {prop:?}"); - let response = self.send( - CommandCode::GetDevicePropValue, - &[prop.into()], - None, - timeout, - )?; + let response = self.send(CommandCode::GetDevicePropValue, &[prop.into()], None)?; debug!("Received response with {} bytes", response.len()); Ok(response) } @@ -80,15 +70,9 @@ impl Ptp { &mut self, prop: DevicePropCode, value: &[u8], - timeout: Duration, ) -> anyhow::Result> { debug!("Sending GetDevicePropValue command for property {prop:?}"); - let response = self.send( - CommandCode::SetDevicePropValue, - &[prop.into()], - Some(value), - timeout, - )?; + let response = self.send(CommandCode::SetDevicePropValue, &[prop.into()], Some(value))?; debug!("Received response with {} bytes", response.len()); Ok(response) } @@ -98,7 +82,6 @@ impl Ptp { code: CommandCode, params: &[u32], transaction_id: u32, - timeout: Duration, ) -> anyhow::Result<()> { let mut payload = Vec::with_capacity(params.len() * 4); for p in params { @@ -112,21 +95,15 @@ impl Ptp { payload.len(), payload, ); - self.write( - ContainerType::Command, - code, - &payload, - transaction_id, - timeout, - )?; + self.write(ContainerType::Command, code, &payload, transaction_id)?; Ok(()) } - fn receive_response(&self, timeout: Duration) -> anyhow::Result> { + fn receive_response(&self) -> anyhow::Result> { let mut response = Vec::new(); loop { - let (container, payload) = self.read(timeout)?; + let (container, payload) = self.read()?; match container.kind { ContainerType::Data => { trace!("Response received: data ({} bytes)", payload.len()); @@ -168,7 +145,6 @@ impl Ptp { code: CommandCode, payload: &[u8], transaction_id: u32, - timeout: Duration, ) -> anyhow::Result<()> { let container_info = ContainerInfo::new(kind, code, transaction_id, payload.len())?; let mut buffer: Vec = container_info.try_into_ptp()?; @@ -179,11 +155,13 @@ impl Ptp { trace!( "Writing PTP {kind:?} container, code: {code:?}, transaction: {transaction_id:?}, first payload chunk ({first_chunk_len} bytes)", ); - self.handle.write_bulk(self.bulk_out, &buffer, timeout)?; + self.handle + .write_bulk(self.bulk_out, &buffer, Duration::ZERO)?; for chunk in payload[first_chunk_len..].chunks(self.chunk_size) { trace!("Writing additional payload chunk ({} bytes)", chunk.len(),); - self.handle.write_bulk(self.bulk_out, chunk, timeout)?; + self.handle + .write_bulk(self.bulk_out, chunk, Duration::ZERO)?; } trace!( @@ -194,12 +172,12 @@ impl Ptp { Ok(()) } - fn read(&self, timeout: Duration) -> anyhow::Result<(ContainerInfo, Vec)> { + fn read(&self) -> anyhow::Result<(ContainerInfo, Vec)> { let mut stack_buf = [0u8; 8 * 1024]; let n = self .handle - .read_bulk(self.bulk_in, &mut stack_buf, timeout)?; + .read_bulk(self.bulk_in, &mut stack_buf, Duration::ZERO)?; let buf = &stack_buf[..n]; trace!("Read chunk ({n} bytes)"); @@ -220,7 +198,9 @@ impl Ptp { while payload.len() < payload_len { let remaining = payload_len - payload.len(); let mut chunk = vec![0u8; min(remaining, self.chunk_size)]; - let n = self.handle.read_bulk(self.bulk_in, &mut chunk, timeout)?; + let n = self + .handle + .read_bulk(self.bulk_in, &mut chunk, Duration::ZERO)?; trace!("Read additional chunk ({n} bytes)"); if n == 0 { break; 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/common/film.rs b/src/cli/common/film.rs index 6c6c241..6e9b357 100644 --- a/src/cli/common/film.rs +++ b/src/cli/common/film.rs @@ -9,10 +9,11 @@ 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, UsbMode, + FujiExposureOffset, FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, + FujiImageQuality, FujiImageSize, FujiLensModulationOptimizer, + FujiMonochromaticColorTemperature, FujiMonochromaticColorTint, FujiShadowTone, + FujiSharpness, FujiSmoothSkinEffect, FujiWhiteBalance, FujiWhiteBalanceShift, + FujiWhiteBalanceTemperature, UsbMode, }, cli::common::suggest::get_closest, }; @@ -291,6 +292,7 @@ impl fmt::Display for FujiDynamicRange { Self::HDR100 => write!(f, "HDR100"), Self::HDR200 => write!(f, "HDR200"), Self::HDR400 => write!(f, "HDR400"), + Self::HDR800 => write!(f, "HDR800"), } } } @@ -306,6 +308,7 @@ impl FromStr for FujiDynamicRange { "100" | "hdr100" | "dr100" => return Ok(Self::HDR100), "200" | "hdr200" | "dr200" => return Ok(Self::HDR200), "400" | "hdr400" | "dr400" => return Ok(Self::HDR400), + "800" | "hdr800" | "dr800" => return Ok(Self::HDR800), _ => {} } @@ -325,6 +328,7 @@ impl fmt::Display for FujiDynamicRangePriority { Self::Strong => write!(f, "Strong"), Self::Weak => write!(f, "Weak"), Self::Off => write!(f, "Off"), + Self::Plus => writeln!(f, "Plus"), } } } @@ -340,6 +344,7 @@ impl FromStr for FujiDynamicRangePriority { "strong" | "drpstrong" => return Ok(Self::Strong), "weak" | "drpweak" => return Ok(Self::Weak), "off" | "drpoff" => return Ok(Self::Off), + "plus" => return Ok(Self::Plus), _ => {} } @@ -584,7 +589,6 @@ impl FromStr for FujiSmoothSkinEffect { impl fmt::Display for FujiWhiteBalance { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::AsShot => write!(f, "As Shot"), Self::WhitePriority => write!(f, "White Priority"), Self::Auto => write!(f, "Auto"), Self::AmbiencePriority => write!(f, "Ambience Priority"), @@ -611,8 +615,7 @@ impl FromStr for FujiWhiteBalance { match input.as_str() { "whitepriority" | "white" => return Ok(Self::WhitePriority), - // We can't set a film simulation to be "As Shot", so silently parse it to Auto - "auto" | "shot" | "asshot" | "original" => return Ok(Self::Auto), + "auto" => return Ok(Self::Auto), "ambiencepriority" | "ambience" | "ambient" => { return Ok(Self::AmbiencePriority); } @@ -764,6 +767,108 @@ impl FromStr for FujiColorSpace { } } +impl FromStr for FujiExposureOffset { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s + .trim() + .parse::() + .with_context(|| format!("Invalid numeric value '{s}'"))?; + + let round = (input * 10.0).round() / 10.0; + + match round { + 3.0 => return Ok(Self::Plus3), + 2.7 => return Ok(Self::Plus2_7), + 2.3 => return Ok(Self::Plus2_3), + 2.0 => return Ok(Self::Plus2), + 1.7 => return Ok(Self::Plus1_7), + 1.3 => return Ok(Self::Plus1_3), + 1.0 => return Ok(Self::Plus1), + 0.7 => return Ok(Self::Plus0_7), + 0.3 => return Ok(Self::Plus0_3), + 0.0 => return Ok(Self::Zero), + -0.3 => return Ok(Self::Minus0_3), + -0.7 => return Ok(Self::Minus0_7), + -1.0 => return Ok(Self::Minus1), + -1.3 => return Ok(Self::Minus1_3), + -1.7 => return Ok(Self::Minus1_7), + -2.0 => return Ok(Self::Minus2), + -2.3 => return Ok(Self::Minus2_3), + -2.7 => return Ok(Self::Minus2_7), + -3.0 => return Ok(Self::Minus3), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = get_closest(s, &choices) { + bail!("Unknown exposure offset '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown exposure offset '{s}'"); + } +} + +impl fmt::Display for FujiExposureOffset { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let val = match self { + Self::Minus3 => -3.0, + Self::Minus2_7 => -2.7, + Self::Minus2_3 => -2.3, + Self::Minus2 => -2.0, + Self::Minus1_7 => -1.7, + Self::Minus1_3 => -1.3, + Self::Minus1 => -1.0, + Self::Minus0_7 => -0.7, + Self::Minus0_3 => -0.3, + Self::Zero => 0.0, + Self::Plus0_3 => 0.3, + Self::Plus0_7 => 0.7, + Self::Plus1 => 1.0, + Self::Plus1_3 => 1.3, + Self::Plus1_7 => 1.7, + Self::Plus2 => 2.0, + Self::Plus2_3 => 2.3, + Self::Plus2_7 => 2.7, + Self::Plus3 => 3.0, + }; + + write!(f, "{val}") + } +} + +impl Serialize for FujiExposureOffset { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let val = match self { + Self::Minus3 => -3.0, + Self::Minus2_7 => -2.7, + Self::Minus2_3 => -2.3, + Self::Minus2 => -2.0, + Self::Minus1_7 => -1.7, + Self::Minus1_3 => -1.3, + Self::Minus1 => -1.0, + Self::Minus0_7 => -0.7, + Self::Minus0_3 => -0.3, + Self::Zero => 0.0, + Self::Plus0_3 => 0.3, + Self::Plus0_7 => 0.7, + Self::Plus1 => 1.0, + Self::Plus1_3 => 1.3, + Self::Plus1_7 => 1.7, + Self::Plus2 => 2.0, + Self::Plus2_3 => 2.3, + Self::Plus2_7 => 2.7, + Self::Plus3 => 3.0, + }; + + serializer.serialize_f32(val) + } +} + macro_rules! fuji_i16_cli { ($name:ident) => { impl std::str::FromStr for $name { diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs index e278fd9..e8c7fd1 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::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(()) }