chore: refactor for device support

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-10-21 15:55:00 +03:00
parent 7e8599fa61
commit 4532fb1775
11 changed files with 754 additions and 663 deletions

18
Cargo.lock generated
View File

@@ -210,6 +210,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "errno" name = "errno"
version = "0.3.14" version = "0.3.14"
@@ -239,6 +250,7 @@ dependencies = [
"anyhow", "anyhow",
"byteorder", "byteorder",
"clap", "clap",
"erased-serde",
"log", "log",
"log4rs", "log4rs",
"num_enum", "num_enum",
@@ -840,6 +852,12 @@ dependencies = [
"winnow", "winnow",
] ]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]] [[package]]
name = "typemap-ors" name = "typemap-ors"
version = "1.0.0" version = "1.0.0"

View File

@@ -29,3 +29,4 @@ ptp_cursor = { path = "crates/ptp/cursor" }
strum = { version = "0.27.2", features = ["strum_macros"] } strum = { version = "0.27.2", features = ["strum_macros"] }
strum_macros = "0.27.2" strum_macros = "0.27.2"
paste = "1.0.15" paste = "1.0.15"
erased-serde = "0.4.8"

View File

@@ -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<P> = fn() -> Box<dyn CameraImpl<P>>; type ImplFactory<P> = fn() -> Box<dyn CameraImpl<P>>;
@@ -12,31 +17,34 @@ pub struct SupportedCamera<P: rusb::UsbContext> {
pub impl_factory: ImplFactory<P>, pub impl_factory: ImplFactory<P>,
} }
macro_rules! default_camera_impl { pub const SUPPORTED: &[SupportedCamera<GlobalContext>] = &[x_trans_v::x_t5::FUJIFILM_XT5];
(
$const_name:ident,
$struct_name:ident,
$vendor:expr,
$product:expr,
$display_name:expr
) => {
pub const $const_name: SupportedCamera<GlobalContext> = SupportedCamera {
name: $display_name,
vendor: $vendor,
product: $product,
impl_factory: || Box::new($struct_name {}),
};
pub struct $struct_name {} #[derive(Serialize)]
#[serde(rename_all = "camelCase")]
impl crate::camera::CameraImpl<GlobalContext> for $struct_name { pub struct CameraInfoListItem {
fn supported_camera(&self) -> &'static SupportedCamera<GlobalContext> { pub name: &'static str,
&$const_name 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<GlobalContext>] = &[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
)
}
}

View File

@@ -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<Self> {
Ok(Self {
compressed_size: u32::try_from(buffer_len)?,
})
}
}
impl PtpSerialize for FujiBackupObjectInfo {
fn try_into_ptp(&self) -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.try_write_ptp(&mut buf)?;
Ok(buf)
}
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> 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<bool, anyhow::Error> {
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<bool, anyhow::Error> {
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<bool, anyhow::Error> {
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)
}
}

View File

