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(())
}