diff --git a/Cargo.lock b/Cargo.lock
index 20372bd..e5dac52 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -210,6 +210,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+[[package]]
+name = "erased-serde"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
+dependencies = [
+ "serde",
+ "serde_core",
+ "typeid",
+]
+
[[package]]
name = "errno"
version = "0.3.14"
@@ -239,6 +250,7 @@ dependencies = [
"anyhow",
"byteorder",
"clap",
+ "erased-serde",
"log",
"log4rs",
"num_enum",
@@ -840,6 +852,12 @@ dependencies = [
"winnow",
]
+[[package]]
+name = "typeid"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
+
[[package]]
name = "typemap-ors"
version = "1.0.0"
diff --git a/Cargo.toml b/Cargo.toml
index 882d27b..4de6b3b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -29,3 +29,4 @@ ptp_cursor = { path = "crates/ptp/cursor" }
strum = { version = "0.27.2", features = ["strum_macros"] }
strum_macros = "0.27.2"
paste = "1.0.15"
+erased-serde = "0.4.8"
diff --git a/src/camera/devices/mod.rs b/src/camera/devices/mod.rs
index 7a80d30..9e53ea5 100644
--- a/src/camera/devices/mod.rs
+++ b/src/camera/devices/mod.rs
@@ -1,6 +1,11 @@
-use rusb::GlobalContext;
+mod x_trans_v;
-use super::CameraImpl;
+use std::fmt;
+
+use rusb::GlobalContext;
+use serde::Serialize;
+
+use super::{Camera, CameraImpl};
type ImplFactory
= fn() -> Box>;
@@ -12,31 +17,34 @@ pub struct SupportedCamera {
pub impl_factory: ImplFactory,
}
-macro_rules! default_camera_impl {
- (
- $const_name:ident,
- $struct_name:ident,
- $vendor:expr,
- $product:expr,
- $display_name:expr
- ) => {
- pub const $const_name: SupportedCamera = SupportedCamera {
- name: $display_name,
- vendor: $vendor,
- product: $product,
- impl_factory: || Box::new($struct_name {}),
- };
+pub const SUPPORTED: &[SupportedCamera] = &[x_trans_v::x_t5::FUJIFILM_XT5];
- pub struct $struct_name {}
-
- impl crate::camera::CameraImpl for $struct_name {
- fn supported_camera(&self) -> &'static SupportedCamera {
- &$const_name
- }
- }
- };
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CameraInfoListItem {
+ pub name: &'static str,
+ pub usb_id: String,
+ pub vendor_id: String,
+ pub product_id: String,
}
-default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5");
+impl From<&Camera> for CameraInfoListItem {
+ fn from(camera: &Camera) -> Self {
+ Self {
+ name: camera.name(),
+ usb_id: camera.connected_usb_id(),
+ vendor_id: format!("0x{:04x}", camera.vendor_id()),
+ product_id: format!("0x{:04x}", camera.product_id()),
+ }
+ }
+}
-pub const SUPPORTED: &[SupportedCamera] = &[FUJIFILM_XT5];
+impl fmt::Display for CameraInfoListItem {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(
+ f,
+ "{} ({}:{}) (USB ID: {})",
+ self.name, self.vendor_id, self.product_id, self.usb_id
+ )
+ }
+}
diff --git a/src/camera/devices/x_trans_v/mod.rs b/src/camera/devices/x_trans_v/mod.rs
new file mode 100644
index 0000000..e82aeaa
--- /dev/null
+++ b/src/camera/devices/x_trans_v/mod.rs
@@ -0,0 +1,287 @@
+use std::{
+ fmt,
+ io::{self, Write},
+};
+
+use log::error;
+use ptp_cursor::PtpSerialize;
+use serde::Serialize;
+
+use crate::{
+ camera::ptp::{
+ hex::{
+ FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace,
+ FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority,
+ FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone,
+ FujiImageQuality, FujiImageSize, FujiLensModulationOptimizer,
+ FujiMonochromaticColorTemperature, FujiMonochromaticColorTint, FujiShadowTone,
+ FujiSharpness, FujiSmoothSkinEffect, FujiWhiteBalance, FujiWhiteBalanceShift,
+ FujiWhiteBalanceTemperature, ObjectFormat, UsbMode,
+ },
+ structs::ObjectInfo,
+ },
+ cli::common::film::FilmSimulationOptions,
+};
+
+pub mod x_t5;
+
+// TODO: Assuming that all cameras using the same sensor also have the same feature set.
+// This might not actually be the case.
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct CameraInfo {
+ pub manufacturer: String,
+ pub model: String,
+ pub device_version: String,
+ pub serial_number: String,
+ pub mode: UsbMode,
+ pub battery: u32,
+}
+
+impl fmt::Display for CameraInfo {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ writeln!(f, "Manufacturer: {}", self.manufacturer)?;
+ writeln!(f, "Model: {}", self.model)?;
+ writeln!(f, "Version: {}", self.device_version)?;
+ writeln!(f, "Serial Number: {}", self.serial_number)?;
+ writeln!(f, "Mode: {}", self.mode)?;
+ write!(f, "Battery: {}%", self.battery)
+ }
+}
+
+#[derive(Debug, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct SimulationListItem {
+ pub slot: FujiCustomSetting,
+ pub name: FujiCustomSettingName,
+}
+
+impl fmt::Display for SimulationListItem {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "{}: {}", self.slot, self.name)
+ }
+}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct Simulation {
+ pub name: FujiCustomSettingName,
+ pub size: FujiImageSize,
+ pub quality: FujiImageQuality,
+ #[allow(clippy::struct_field_names)]
+ pub simulation: FujiFilmSimulation,
+ pub monochromatic_color_temperature: FujiMonochromaticColorTemperature,
+ pub monochromatic_color_tint: FujiMonochromaticColorTint,
+ pub highlight: FujiHighlightTone,
+ pub shadow: FujiShadowTone,
+ pub color: FujiColor,
+ pub sharpness: FujiSharpness,
+ pub clarity: FujiClarity,
+ pub noise_reduction: FujiHighISONR,
+ pub grain: FujiGrainEffect,
+ pub color_chrome_effect: FujiColorChromeEffect,
+ pub color_chrome_fx_blue: FujiColorChromeFXBlue,
+ pub smooth_skin_effect: FujiSmoothSkinEffect,
+ pub white_balance: FujiWhiteBalance,
+ pub white_balance_shift_red: FujiWhiteBalanceShift,
+ pub white_balance_shift_blue: FujiWhiteBalanceShift,
+ pub white_balance_temperature: FujiWhiteBalanceTemperature,
+ pub dynamic_range: FujiDynamicRange,
+ pub dynamic_range_priority: FujiDynamicRangePriority,
+ pub lens_modulation_optimizer: FujiLensModulationOptimizer,
+ pub color_space: FujiColorSpace,
+}
+
+impl fmt::Display for Simulation {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ writeln!(f, "Name: {}", self.name)?;
+ writeln!(f, "Size: {}", self.size)?;
+ writeln!(f, "Quality: {}", self.quality)?;
+
+ writeln!(f, "Simulation: {}", self.simulation)?;
+
+ match self.simulation {
+ FujiFilmSimulation::Monochrome
+ | FujiFilmSimulation::MonochromeYe
+ | FujiFilmSimulation::MonochromeR
+ | FujiFilmSimulation::MonochromeG
+ | FujiFilmSimulation::AcrosSTD
+ | FujiFilmSimulation::AcrosYe
+ | FujiFilmSimulation::AcrosR
+ | FujiFilmSimulation::AcrosG => {
+ writeln!(
+ f,
+ "Monochromatic Color Temperature: {}",
+ self.monochromatic_color_temperature
+ )?;
+ writeln!(
+ f,
+ "Monochromatic Color Tint: {}",
+ self.monochromatic_color_tint
+ )?;
+ }
+ _ => {}
+ }
+
+ if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
+ writeln!(f, "Highlights: {}", self.highlight)?;
+ writeln!(f, "Shadows: {}", self.shadow)?;
+ }
+
+ writeln!(f, "Color: {}", self.color)?;
+ writeln!(f, "Sharpness: {}", self.sharpness)?;
+ writeln!(f, "Clarity: {}", self.clarity)?;
+ writeln!(f, "Noise Reduction: {}", self.noise_reduction)?;
+ writeln!(f, "Grain: {}", self.grain)?;
+ writeln!(f, "Color Chrome Effect: {}", self.color_chrome_effect)?;
+ writeln!(f, "Color Chrome FX Blue: {}", self.color_chrome_fx_blue)?;
+ writeln!(f, "Smooth Skin Effect: {}", self.smooth_skin_effect)?;
+
+ writeln!(f, "White Balance: {}", self.white_balance)?;
+ writeln!(
+ f,
+ "White Balance Shift (R/B): {} / {}",
+ self.white_balance_shift_red, self.white_balance_shift_blue
+ )?;
+
+ if self.white_balance == FujiWhiteBalance::Temperature {
+ writeln!(
+ f,
+ "White Balance Temperature: {}K",
+ self.white_balance_temperature
+ )?;
+ }
+
+ if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
+ writeln!(f, "Dynamic Range: {}", self.dynamic_range)?;
+ }
+
+ writeln!(f, "Dynamic Range Priority: {}", self.dynamic_range_priority)?;
+
+ writeln!(
+ f,
+ "Lens Modulation Optimizer: {}",
+ self.lens_modulation_optimizer
+ )?;
+ writeln!(f, "Color Space: {}", self.color_space)
+ }
+}
+
+pub struct FujiBackupObjectInfo {
+ compressed_size: u32,
+}
+
+impl FujiBackupObjectInfo {
+ pub fn new(buffer_len: usize) -> anyhow::Result {
+ Ok(Self {
+ compressed_size: u32::try_from(buffer_len)?,
+ })
+ }
+}
+
+impl PtpSerialize for FujiBackupObjectInfo {
+ fn try_into_ptp(&self) -> io::Result> {
+ let mut buf = Vec::new();
+ self.try_write_ptp(&mut buf)?;
+ Ok(buf)
+ }
+
+ fn try_write_ptp(&self, buf: &mut Vec) -> io::Result<()> {
+ let object_info = ObjectInfo {
+ object_format: ObjectFormat::FujiBackup,
+ compressed_size: self.compressed_size,
+ ..Default::default()
+ };
+
+ object_info.try_write_ptp(buf)?;
+
+ // TODO: What is this?
+ buf.write_all(&[0x0u8; 1020])?;
+
+ Ok(())
+ }
+}
+
+pub trait XTransV {
+ fn validate_monochromatic(
+ final_options: &FilmSimulationOptions,
+ prev_simulation: FujiFilmSimulation,
+ ) -> Result {
+ let mut fail = true;
+
+ if !matches!(
+ prev_simulation,
+ FujiFilmSimulation::Monochrome
+ | FujiFilmSimulation::MonochromeYe
+ | FujiFilmSimulation::MonochromeR
+ | FujiFilmSimulation::MonochromeG
+ | FujiFilmSimulation::AcrosSTD
+ | FujiFilmSimulation::AcrosYe
+ | FujiFilmSimulation::AcrosR
+ | FujiFilmSimulation::AcrosG
+ ) && (final_options.monochromatic_color_temperature.is_some()
+ || final_options.monochromatic_color_tint.is_some())
+ {
+ if final_options.monochromatic_color_temperature.is_some() {
+ error!(
+ "A B&W film simulation is not selected, refusing to set monochromatic color temperature"
+ );
+ fail = false;
+ }
+
+ if final_options.monochromatic_color_tint.is_some() {
+ error!(
+ "A B&W film simulation is not selected, refusing to set monochromatic color tint"
+ );
+ fail = false;
+ }
+ }
+
+ Ok(fail)
+ }
+
+ fn validate_white_balance_temperature(
+ final_options: &FilmSimulationOptions,
+ prev_white_balance: FujiWhiteBalance,
+ ) -> Result {
+ if prev_white_balance != FujiWhiteBalance::Temperature
+ && final_options.white_balance_temperature.is_some()
+ {
+ error!("White Balance mode is not set to 'Temperature', refusing to set temperature");
+ Ok(false)
+ } else {
+ Ok(true)
+ }
+ }
+
+ fn validate_exposure(
+ final_options: &FilmSimulationOptions,
+ previous_dynamic_range_priority: FujiDynamicRangePriority,
+ ) -> Result {
+ let mut fail = true;
+
+ if previous_dynamic_range_priority != FujiDynamicRangePriority::Off
+ && (final_options.dynamic_range.is_some()
+ || final_options.highlight.is_some()
+ || final_options.shadow.is_some())
+ {
+ if final_options.dynamic_range.is_some() {
+ error!("Dynamic Range Priority is enabled, refusing to set dynamic range");
+ fail = false;
+ }
+
+ if final_options.highlight.is_some() {
+ error!("Dynamic Range Priority is enabled, refusing to set highlight tone");
+ fail = false;
+ }
+
+ if final_options.shadow.is_some() {
+ error!("Dynamic Range Priority is enabled, refusing to set shadow tone");
+ fail = false;
+ }
+ }
+
+ Ok(fail)
+ }
+}
diff --git a/src/camera/devices/x_trans_v/x_t5/mod.rs b/src/camera/devices/x_trans_v/x_t5/mod.rs
new file mode 100644
index 0000000..5a0aac9
--- /dev/null
+++ b/src/camera/devices/x_trans_v/x_t5/mod.rs
@@ -0,0 +1,308 @@
+use anyhow::{Ok, anyhow, bail};
+use log::debug;
+use ptp_cursor::{PtpDeserialize, PtpSerialize};
+use rusb::GlobalContext;
+use strum::IntoEnumIterator;
+
+use crate::{
+ camera::{
+ CameraImpl, CameraResult, camera_prop_getter, camera_prop_setter, camera_set_prop_if_some,
+ devices::{SupportedCamera, x_trans_v::FujiBackupObjectInfo},
+ ptp::{
+ Ptp,
+ hex::{
+ CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect,
+ FujiColorChromeFXBlue, FujiColorSpace, FujiCustomSetting, FujiCustomSettingName,
+ FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect,
+ FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize,
+ FujiLensModulationOptimizer, FujiMonochromaticColorTemperature,
+ FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect,
+ FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode,
+ },
+ },
+ },
+ cli::{common::film::FilmSimulationOptions, simulation::SetFilmSimulationOptions},
+};
+
+use super::{CameraInfo, Simulation, SimulationListItem, XTransV};
+
+pub const FUJIFILM_XT5: SupportedCamera = SupportedCamera {
+ name: "FUJIFILM XT-5",
+ vendor: 0x04cb,
+ product: 0x02fc,
+ impl_factory: || Box::new(FujifilmXT5 {}),
+};
+
+pub struct FujifilmXT5 {}
+
+impl XTransV for FujifilmXT5 {}
+
+impl FujifilmXT5 {
+ camera_prop_getter!(get_usb_mode: UsbMode => DevicePropCode::FujiUsbMode);
+
+ fn get_battery_info(&self, ptp: &mut Ptp) -> anyhow::Result {
+ let data = ptp.get_prop_value(DevicePropCode::FujiBatteryInfo2, self.timeout())?;
+
+ debug!("Raw battery data: {data:?}");
+
+ let raw_string = String::try_from_ptp(&data)?;
+ debug!("Decoded raw string: {raw_string}");
+
+ let percentage: u32 = raw_string
+ .split(',')
+ .next()
+ .ok_or_else(|| anyhow!("Failed to parse battery percentage"))?
+ .parse()?;
+
+ Ok(percentage)
+ }
+
+ camera_prop_getter!(get_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName);
+ camera_prop_getter!(get_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize);
+ camera_prop_getter!(get_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality);
+ camera_prop_getter!(get_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange);
+ camera_prop_getter!(get_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority);
+ camera_prop_getter!(get_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation);
+ camera_prop_getter!(get_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature);
+ camera_prop_getter!(get_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint);
+ camera_prop_getter!(get_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect);
+ camera_prop_getter!(get_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance);
+ camera_prop_getter!(get_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR);
+ camera_prop_getter!(get_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone);
+ camera_prop_getter!(get_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone);
+ camera_prop_getter!(get_color: FujiColor => DevicePropCode::FujiCustomSettingColor);
+ camera_prop_getter!(get_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness);
+ camera_prop_getter!(get_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity);
+ camera_prop_getter!(get_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed);
+ camera_prop_getter!(get_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue);
+ camera_prop_getter!(get_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature);
+ camera_prop_getter!(get_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect);
+ camera_prop_getter!(get_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue);
+ camera_prop_getter!(get_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect);
+ camera_prop_getter!(get_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer);
+ camera_prop_getter!(get_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace);
+
+ camera_prop_setter!(set_active_custom_setting: FujiCustomSetting => DevicePropCode::FujiCustomSetting);
+ camera_prop_setter!(set_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName);
+ camera_prop_setter!(set_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize);
+ camera_prop_setter!(set_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality);
+ camera_prop_setter!(set_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange);
+ camera_prop_setter!(set_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority);
+ camera_prop_setter!(set_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation);
+ camera_prop_setter!(set_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature);
+ camera_prop_setter!(set_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint);
+ camera_prop_setter!(set_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect);
+ camera_prop_setter!(set_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance);
+ camera_prop_setter!(set_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR);
+ camera_prop_setter!(set_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone);
+ camera_prop_setter!(set_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone);
+ camera_prop_setter!(set_color: FujiColor => DevicePropCode::FujiCustomSettingColor);
+ camera_prop_setter!(set_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness);
+ camera_prop_setter!(set_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity);
+ camera_prop_setter!(set_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed);
+ camera_prop_setter!(set_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue);
+ camera_prop_setter!(set_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature);
+ camera_prop_setter!(set_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect);
+ camera_prop_setter!(set_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue);
+ camera_prop_setter!(set_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect);
+ camera_prop_setter!(set_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer);
+ camera_prop_setter!(set_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace);
+
+ fn validate_simulation_set(
+ &self,
+ ptp: &mut Ptp,
+ options: &FilmSimulationOptions,
+ ) -> Result<(), anyhow::Error> {
+ let prev_simulation = if let Some(simulation) = options.simulation {
+ simulation
+ } else {
+ self.get_film_simulation(ptp)?
+ };
+
+ let prev_white_balance = if let Some(white_balance) = options.white_balance {
+ white_balance
+ } else {
+ self.get_white_balance(ptp)?
+ };
+
+ let prev_dynamic_range_priority =
+ if let Some(dynamic_range_priority) = options.dynamic_range_priority {
+ dynamic_range_priority
+ } else {
+ self.get_dynamic_range_priority(ptp)?
+ };
+
+ if !Self::validate_monochromatic(options, prev_simulation)?
+ || !Self::validate_white_balance_temperature(options, prev_white_balance)?
+ || !Self::validate_exposure(options, prev_dynamic_range_priority)?
+ {
+ bail!("Incompatible options detected")
+ }
+
+ Ok(())
+ }
+}
+
+impl CameraImpl for FujifilmXT5 {
+ fn supported_camera(&self) -> &'static SupportedCamera {
+ &FUJIFILM_XT5
+ }
+
+ fn chunk_size(&self) -> usize {
+ 16128 * 1024
+ }
+
+ fn info_get(&self, ptp: &mut Ptp) -> anyhow::Result> {
+ let info = ptp.get_info(self.timeout())?;
+
+ let mode = self.get_usb_mode(ptp)?;
+ let battery = self.get_battery_info(ptp)?;
+
+ let repr = CameraInfo {
+ manufacturer: info.manufacturer.clone(),
+ model: info.model.clone(),
+ device_version: info.device_version.clone(),
+ serial_number: info.serial_number,
+ mode,
+ battery,
+ };
+
+ let repr = Box::new(repr);
+
+ Ok(repr)
+ }
+
+ fn simulation_list(&self, ptp: &mut Ptp) -> anyhow::Result>> {
+ let mut slots = Vec::new();
+
+ for slot in FujiCustomSetting::iter() {
+ self.set_active_custom_setting(ptp, &slot)?;
+ let name = self.get_custom_setting_name(ptp)?;
+ let repr = SimulationListItem { slot, name };
+ let repr: Box = Box::new(repr);
+ slots.push(repr);
+ }
+
+ Ok(slots)
+ }
+
+ fn simulation_get(
+ &self,
+ ptp: &mut Ptp,
+ slot: FujiCustomSetting,
+ ) -> anyhow::Result> {
+ self.set_active_custom_setting(ptp, &slot)?;
+
+ let repr = Simulation {
+ name: self.get_custom_setting_name(ptp)?,
+ size: self.get_image_size(ptp)?,
+ quality: self.get_image_quality(ptp)?,
+ simulation: self.get_film_simulation(ptp)?,
+ monochromatic_color_temperature: self.get_monochromatic_color_temperature(ptp)?,
+ monochromatic_color_tint: self.get_monochromatic_color_tint(ptp)?,
+ highlight: self.get_highlight_tone(ptp)?,
+ shadow: self.get_shadow_tone(ptp)?,
+ color: self.get_color(ptp)?,
+ sharpness: self.get_sharpness(ptp)?,
+ clarity: self.get_clarity(ptp)?,
+ noise_reduction: self.get_high_iso_nr(ptp)?,
+ grain: self.get_grain_effect(ptp)?,
+ color_chrome_effect: self.get_color_chrome_effect(ptp)?,
+ color_chrome_fx_blue: self.get_color_chrome_fx_blue(ptp)?,
+ smooth_skin_effect: self.get_smooth_skin_effect(ptp)?,
+ white_balance: self.get_white_balance(ptp)?,
+ white_balance_shift_red: self.get_white_balance_shift_red(ptp)?,
+ white_balance_shift_blue: self.get_white_balance_shift_blue(ptp)?,
+ white_balance_temperature: self.get_white_balance_temperature(ptp)?,
+ dynamic_range: self.get_dynamic_range(ptp)?,
+ dynamic_range_priority: self.get_dynamic_range_priority(ptp)?,
+ lens_modulation_optimizer: self.get_lens_modulation_optimizer(ptp)?,
+ color_space: self.get_color_space(ptp)?,
+ };
+
+ let repr = Box::new(repr);
+
+ Ok(repr)
+ }
+
+ fn simulation_set(
+ &self,
+ ptp: &mut Ptp,
+ slot: FujiCustomSetting,
+ set_options: &SetFilmSimulationOptions,
+ options: &FilmSimulationOptions,
+ ) -> anyhow::Result<()> {
+ self.set_active_custom_setting(ptp, &slot)?;
+
+ self.validate_simulation_set(ptp, options)?;
+
+ camera_set_prop_if_some!(self, ptp, set_options,
+ name => set_custom_setting_name,
+ );
+
+ camera_set_prop_if_some!(self, ptp, options,
+ size => set_image_size,
+ quality => set_image_quality,
+ simulation => set_film_simulation,
+ monochromatic_color_temperature => set_monochromatic_color_temperature,
+ monochromatic_color_tint => set_monochromatic_color_tint,
+ color => set_color,
+ sharpness => set_sharpness,
+ clarity => set_clarity,
+ noise_reduction => set_high_iso_nr,
+ grain => set_grain_effect,
+ color_chrome_effect => set_color_chrome_effect,
+ color_chrome_fx_blue => set_color_chrome_fx_blue,
+ smooth_skin_effect => set_smooth_skin_effect,
+ white_balance => set_white_balance,
+ white_balance_temperature => set_white_balance_temperature,
+ white_balance_shift_red => set_white_balance_shift_red,
+ white_balance_shift_blue => set_white_balance_shift_blue,
+ dynamic_range_priority => set_dynamic_range_priority,
+ dynamic_range => set_dynamic_range,
+ highlight => set_highlight_tone,
+ shadow => set_shadow_tone,
+ lens_modulation_optimizer => set_lens_modulation_optimizer,
+ color_space => set_color_space,
+ );
+
+ Ok(())
+ }
+
+ fn backup_export(&self, ptp: &mut Ptp) -> anyhow::Result> {
+ const HANDLE: u32 = 0x0;
+
+ debug!("Sending GetObjectInfo command for backup");
+ let response = ptp.send(CommandCode::GetObjectInfo, &[HANDLE], None, self.timeout())?;
+ debug!("Received response with {} bytes", response.len());
+
+ debug!("Sending GetObject command for backup");
+ let response = ptp.send(CommandCode::GetObject, &[HANDLE], None, self.timeout())?;
+ debug!("Received response with {} bytes", response.len());
+
+ Ok(response)
+ }
+
+ fn backup_import(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> {
+ debug!("Sending SendObjectInfo command for backup");
+ let object_info = FujiBackupObjectInfo::new(buffer.len())?;
+ let response = ptp.send(
+ CommandCode::SendObjectInfo,
+ &[0x0, 0x0],
+ Some(&object_info.try_into_ptp()?),
+ self.timeout(),
+ )?;
+ debug!("Received response with {} bytes", response.len());
+
+ debug!("Sending SendObject command for backup");
+ let response = ptp.send(
+ CommandCode::SendObject,
+ &[0x0],
+ Some(buffer),
+ self.timeout(),
+ )?;
+ debug!("Received response with {} bytes", response.len());
+
+ Ok(())
+ }
+}
diff --git a/src/camera/mod.rs b/src/camera/mod.rs
index 42b60e1..ba7dce4 100644
--- a/src/camera/mod.rs
+++ b/src/camera/mod.rs
@@ -2,29 +2,19 @@ pub mod devices;
pub mod error;
pub mod ptp;
-use std::time::Duration;
+use std::{fmt, time::Duration};
-use anyhow::{anyhow, bail};
-use devices::SupportedCamera;
-use log::{debug, error};
-use ptp::{
- Ptp,
- hex::{
- CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect,
- FujiColorChromeFXBlue, FujiColorSpace, FujiCustomSetting, FujiCustomSettingName,
- FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect,
- FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize,
- FujiLensModulationOptimizer, FujiMonochromaticColorTemperature, FujiMonochromaticColorTint,
- FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect, FujiWhiteBalance,
- FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, ObjectFormat, UsbMode,
- },
- structs::{DeviceInfo, ObjectInfo},
+use crate::{
+ cli::common::film::FilmSimulationOptions, cli::simulation::SetFilmSimulationOptions,
+ usb::find_endpoint,
};
-use ptp_cursor::{PtpDeserialize, PtpSerialize};
+use anyhow::bail;
+use devices::SupportedCamera;
+use erased_serde::serialize_trait_object;
+use log::{debug, error};
+use ptp::{Ptp, hex::FujiCustomSetting};
use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE};
-use crate::usb::find_endpoint;
-
const SESSION: u32 = 1;
pub struct Camera {
@@ -32,25 +22,23 @@ pub struct Camera {
pub ptp: Ptp,
}
-macro_rules! camera_with_ptp {
- ($($fn_name:ident => $ret:ty),* $(,)?) => {
- $(
- #[allow(dead_code)]
- pub fn $fn_name(&mut self) -> anyhow::Result<$ret> {
- self.r#impl.$fn_name(&mut self.ptp)
- }
- )*
+macro_rules! camera_to_impl_with_ptp {
+ ($name:ident -> $ret:ty) => {
+ pub fn $name(&mut self) -> $ret {
+ self.r#impl.$name(&mut self.ptp)
+ }
};
- ($($fn_name:ident($($arg:ident : $arg_ty:ty),*) => $ret:ty),* $(,)?) => {
- $(
- #[allow(dead_code)]
- pub fn $fn_name(&mut self, $($arg: $arg_ty),*) -> anyhow::Result<$ret> {
- self.r#impl.$fn_name(&mut self.ptp, $($arg),*)
- }
- )*
+ ($name:ident($($arg:ident: $arg_ty:ty),*) -> $ret:ty) => {
+ pub fn $name(&mut self, $($arg: $arg_ty),*) -> $ret {
+ self.r#impl.$name(&mut self.ptp, $($arg),*)
+ }
};
}
+pub trait CameraResult: fmt::Display + erased_serde::Serialize {}
+impl CameraResult for T {}
+serialize_trait_object!(CameraResult);
+
impl Camera {
pub fn name(&self) -> &'static str {
self.r#impl.supported_camera().name
@@ -68,69 +56,12 @@ impl Camera {
format!("{}.{}", self.ptp.bus, self.ptp.address)
}
- camera_with_ptp! {
- get_info => DeviceInfo,
- get_usb_mode => UsbMode,
- get_battery_info => u32,
- }
-
- camera_with_ptp! {
- export_backup => Vec,
- get_active_custom_setting => FujiCustomSetting,
- get_custom_setting_name => FujiCustomSettingName,
- get_image_size => FujiImageSize,
- get_image_quality => FujiImageQuality,
- get_dynamic_range => FujiDynamicRange,
- get_dynamic_range_priority => FujiDynamicRangePriority,
- get_film_simulation => FujiFilmSimulation,
- get_monochromatic_color_temperature => FujiMonochromaticColorTemperature,
- get_monochromatic_color_tint => FujiMonochromaticColorTint,
- get_grain_effect => FujiGrainEffect,
- get_white_balance => FujiWhiteBalance,
- get_high_iso_nr => FujiHighISONR,
- get_highlight_tone => FujiHighlightTone,
- get_shadow_tone => FujiShadowTone,
- get_color => FujiColor,
- get_sharpness => FujiSharpness,
- get_clarity => FujiClarity,
- get_white_balance_shift_red => FujiWhiteBalanceShift,
- get_white_balance_shift_blue => FujiWhiteBalanceShift,
- get_white_balance_temperature => FujiWhiteBalanceTemperature,
- get_color_chrome_effect => FujiColorChromeEffect,
- get_color_chrome_fx_blue => FujiColorChromeFXBlue,
- get_smooth_skin_effect => FujiSmoothSkinEffect,
- get_lens_modulation_optimizer => FujiLensModulationOptimizer,
- get_color_space => FujiColorSpace,
- }
-
- camera_with_ptp! {
- import_backup(buffer: &[u8]) => (),
- set_active_custom_setting(value: &FujiCustomSetting) => (),
- set_custom_setting_name(value: &FujiCustomSettingName) => (),
- set_image_size(value: &FujiImageSize) => (),
- set_image_quality(value: &FujiImageQuality) => (),
- set_dynamic_range(value: &FujiDynamicRange) => (),
- set_dynamic_range_priority(value: &FujiDynamicRangePriority) => (),
- set_film_simulation(value: &FujiFilmSimulation) => (),
- set_monochromatic_color_temperature(value: &FujiMonochromaticColorTemperature) => (),
- set_monochromatic_color_tint(value: &FujiMonochromaticColorTint) => (),
- set_grain_effect(value: &FujiGrainEffect) => (),
- set_white_balance(value: &FujiWhiteBalance) => (),
- set_high_iso_nr(value: &FujiHighISONR) => (),
- set_highlight_tone(value: &FujiHighlightTone) => (),
- set_shadow_tone(value: &FujiShadowTone) => (),
- set_color(value: &FujiColor) => (),
- set_sharpness(value: &FujiSharpness) => (),
- set_clarity(value: &FujiClarity) => (),
- set_white_balance_shift_red(value: &FujiWhiteBalanceShift) => (),
- set_white_balance_shift_blue(value: &FujiWhiteBalanceShift) => (),
- set_white_balance_temperature(value: &FujiWhiteBalanceTemperature) => (),
- set_color_chrome_effect(value: &FujiColorChromeEffect) => (),
- set_color_chrome_fx_blue(value: &FujiColorChromeFXBlue) => (),
- set_smooth_skin_effect(value: &FujiSmoothSkinEffect) => (),
- set_lens_modulation_optimizer(value: &FujiLensModulationOptimizer) => (),
- set_color_space(value: &FujiColorSpace) => (),
- }
+ camera_to_impl_with_ptp!(info_get() -> anyhow::Result>);
+ camera_to_impl_with_ptp!(backup_export -> anyhow::Result>);
+ camera_to_impl_with_ptp!(backup_import(buffer: &[u8]) -> anyhow::Result<()>);
+ camera_to_impl_with_ptp!(simulation_list -> anyhow::Result>>);
+ camera_to_impl_with_ptp!(simulation_get(slot: FujiCustomSetting) -> anyhow::Result>);
+ camera_to_impl_with_ptp!(simulation_set(slot: FujiCustomSetting, set_options: &SetFilmSimulationOptions, options: &FilmSimulationOptions) -> anyhow::Result<()>);
}
impl Drop for Camera {
@@ -213,28 +144,6 @@ impl TryFrom<&rusb::Device> for Camera {
}
}
-macro_rules! camera_impl_custom_settings {
- ($( $name:ident : $type:ty => $code:expr ),+ $(,)?) => {
- $(
- paste::paste! {
- #[allow(dead_code)]
- fn [](&self, ptp: &mut Ptp) -> anyhow::Result<$type> {
- let bytes = ptp.get_prop_value($code, self.timeout())?;
- let result = <$type>::try_from_ptp(&bytes)?;
- Ok(result)
- }
-
- #[allow(dead_code)]
- fn [](&self, ptp: &mut Ptp, value: &$type) -> anyhow::Result<()> {
- let bytes = value.try_into_ptp()?;
- ptp.set_prop_value($code, &bytes, self.timeout())?;
- Ok(())
- }
- }
- )+
- };
-}
-
pub trait CameraImpl {
fn supported_camera(&self) -> &'static SupportedCamera;
@@ -247,110 +156,80 @@ pub trait CameraImpl {
1024 * 1024
}
- fn get_info(&mut self, ptp: &mut Ptp) -> anyhow::Result {
- let info = ptp.get_info(self.timeout())?;
- Ok(info)
+ fn info_get(&self, _ptp: &mut Ptp) -> anyhow::Result> {
+ bail!("This device does not support getting detailed info")
}
- fn get_usb_mode(&mut self, ptp: &mut Ptp) -> anyhow::Result {
- let data = ptp.get_prop_value(DevicePropCode::FujiUsbMode, self.timeout())?;
- let result = UsbMode::try_from_ptp(&data)?;
- Ok(result)
+ fn backup_export(&self, _ptp: &mut Ptp) -> anyhow::Result> {
+ bail!("This device does not support exporting backups")
}
- fn get_battery_info(&mut self, ptp: &mut Ptp) -> anyhow::Result {
- let data = ptp.get_prop_value(DevicePropCode::FujiBatteryInfo2, self.timeout())?;
-
- debug!("Raw battery data: {data:?}");
-
- let raw_string = String::try_from_ptp(&data)?;
- debug!("Decoded raw string: {raw_string}");
-
- let percentage: u32 = raw_string
- .split(',')
- .next()
- .ok_or_else(|| anyhow!("Failed to parse battery percentage"))?
- .parse()?;
-
- Ok(percentage)
+ fn backup_import(&self, _ptp: &mut Ptp, _buffer: &[u8]) -> anyhow::Result<()> {
+ bail!("This device does not support importing backups")
}
- fn export_backup(&self, ptp: &mut Ptp) -> anyhow::Result> {
- const HANDLE: u32 = 0x0;
-
- debug!("Sending GetObjectInfo command for backup");
- let response = ptp.send(CommandCode::GetObjectInfo, &[HANDLE], None, self.timeout())?;
- debug!("Received response with {} bytes", response.len());
-
- debug!("Sending GetObject command for backup");
- let response = ptp.send(CommandCode::GetObject, &[HANDLE], None, self.timeout())?;
- debug!("Received response with {} bytes", response.len());
-
- Ok(response)
+ fn simulation_list(&self, _ptp: &mut Ptp) -> anyhow::Result>> {
+ bail!("This device does not support listing simulations")
}
- fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> {
- debug!("Preparing ObjectInfo header for backup");
-
- let mut header = Vec::with_capacity(1076);
-
- let object_info = ObjectInfo {
- object_format: ObjectFormat::FujiBackup,
- compressed_size: u32::try_from(buffer.len())?,
- ..Default::default()
- };
- object_info.try_write_ptp(&mut header)?;
- // TODO: What is this?
- for _ in 0..1020 {
- 0x0u8.try_write_ptp(&mut header)?;
- }
-
- debug!("Sending SendObjectInfo command for backup");
- let response = ptp.send(
- CommandCode::SendObjectInfo,
- &[0x0, 0x0],
- Some(&header),
- self.timeout(),
- )?;
- debug!("Received response with {} bytes", response.len());
-
- debug!("Sending SendObject command for backup");
- let response = ptp.send(
- CommandCode::SendObject,
- &[0x0],
- Some(buffer),
- self.timeout(),
- )?;
- debug!("Received response with {} bytes", response.len());
-
- Ok(())
+ fn simulation_get(
+ &self,
+ _ptp: &mut Ptp,
+ _slot: FujiCustomSetting,
+ ) -> anyhow::Result> {
+ bail!("This device does not support getting simulation options")
}
- camera_impl_custom_settings! {
- active_custom_setting: FujiCustomSetting => DevicePropCode::FujiCustomSetting,
- custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName,
- image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize,
- image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality,
- dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange,
- dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority,
- film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation,
- monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature,
- monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint,
- grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect,
- white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance,
- high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR,
- highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone,
- shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone,
- color: FujiColor => DevicePropCode::FujiCustomSettingColor,
- sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness,
- clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity,
- white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed,
- white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue,
- white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature,
- color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect,
- color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue,
- smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect,
- lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer,
- color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace,
+ fn simulation_set(
+ &self,
+ _ptp: &mut Ptp,
+ _slot: FujiCustomSetting,
+ _set_options: &SetFilmSimulationOptions,
+ _options: &FilmSimulationOptions,
+ ) -> anyhow::Result<()> {
+ bail!("This device does not support setting simulation options")
}
}
+
+macro_rules! camera_prop_getter {
+ ($name:ident: $type:ty => $code:expr) => {
+ pub fn $name(&self, ptp: &mut crate::camera::ptp::Ptp) -> anyhow::Result<$type> {
+ use ptp_cursor::PtpDeserialize;
+
+ let bytes = ptp.get_prop_value($code, self.timeout())?;
+ let result = <$type>::try_from_ptp(&bytes)?;
+ Ok(result)
+ }
+ };
+}
+
+macro_rules! camera_prop_setter {
+ ($name:ident: $type:ty => $code:expr) => {
+ pub fn $name(
+ &self,
+ ptp: &mut crate::camera::ptp::Ptp,
+ value: &$type,
+ ) -> anyhow::Result<()> {
+ use ptp_cursor::PtpSerialize;
+
+ let bytes = value.try_into_ptp()?;
+ ptp.set_prop_value($code, &bytes, self.timeout())?;
+ Ok(())
+ }
+ };
+}
+
+macro_rules! camera_set_prop_if_some {
+ ($self:ident, $ptp:ident, $options:ident,
+ $( $field:ident => $setter:ident ),* $(,)? ) => {
+ $(
+ if let Some(val) = &$options.$field {
+ $self.$setter($ptp, val)?;
+ }
+ )*
+ };
+}
+
+pub(crate) use camera_prop_getter;
+pub(crate) use camera_prop_setter;
+pub(crate) use camera_set_prop_if_some;
diff --git a/src/camera/ptp/structs.rs b/src/camera/ptp/structs.rs
index 8bc3f24..72df8fd 100644
--- a/src/camera/ptp/structs.rs
+++ b/src/camera/ptp/structs.rs
@@ -2,7 +2,6 @@ use ptp_macro::{PtpDeserialize, PtpSerialize};
use super::hex::{CommandCode, ContainerCode, ContainerType, ObjectFormat};
-#[allow(dead_code)]
#[derive(Debug, PtpSerialize, PtpDeserialize)]
pub struct DeviceInfo {
pub version: u16,
diff --git a/src/cli/backup/mod.rs b/src/cli/backup/mod.rs
index 9511498..fa912eb 100644
--- a/src/cli/backup/mod.rs
+++ b/src/cli/backup/mod.rs
@@ -24,7 +24,7 @@ fn handle_export(device_id: Option<&str>, output: &Output) -> anyhow::Result<()>
let mut camera = usb::get_camera(device_id)?;
let mut writer = output.get_writer()?;
- let backup = camera.export_backup()?;
+ let backup = camera.backup_export()?;
writer.write_all(&backup)?;
Ok(())
@@ -36,7 +36,7 @@ fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> {
let mut reader = input.get_reader()?;
let mut backup = Vec::new();
reader.read_to_end(&mut backup)?;
- camera.import_backup(&backup)?;
+ camera.backup_import(&backup)?;
Ok(())
}
diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs
index e278fd9..1ab5556 100644
--- a/src/cli/device/mod.rs
+++ b/src/cli/device/mod.rs
@@ -1,12 +1,6 @@
-use std::fmt;
-
use clap::Subcommand;
-use serde::Serialize;
-use crate::{
- camera::{Camera, ptp::hex::UsbMode},
- usb,
-};
+use crate::{camera::devices::CameraInfoListItem, usb};
#[derive(Subcommand, Debug, Clone, Copy)]
pub enum DeviceCmd {
@@ -19,38 +13,8 @@ pub enum DeviceCmd {
Info,
}
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct CameraItemRepr {
- pub name: &'static str,
- pub usb_id: String,
- pub vendor_id: String,
- pub product_id: String,
-}
-
-impl From<&Camera> for CameraItemRepr {
- fn from(camera: &Camera) -> Self {
- Self {
- name: camera.name(),
- usb_id: camera.connected_usb_id(),
- vendor_id: format!("0x{:04x}", camera.vendor_id()),
- product_id: format!("0x{:04x}", camera.product_id()),
- }
- }
-}
-
-impl fmt::Display for CameraItemRepr {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(
- f,
- "{} ({}:{}) (USB ID: {})",
- self.name, self.vendor_id, self.product_id, self.usb_id
- )
- }
-}
-
fn handle_list(json: bool) -> anyhow::Result<()> {
- let cameras: Vec = usb::get_connected_cameras()?
+ let cameras: Vec = usb::get_connected_cameras()?
.iter()
.map(std::convert::Into::into)
.collect();
@@ -65,7 +29,6 @@ fn handle_list(json: bool) -> anyhow::Result<()> {
return Ok(());
}
- println!("Connected Cameras:");
for d in cameras {
println!("- {d}");
}
@@ -73,54 +36,10 @@ fn handle_list(json: bool) -> anyhow::Result<()> {
Ok(())
}
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct CameraRepr {
- #[serde(flatten)]
- pub device: CameraItemRepr,
-
- pub manufacturer: String,
- pub model: String,
- pub device_version: String,
- pub serial_number: String,
- pub mode: UsbMode,
- pub battery: u32,
-}
-
-impl fmt::Display for CameraRepr {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- writeln!(f, "Name: {}", self.device.name)?;
- writeln!(f, "USB ID: {}", self.device.usb_id)?;
- writeln!(
- f,
- "Vendor ID: {}, Product ID: {}",
- self.device.vendor_id, self.device.product_id
- )?;
- writeln!(f, "Manufacturer: {}", self.manufacturer)?;
- writeln!(f, "Model: {}", self.model)?;
- writeln!(f, "Version: {}", self.device_version)?;
- writeln!(f, "Serial Number: {}", self.serial_number)?;
- writeln!(f, "Mode: {}", self.mode)?;
- write!(f, "Battery: {}%", self.battery)
- }
-}
-
fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
- let info = camera.get_info()?;
- let mode = camera.get_usb_mode()?;
- let battery = camera.get_battery_info()?;
-
- let repr = CameraRepr {
- device: (&camera).into(),
- manufacturer: info.manufacturer.clone(),
- model: info.model.clone(),
- device_version: info.device_version.clone(),
- serial_number: info.serial_number,
- mode,
- battery,
- };
+ let repr = camera.info_get()?;
if json {
println!("{}", serde_json::to_string_pretty(&repr)?);
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
index 503fb80..8d297dd 100644
--- a/src/cli/mod.rs
+++ b/src/cli/mod.rs
@@ -1,6 +1,5 @@
-mod common;
-
pub mod backup;
+pub mod common;
pub mod device;
pub mod render;
pub mod simulation;
diff --git a/src/cli/simulation/mod.rs b/src/cli/simulation/mod.rs
index d9319cd..0e705cc 100644
--- a/src/cli/simulation/mod.rs
+++ b/src/cli/simulation/mod.rs
@@ -1,14 +1,5 @@
-use std::fmt;
-
use crate::{
- camera::ptp::hex::{
- FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace,
- FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority,
- FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality,
- FujiImageSize, FujiLensModulationOptimizer, FujiMonochromaticColorTemperature,
- FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect,
- FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature,
- },
+ camera::ptp::hex::{FujiCustomSetting, FujiCustomSettingName},
usb,
};
@@ -17,9 +8,6 @@ use super::common::{
film::FilmSimulationOptions,
};
use clap::{Args, Subcommand};
-use log::warn;
-use serde::Serialize;
-use strum::IntoEnumIterator;
#[derive(Subcommand, Debug)]
pub enum SimulationCmd {
@@ -72,173 +60,27 @@ pub enum SimulationCmd {
pub struct SetFilmSimulationOptions {
/// The name of the slot
#[clap(long)]
- name: Option,
-}
-
-#[derive(Debug, Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct CustomSettingRepr {
- pub slot: FujiCustomSetting,
- pub name: FujiCustomSettingName,
+ pub name: Option,
}
fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
-
- let mut slots = Vec::new();
-
- for slot in FujiCustomSetting::iter() {
- camera.set_active_custom_setting(&slot)?;
- let name = camera.get_custom_setting_name()?;
- slots.push(CustomSettingRepr { slot, name });
- }
+ let slots = camera.simulation_list()?;
if json {
println!("{}", serde_json::to_string_pretty(&slots)?);
} else {
- println!("Film Simulations:");
- for slot in slots {
- println!("- {}: {}", slot.slot, slot.name);
+ for repr in slots {
+ println!("- {repr}");
}
}
Ok(())
}
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-pub struct FilmSimulationRepr {
- pub name: FujiCustomSettingName,
- pub size: FujiImageSize,
- pub quality: FujiImageQuality,
- pub simulation: FujiFilmSimulation,
- pub monochromatic_color_temperature: FujiMonochromaticColorTemperature,
- pub monochromatic_color_tint: FujiMonochromaticColorTint,
- pub highlight: FujiHighlightTone,
- pub shadow: FujiShadowTone,
- pub color: FujiColor,
- pub sharpness: FujiSharpness,
- pub clarity: FujiClarity,
- pub noise_reduction: FujiHighISONR,
- pub grain: FujiGrainEffect,
- pub color_chrome_effect: FujiColorChromeEffect,
- pub color_chrome_fx_blue: FujiColorChromeFXBlue,
- pub smooth_skin_effect: FujiSmoothSkinEffect,
- pub white_balance: FujiWhiteBalance,
- pub white_balance_shift_red: FujiWhiteBalanceShift,
- pub white_balance_shift_blue: FujiWhiteBalanceShift,
- pub white_balance_temperature: FujiWhiteBalanceTemperature,
- pub dynamic_range: FujiDynamicRange,
- pub dynamic_range_priority: FujiDynamicRangePriority,
- pub lens_modulation_optimizer: FujiLensModulationOptimizer,
- pub color_space: FujiColorSpace,
-}
-
-impl fmt::Display for FilmSimulationRepr {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- writeln!(f, "Name: {}", self.name)?;
- writeln!(f, "Size: {}", self.size)?;
- writeln!(f, "Quality: {}", self.quality)?;
-
- writeln!(f, "Simulation: {}", self.simulation)?;
-
- match self.simulation {
- FujiFilmSimulation::Monochrome
- | FujiFilmSimulation::MonochromeYe
- | FujiFilmSimulation::MonochromeR
- | FujiFilmSimulation::MonochromeG
- | FujiFilmSimulation::AcrosSTD
- | FujiFilmSimulation::AcrosYe
- | FujiFilmSimulation::AcrosR
- | FujiFilmSimulation::AcrosG => {
- writeln!(
- f,
- "Monochromatic Color Temperature: {}",
- self.monochromatic_color_temperature
- )?;
- writeln!(
- f,
- "Monochromatic Color Tint: {}",
- self.monochromatic_color_tint
- )?;
- }
- _ => {}
- }
-
- if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
- writeln!(f, "Highlights: {}", self.highlight)?;
- writeln!(f, "Shadows: {}", self.shadow)?;
- }
-
- writeln!(f, "Color: {}", self.color)?;
- writeln!(f, "Sharpness: {}", self.sharpness)?;
- writeln!(f, "Clarity: {}", self.clarity)?;
- writeln!(f, "Noise Reduction: {}", self.noise_reduction)?;
- writeln!(f, "Grain: {}", self.grain)?;
- writeln!(f, "Color Chrome Effect: {}", self.color_chrome_effect)?;
- writeln!(f, "Color Chrome FX Blue: {}", self.color_chrome_fx_blue)?;
- writeln!(f, "Smooth Skin Effect: {}", self.smooth_skin_effect)?;
-
- writeln!(f, "White Balance: {}", self.white_balance)?;
- writeln!(
- f,
- "White Balance Shift (R/B): {} / {}",
- self.white_balance_shift_red, self.white_balance_shift_blue
- )?;
-
- if self.white_balance == FujiWhiteBalance::Temperature {
- writeln!(
- f,
- "White Balance Temperature: {}K",
- self.white_balance_temperature
- )?;
- }
-
- if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
- writeln!(f, "Dynamic Range: {}", self.dynamic_range)?;
- }
-
- writeln!(f, "Dynamic Range Priority: {}", self.dynamic_range_priority)?;
-
- writeln!(
- f,
- "Lens Modulation Optimizer: {}",
- self.lens_modulation_optimizer
- )?;
- writeln!(f, "Color Space: {}", self.color_space)
- }
-}
-
fn handle_get(json: bool, device_id: Option<&str>, slot: FujiCustomSetting) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
- camera.set_active_custom_setting(&slot)?;
-
- let repr = FilmSimulationRepr {
- name: camera.get_custom_setting_name()?,
- size: camera.get_image_size()?,
- quality: camera.get_image_quality()?,
- simulation: camera.get_film_simulation()?,
- monochromatic_color_temperature: camera.get_monochromatic_color_temperature()?,
- monochromatic_color_tint: camera.get_monochromatic_color_tint()?,
- highlight: camera.get_highlight_tone()?,
- shadow: camera.get_shadow_tone()?,
- color: camera.get_color()?,
- sharpness: camera.get_sharpness()?,
- clarity: camera.get_clarity()?,
- noise_reduction: camera.get_high_iso_nr()?,
- grain: camera.get_grain_effect()?,
- color_chrome_effect: camera.get_color_chrome_effect()?,
- color_chrome_fx_blue: camera.get_color_chrome_fx_blue()?,
- smooth_skin_effect: camera.get_smooth_skin_effect()?,
- white_balance: camera.get_white_balance()?,
- white_balance_shift_red: camera.get_white_balance_shift_red()?,
- white_balance_shift_blue: camera.get_white_balance_shift_blue()?,
- white_balance_temperature: camera.get_white_balance_temperature()?,
- dynamic_range: camera.get_dynamic_range()?,
- dynamic_range_priority: camera.get_dynamic_range_priority()?,
- lens_modulation_optimizer: camera.get_lens_modulation_optimizer()?,
- color_space: camera.get_color_space()?,
- };
+ let repr = camera.simulation_get(slot)?;
if json {
println!("{}", serde_json::to_string_pretty(&repr)?);
@@ -258,176 +100,7 @@ fn handle_set(
options: &FilmSimulationOptions,
) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
- camera.set_active_custom_setting(&slot)?;
-
- // General
- if let Some(name) = &set_options.name {
- camera.set_custom_setting_name(name)?;
- }
-
- if let Some(size) = &options.size {
- camera.set_image_size(size)?;
- }
-
- if let Some(quality) = &options.quality {
- camera.set_image_quality(quality)?;
- }
-
- // Style
- if let Some(simulation) = &options.simulation {
- camera.set_film_simulation(simulation)?;
- }
-
- if options.monochromatic_color_temperature.is_some()
- || options.monochromatic_color_tint.is_some()
- {
- let simulation = if let Some(simulation) = &options.simulation {
- simulation
- } else {
- &camera.get_film_simulation()?
- };
-
- let is_bnw = matches!(
- *simulation,
- FujiFilmSimulation::Monochrome
- | FujiFilmSimulation::MonochromeYe
- | FujiFilmSimulation::MonochromeR
- | FujiFilmSimulation::MonochromeG
- | FujiFilmSimulation::AcrosSTD
- | FujiFilmSimulation::AcrosYe
- | FujiFilmSimulation::AcrosR
- | FujiFilmSimulation::AcrosG
- );
-
- if let Some(monochromatic_color_temperature) = &options.monochromatic_color_temperature {
- if is_bnw {
- camera.set_monochromatic_color_temperature(monochromatic_color_temperature)?;
- } else {
- warn!(
- "A B&W film simulation is not selected, refusing to set monochromatic color temperature"
- );
- }
- }
-
- if let Some(monochromatic_color_tint) = &options.monochromatic_color_tint {
- if is_bnw {
- camera.set_monochromatic_color_tint(monochromatic_color_tint)?;
- } else {
- warn!(
- "A B&W film simulation is not selected, refusing to set monochromatic color tint"
- );
- }
- }
- }
-
- if let Some(color) = &options.color {
- camera.set_color(color)?;
- }
-
- if let Some(sharpness) = &options.sharpness {
- camera.set_sharpness(sharpness)?;
- }
-
- if let Some(clarity) = &options.clarity {
- camera.set_clarity(clarity)?;
- }
-
- if let Some(noise_reduction) = &options.noise_reduction {
- camera.set_high_iso_nr(noise_reduction)?;
- }
-
- if let Some(grain) = &options.grain {
- camera.set_grain_effect(grain)?;
- }
-
- if let Some(color_chrome_effect) = &options.color_chrome_effect {
- camera.set_color_chrome_effect(color_chrome_effect)?;
- }
-
- if let Some(color_chrome_fx_blue) = &options.color_chrome_fx_blue {
- camera.set_color_chrome_fx_blue(color_chrome_fx_blue)?;
- }
-
- if let Some(smooth_skin_effect) = &options.smooth_skin_effect {
- camera.set_smooth_skin_effect(smooth_skin_effect)?;
- }
-
- // White Balance
- if let Some(white_balance) = &options.white_balance {
- camera.set_white_balance(white_balance)?;
- }
-
- if let Some(temperature) = &options.white_balance_temperature {
- let white_balance = if let Some(white_balance) = &options.white_balance {
- white_balance
- } else {
- &camera.get_white_balance()?
- };
-
- if *white_balance == FujiWhiteBalance::Temperature {
- camera.set_white_balance_temperature(temperature)?;
- } else {
- warn!("White Balance mode is not set to 'Temperature', refusing to set temperature");
- }
- }
-
- if let Some(shift_red) = &options.white_balance_shift_red {
- camera.set_white_balance_shift_red(shift_red)?;
- }
-
- if let Some(shift_blue) = &options.white_balance_shift_blue {
- camera.set_white_balance_shift_blue(shift_blue)?;
- }
-
- // Exposure
- if let Some(dynamic_range_priority) = &options.dynamic_range_priority {
- camera.set_dynamic_range_priority(dynamic_range_priority)?;
- }
-
- if options.dynamic_range.is_some() || options.highlight.is_some() || options.shadow.is_some() {
- let dynamic_range_priority =
- if let Some(dynamic_range_priority) = &options.dynamic_range_priority {
- dynamic_range_priority
- } else {
- &camera.get_dynamic_range_priority()?
- };
-
- let is_drp_off = *dynamic_range_priority == FujiDynamicRangePriority::Off;
-
- if let Some(dynamic_range) = &options.dynamic_range {
- if is_drp_off {
- camera.set_dynamic_range(dynamic_range)?;
- } else {
- warn!("Dynamic Range Priority is enabled, refusing to set dynamic range");
- }
- }
-
- if let Some(highlights) = &options.highlight {
- if is_drp_off {
- camera.set_highlight_tone(highlights)?;
- } else {
- warn!("Dynamic Range Priority is enabled, refusing to set highlight tone");
- }
- }
-
- if let Some(shadows) = &options.shadow {
- if is_drp_off {
- camera.set_shadow_tone(shadows)?;
- } else {
- warn!("Dynamic Range Priority is enabled, refusing to set shadow tone");
- }
- }
- }
-
- // Extras
- if let Some(lens_modulation_optimizer) = &options.lens_modulation_optimizer {
- camera.set_lens_modulation_optimizer(lens_modulation_optimizer)?;
- }
-
- if let Some(color_space) = &options.color_space {
- camera.set_color_space(color_space)?;
- }
-
+ camera.simulation_set(slot, set_options, options)?;
Ok(())
}