@@ -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<GlobalContext> = 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<u32> {
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<GlobalContext> for FujifilmXT5 {
fn supported_camera(&self) -> &'static SupportedCamera<GlobalContext> {
&FUJIFILM_XT5
}
fn chunk_size(&self) -> usize {
16128 * 1024
}
fn info_get(&self, ptp: &mut Ptp) -> anyhow::Result<Box<dyn CameraResult>> {
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<Vec<Box<dyn CameraResult>>> {
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<dyn CameraResult> = Box::new(repr);
slots.push(repr);
}
Ok(slots)
}
fn simulation_get(
&self,
ptp: &mut Ptp,
slot: FujiCustomSetting,
) -> anyhow::Result<Box<dyn CameraResult>> {
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<Vec<u8>> {
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(())
}
}

View File

@@ -2,29 +2,19 @@ pub mod devices;
pub mod error; pub mod error;
pub mod ptp; pub mod ptp;
use std::time::Duration; use std::{fmt, time::Duration};
use anyhow::{anyhow, bail}; use crate::{
use devices::SupportedCamera; cli::common::film::FilmSimulationOptions, cli::simulation::SetFilmSimulationOptions,
use log::{debug, error}; usb::find_endpoint,
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 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 rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE};
use crate::usb::find_endpoint;
const SESSION: u32 = 1; const SESSION: u32 = 1;
pub struct Camera { pub struct Camera {
@@ -32,25 +22,23 @@ pub struct Camera {
pub ptp: Ptp, pub ptp: Ptp,
} }
macro_rules! camera_with_ptp { macro_rules! camera_to_impl_with_ptp {
($($fn_name:ident => $ret:ty),* $(,)?) => { ($name:ident -> $ret:ty) => {
$( pub fn $name(&mut self) -> $ret {
#[allow(dead_code)] self.r#impl.$name(&mut self.ptp)
pub fn $fn_name(&mut self) -> anyhow::Result<$ret> {
self.r#impl.$fn_name(&mut self.ptp)
} }
)*
}; };
($($fn_name:ident($($arg:ident : $arg_ty:ty),*) => $ret:ty),* $(,)?) => { ($name:ident($($arg:ident: $arg_ty:ty),*) -> $ret:ty) => {
$( pub fn $name(&mut self, $($arg: $arg_ty),*) -> $ret {
#[allow(dead_code)] self.r#impl.$name(&mut self.ptp, $($arg),*)
pub fn $fn_name(&mut self, $($arg: $arg_ty),*) -> anyhow::Result<$ret> {
self.r#impl.$fn_name(&mut self.ptp, $($arg),*)
} }
)*
}; };
} }
pub trait CameraResult: fmt::Display + erased_serde::Serialize {}
impl<T: fmt::Display + serde::Serialize> CameraResult for T {}
serialize_trait_object!(CameraResult);
impl Camera { impl Camera {
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
self.r#impl.supported_camera().name self.r#impl.supported_camera().name
@@ -68,69 +56,12 @@ impl Camera {
format!("{}.{}", self.ptp.bus, self.ptp.address) format!("{}.{}", self.ptp.bus, self.ptp.address)
} }
camera_with_ptp! { camera_to_impl_with_ptp!(info_get() -> anyhow::Result<Box<dyn CameraResult>>);
get_info => DeviceInfo, camera_to_impl_with_ptp!(backup_export -> anyhow::Result<Vec<u8>>);
get_usb_mode => UsbMode, camera_to_impl_with_ptp!(backup_import(buffer: &[u8]) -> anyhow::Result<()>);
get_battery_info => u32, camera_to_impl_with_ptp!(simulation_list -> anyhow::Result<Vec<Box<dyn CameraResult>>>);
} camera_to_impl_with_ptp!(simulation_get(slot: FujiCustomSetting) -> anyhow::Result<Box<dyn CameraResult>>);
camera_to_impl_with_ptp!(simulation_set(slot: FujiCustomSetting, set_options: &SetFilmSimulationOptions, options: &FilmSimulationOptions) -> anyhow::Result<()>);
camera_with_ptp! {
export_backup => Vec<u8>,
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) => (),
}
} }
impl Drop for Camera { impl Drop for Camera {
@@ -213,28 +144,6 @@ impl TryFrom<&rusb::Device<GlobalContext>> for Camera {
} }
} }
macro_rules! camera_impl_custom_settings {
($( $name:ident : $type:ty => $code:expr ),+ $(,)?) => {
$(
paste::paste! {
#[allow(dead_code)]
fn [<get_ $name>](&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 [<set_ $name>](&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<P: rusb::UsbContext> { pub trait CameraImpl<P: rusb::UsbContext> {
fn supported_camera(&self) -> &'static SupportedCamera<P>; fn supported_camera(&self) -> &'static SupportedCamera<P>;
@@ -247,110 +156,80 @@ pub trait CameraImpl<P: rusb::UsbContext> {
1024 * 1024 1024 * 1024
} }
fn get_info(&mut self, ptp: &mut Ptp) -> anyhow::Result<DeviceInfo> { fn info_get(&self, _ptp: &mut Ptp) -> anyhow::Result<Box<dyn CameraResult>> {
let info = ptp.get_info(self.timeout())?; bail!("This device does not support getting detailed info")
Ok(info)
} }
fn get_usb_mode(&mut self, ptp: &mut Ptp) -> anyhow::Result<UsbMode> { fn backup_export(&self, _ptp: &mut Ptp) -> anyhow::Result<Vec<u8>> {
let data = ptp.get_prop_value(DevicePropCode::FujiUsbMode, self.timeout())?; bail!("This device does not support exporting backups")
let result = UsbMode::try_from_ptp(&data)?; }
fn backup_import(&self, _ptp: &mut Ptp, _buffer: &[u8]) -> anyhow::Result<()> {
bail!("This device does not support importing backups")
}
fn simulation_list(&self, _ptp: &mut Ptp) -> anyhow::Result<Vec<Box<dyn CameraResult>>> {
bail!("This device does not support listing simulations")
}
fn simulation_get(
&self,
_ptp: &mut Ptp,
_slot: FujiCustomSetting,
) -> anyhow::Result<Box<dyn CameraResult>> {
bail!("This device does not support getting simulation options")
}
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) Ok(result)
} }
fn get_battery_info(&mut self, ptp: &mut Ptp) -> anyhow::Result<u32> {
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<Vec<u8>> {
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)?;
} }
debug!("Sending SendObjectInfo command for backup"); macro_rules! camera_prop_setter {
let response = ptp.send( ($name:ident: $type:ty => $code:expr) => {
CommandCode::SendObjectInfo, pub fn $name(
&[0x0, 0x0], &self,
Some(&header), ptp: &mut crate::camera::ptp::Ptp,
self.timeout(), value: &$type,
)?; ) -> anyhow::Result<()> {
debug!("Received response with {} bytes", response.len()); use ptp_cursor::PtpSerialize;
debug!("Sending SendObject command for backup");
let response = ptp.send(
CommandCode::SendObject,
&[0x0],
Some(buffer),
self.timeout(),
)?;
debug!("Received response with {} bytes", response.len());
let bytes = value.try_into_ptp()?;
ptp.set_prop_value($code, &bytes, self.timeout())?;
Ok(()) Ok(())
} }
};
}
camera_impl_custom_settings! { macro_rules! camera_set_prop_if_some {
active_custom_setting: FujiCustomSetting => DevicePropCode::FujiCustomSetting, ($self:ident, $ptp:ident, $options:ident,
custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName, $( $field:ident => $setter:ident ),* $(,)? ) => {
image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize, $(
image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality, if let Some(val) = &$options.$field {
dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange, $self.$setter($ptp, val)?;
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,
} }
)*
};
} }
pub(crate) use camera_prop_getter;
pub(crate) use camera_prop_setter;
pub(crate) use camera_set_prop_if_some;

View File

@@ -2,7 +2,6 @@ use ptp_macro::{PtpDeserialize, PtpSerialize};
use super::hex::{CommandCode, ContainerCode, ContainerType, ObjectFormat}; use super::hex::{CommandCode, ContainerCode, ContainerType, ObjectFormat};
#[allow(dead_code)]
#[derive(Debug, PtpSerialize, PtpDeserialize)] #[derive(Debug, PtpSerialize, PtpDeserialize)]
pub struct DeviceInfo { pub struct DeviceInfo {
pub version: u16, pub version: u16,

View File

@@ -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 camera = usb::get_camera(device_id)?;
let mut writer = output.get_writer()?; let mut writer = output.get_writer()?;
let backup = camera.export_backup()?; let backup = camera.backup_export()?;
writer.write_all(&backup)?; writer.write_all(&backup)?;
Ok(()) Ok(())
@@ -36,7 +36,7 @@ fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> {
let mut reader = input.get_reader()?; let mut reader = input.get_reader()?;
let mut backup = Vec::new(); let mut backup = Vec::new();
reader.read_to_end(&mut backup)?; reader.read_to_end(&mut backup)?;
camera.import_backup(&backup)?; camera.backup_import(&backup)?;
Ok(()) Ok(())
} }

View File

@@ -1,12 +1,6 @@
use std::fmt;
use clap::Subcommand; use clap::Subcommand;
use serde::Serialize;
use crate::{ use crate::{camera::devices::CameraInfoListItem, usb};
camera::{Camera, ptp::hex::UsbMode},
usb,
};
#[derive(Subcommand, Debug, Clone, Copy)] #[derive(Subcommand, Debug, Clone, Copy)]
pub enum DeviceCmd { pub enum DeviceCmd {
@@ -19,38 +13,8 @@ pub enum DeviceCmd {
Info, 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<()> { fn handle_list(json: bool) -> anyhow::Result<()> {
let cameras: Vec<CameraItemRepr> = usb::get_connected_cameras()? let cameras: Vec<CameraInfoListItem> = usb::get_connected_cameras()?
.iter() .iter()
.map(std::convert::Into::into) .map(std::convert::Into::into)
.collect(); .collect();
@@ -65,7 +29,6 @@ fn handle_list(json: bool) -> anyhow::Result<()> {
return Ok(()); return Ok(());
} }
println!("Connected Cameras:");
for d in cameras { for d in cameras {
println!("- {d}"); println!("- {d}");
} }
@@ -73,54 +36,10 @@ fn handle_list(json: bool) -> anyhow::Result<()> {
Ok(()) 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<()> { fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?; let mut camera = usb::get_camera(device_id)?;
let info = camera.get_info()?; let repr = camera.info_get()?;
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,
};
if json { if json {
println!("{}", serde_json::to_string_pretty(&repr)?); println!("{}", serde_json::to_string_pretty(&repr)?);

View File

@@ -1,6 +1,5 @@
mod common;
pub mod backup; pub mod backup;
pub mod common;
pub mod device; pub mod device;
pub mod render; pub mod render;
pub mod simulation; pub mod simulation;

View File

@@ -1,14 +1,5 @@
use std::fmt;
use crate::{ use crate::{
camera::ptp::hex::{ camera::ptp::hex::{FujiCustomSetting, FujiCustomSettingName},
FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace,
FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority,
FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality,
FujiImageSize, FujiLensModulationOptimizer, FujiMonochromaticColorTemperature,
FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect,
FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature,
},
usb, usb,
}; };
@@ -17,9 +8,6 @@ use super::common::{
film::FilmSimulationOptions, film::FilmSimulationOptions,
}; };
use clap::{Args, Subcommand}; use clap::{Args, Subcommand};
use log::warn;
use serde::Serialize;
use strum::IntoEnumIterator;
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub enum SimulationCmd { pub enum SimulationCmd {
@@ -72,173 +60,27 @@ pub enum SimulationCmd {
pub struct SetFilmSimulationOptions { pub struct SetFilmSimulationOptions {
/// The name of the slot /// The name of the slot
#[clap(long)] #[clap(long)]
name: Option<FujiCustomSettingName>, pub name: Option<FujiCustomSettingName>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CustomSettingRepr {
pub slot: FujiCustomSetting,
pub name: FujiCustomSettingName,
} }
fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> { fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?; let mut camera = usb::get_camera(device_id)?;
let slots = camera.simulation_list()?;
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 });
}
if json { if json {
println!("{}", serde_json::to_string_pretty(&slots)?); println!("{}", serde_json::to_string_pretty(&slots)?);
} else { } else {
println!("Film Simulations:"); for repr in slots {
for slot in slots { println!("- {repr}");
println!("- {}: {}", slot.slot, slot.name);
} }
} }
Ok(()) 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<()> { fn handle_get(json: bool, device_id: Option<&str>, slot: FujiCustomSetting) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?; let mut camera = usb::get_camera(device_id)?;
camera.set_active_custom_setting(&slot)?; let repr = camera.simulation_get(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()?,
};
if json { if json {
println!("{}", serde_json::to_string_pretty(&repr)?); println!("{}", serde_json::to_string_pretty(&repr)?);
@@ -258,176 +100,7 @@ fn handle_set(
options: &FilmSimulationOptions, options: &FilmSimulationOptions,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?; let mut camera = usb::get_camera(device_id)?;
camera.set_active_custom_setting(&slot)?; camera.simulation_set(slot, set_options, options)?;
// 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)?;
}
Ok(()) Ok(())
} }