feat: custom option getter

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-10-18 02:40:09 +01:00
parent 7c43e0f7ab
commit a1668bb277
21 changed files with 2445 additions and 606 deletions

View File

@@ -2,17 +2,23 @@ pub mod devices;
pub mod error;
pub mod ptp;
use std::{io::Cursor, time::Duration};
use std::time::Duration;
use anyhow::{anyhow, bail};
use byteorder::{LittleEndian, WriteBytesExt};
use devices::SupportedCamera;
use log::{debug, error};
use ptp::{
Ptp,
enums::{CommandCode, PropCode, UsbMode},
hex::{
CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect,
FujiColorChromeFXBlue, FujiCustomSetting, FujiDynamicRange, FujiFilmSimulation,
FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize,
FujiShadowTone, FujiSharpness, FujiStillDynamicRangePriority, FujiWhiteBalance,
FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode,
},
structs::DeviceInfo,
};
use ptp_cursor::{PtpDeserialize, PtpSerialize};
use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE};
const SESSION: u32 = 1;
@@ -108,17 +114,6 @@ impl Camera {
format!("{}.{}", self.ptp.bus, self.ptp.address)
}
fn prop_value_as_scalar(data: &[u8]) -> anyhow::Result<u32> {
let data = match data.len() {
1 => anyhow::Ok(u32::from(data[0])),
2 => anyhow::Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))),
4 => anyhow::Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])),
n => bail!("Cannot parse {n} bytes as scalar"),
}?;
Ok(data)
}
pub fn get_info(&mut self) -> anyhow::Result<DeviceInfo> {
let info = self.r#impl.get_info(&mut self.ptp)?;
Ok(info)
@@ -127,30 +122,22 @@ impl Camera {
pub fn get_usb_mode(&mut self) -> anyhow::Result<UsbMode> {
let data = self
.r#impl
.get_prop_value(&mut self.ptp, PropCode::FujiUsbMode);
let result = Self::prop_value_as_scalar(&data?)?.into();
.get_prop_value(&mut self.ptp, DevicePropCode::FujiUsbMode)?;
let result = UsbMode::try_from_ptp(&data)?;
Ok(result)
}
pub fn get_battery_info(&mut self) -> anyhow::Result<u32> {
let data = self
.r#impl
.get_prop_value(&mut self.ptp, PropCode::FujiBatteryInfo2);
.get_prop_value(&mut self.ptp, DevicePropCode::FujiBatteryInfo2)?;
let data = data?;
debug!("Raw battery data: {data:?}");
let utf16: Vec<u16> = data[1..]
.chunks(2)
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
.take_while(|&c| c != 0)
.collect();
let raw_string = String::try_from_ptp(&data)?;
debug!("Decoded raw string: {raw_string}");
let utf8_string = String::from_utf16(&utf16)?;
debug!("Decoded UTF-16 string: {utf8_string}");
let percentage: u32 = utf8_string
let percentage: u32 = raw_string
.split(',')
.next()
.ok_or_else(|| anyhow!("Failed to parse battery percentage"))?
@@ -166,6 +153,86 @@ impl Camera {
pub fn import_backup(&mut self, backup: &[u8]) -> anyhow::Result<()> {
self.r#impl.import_backup(&mut self.ptp, backup)
}
pub fn set_active_custom_setting(&mut self, slot: FujiCustomSetting) -> anyhow::Result<()> {
self.r#impl.set_custom_setting_slot(&mut self.ptp, slot)
}
pub fn get_custom_setting_name(&mut self) -> anyhow::Result<String> {
self.r#impl.get_custom_setting_name(&mut self.ptp)
}
pub fn get_image_size(&mut self) -> anyhow::Result<FujiImageSize> {
self.r#impl.get_image_size(&mut self.ptp)
}
pub fn get_image_quality(&mut self) -> anyhow::Result<FujiImageQuality> {
self.r#impl.get_image_quality(&mut self.ptp)
}
pub fn get_dynamic_range(&mut self) -> anyhow::Result<FujiDynamicRange> {
self.r#impl.get_dynamic_range(&mut self.ptp)
}
pub fn get_dynamic_range_priority(&mut self) -> anyhow::Result<FujiStillDynamicRangePriority> {
self.r#impl.get_dynamic_range_priority(&mut self.ptp)
}
pub fn get_film_simulation(&mut self) -> anyhow::Result<FujiFilmSimulation> {
self.r#impl.get_film_simulation(&mut self.ptp)
}
pub fn get_grain_effect(&mut self) -> anyhow::Result<FujiGrainEffect> {
self.r#impl.get_grain_effect(&mut self.ptp)
}
pub fn get_white_balance(&mut self) -> anyhow::Result<FujiWhiteBalance> {
self.r#impl.get_white_balance(&mut self.ptp)
}
pub fn get_high_iso_nr(&mut self) -> anyhow::Result<FujiHighISONR> {
self.r#impl.get_high_iso_nr(&mut self.ptp)
}
pub fn get_highlight_tone(&mut self) -> anyhow::Result<FujiHighlightTone> {
self.r#impl.get_highlight_tone(&mut self.ptp)
}
pub fn get_shadow_tone(&mut self) -> anyhow::Result<FujiShadowTone> {
self.r#impl.get_shadow_tone(&mut self.ptp)
}
pub fn get_color(&mut self) -> anyhow::Result<FujiColor> {
self.r#impl.get_color(&mut self.ptp)
}
pub fn get_sharpness(&mut self) -> anyhow::Result<FujiSharpness> {
self.r#impl.get_sharpness(&mut self.ptp)
}
pub fn get_clarity(&mut self) -> anyhow::Result<FujiClarity> {
self.r#impl.get_clarity(&mut self.ptp)
}
pub fn get_wb_shift_red(&mut self) -> anyhow::Result<FujiWhiteBalanceShift> {
self.r#impl.get_wb_shift_red(&mut self.ptp)
}
pub fn get_wb_shift_blue(&mut self) -> anyhow::Result<FujiWhiteBalanceShift> {
self.r#impl.get_wb_shift_blue(&mut self.ptp)
}
pub fn get_wb_temperature(&mut self) -> anyhow::Result<FujiWhiteBalanceTemperature> {
self.r#impl.get_wb_temperature(&mut self.ptp)
}
pub fn get_color_chrome_effect(&mut self) -> anyhow::Result<FujiColorChromeEffect> {
self.r#impl.get_color_chrome_effect(&mut self.ptp)
}
pub fn get_color_chrome_fx_blue(&mut self) -> anyhow::Result<FujiColorChromeFXBlue> {
self.r#impl.get_color_chrome_fx_blue(&mut self.ptp)
}
}
impl Drop for Camera {
@@ -193,9 +260,8 @@ pub trait CameraImpl<P: rusb::UsbContext> {
debug!("Sending OpenSession command");
_ = ptp.send(
CommandCode::OpenSession,
Some(&[session_id]),
&[session_id],
None,
true,
self.timeout(),
)?;
Ok(())
@@ -203,25 +269,41 @@ pub trait CameraImpl<P: rusb::UsbContext> {
fn close_session(&self, ptp: &mut Ptp, _: u32) -> anyhow::Result<()> {
debug!("Sending CloseSession command");
_ = ptp.send(CommandCode::CloseSession, None, None, true, self.timeout())?;
_ = ptp.send(CommandCode::CloseSession, &[], None, self.timeout())?;
Ok(())
}
fn get_info(&self, ptp: &mut Ptp) -> anyhow::Result<DeviceInfo> {
debug!("Sending GetDeviceInfo command");
let response = ptp.send(CommandCode::GetDeviceInfo, None, None, true, self.timeout())?;
let response = ptp.send(CommandCode::GetDeviceInfo, &[], None, self.timeout())?;
debug!("Received response with {} bytes", response.len());
let info = DeviceInfo::try_from(response.as_slice())?;
let info = DeviceInfo::try_from_ptp(&response)?;
Ok(info)
}
fn get_prop_value(&self, ptp: &mut Ptp, prop: PropCode) -> anyhow::Result<Vec<u8>> {
fn get_prop_value(&self, ptp: &mut Ptp, prop: DevicePropCode) -> anyhow::Result<Vec<u8>> {
debug!("Sending GetDevicePropValue command for property {prop:?}");
let response = ptp.send(
CommandCode::GetDevicePropValue,
Some(&[prop as u32]),
&[prop.into()],
None,
true,
self.timeout(),
)?;
debug!("Received response with {} bytes", response.len());
Ok(response)
}
fn set_prop_value(
&self,
ptp: &mut Ptp,
prop: DevicePropCode,
value: &[u8],
) -> anyhow::Result<Vec<u8>> {
debug!("Sending GetDevicePropValue command for property {prop:?}");
let response = ptp.send(
CommandCode::SetDevicePropValue,
&[prop.into()],
Some(value),
self.timeout(),
)?;
debug!("Received response with {} bytes", response.len());
@@ -232,23 +314,11 @@ pub trait CameraImpl<P: rusb::UsbContext> {
const HANDLE: u32 = 0x0;
debug!("Sending GetObjectInfo command for backup");
let response = ptp.send(
CommandCode::GetObjectInfo,
Some(&[HANDLE]),
None,
true,
self.timeout(),
)?;
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,
Some(&[HANDLE]),
None,
true,
self.timeout(),
)?;
let response = ptp.send(CommandCode::GetObject, &[HANDLE], None, self.timeout())?;
debug!("Received response with {} bytes", response.len());
Ok(response)
@@ -257,21 +327,20 @@ pub trait CameraImpl<P: rusb::UsbContext> {
fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> {
debug!("Preparing ObjectInfo header for backup");
let mut header1 = vec![0u8; 1012];
let mut cursor = Cursor::new(&mut header1[..]);
cursor.write_u32::<LittleEndian>(0x0)?;
cursor.write_u16::<LittleEndian>(0x5000)?;
cursor.write_u16::<LittleEndian>(0x0)?;
cursor.write_u32::<LittleEndian>(u32::try_from(buffer.len())?)?;
let header2 = vec![0u8; 64];
let mut header = Vec::with_capacity(1076);
0x0u32.try_write_ptp(&mut header)?;
0x5000u16.try_write_ptp(&mut header)?;
0x0u16.try_write_ptp(&mut header)?;
u32::try_from(buffer.len())?.try_write_ptp(&mut header)?;
for _ in 0..1064 {
0x0u8.try_write_ptp(&mut header)?;
}
debug!("Sending SendObjectInfo command for backup");
let response = ptp.send_many(
let response = ptp.send(
CommandCode::SendObjectInfo,
Some(&[0x0, 0x0]),
Some(&[&header1, &header2]),
true,
&[0x0, 0x0],
Some(&header),
self.timeout(),
)?;
debug!("Received response with {} bytes", response.len());
@@ -279,13 +348,154 @@ pub trait CameraImpl<P: rusb::UsbContext> {
debug!("Sending SendObject command for backup");
let response = ptp.send(
CommandCode::SendObject,
Some(&[0x0]),
&[0x0],
Some(buffer),
true,
self.timeout(),
)?;
debug!("Received response with {} bytes", response.len());
Ok(())
}
fn set_custom_setting_slot(
&self,
ptp: &mut Ptp,
slot: FujiCustomSetting,
) -> anyhow::Result<()> {
self.set_prop_value(
ptp,
DevicePropCode::FujiStillCustomSetting,
&slot.try_into_ptp()?,
)?;
Ok(())
}
fn get_custom_setting_name(&self, ptp: &mut Ptp) -> anyhow::Result<String> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingName)?;
let name = String::try_from_ptp(&bytes)?;
Ok(name)
}
fn get_image_size(&self, ptp: &mut Ptp) -> anyhow::Result<FujiImageSize> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingImageSize)?;
let result = FujiImageSize::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_image_quality(&self, ptp: &mut Ptp) -> anyhow::Result<FujiImageQuality> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingImageQuality)?;
let result = FujiImageQuality::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_dynamic_range(&self, ptp: &mut Ptp) -> anyhow::Result<FujiDynamicRange> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingDynamicRange)?;
let result = FujiDynamicRange::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_dynamic_range_priority(
&self,
ptp: &mut Ptp,
) -> anyhow::Result<FujiStillDynamicRangePriority> {
let bytes = self.get_prop_value(
ptp,
DevicePropCode::FujiStillCustomSettingDynamicRangePriority,
)?;
let result = FujiStillDynamicRangePriority::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_film_simulation(&self, ptp: &mut Ptp) -> anyhow::Result<FujiFilmSimulation> {
let bytes =
self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingFilmSimulation)?;
let result = FujiFilmSimulation::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_grain_effect(&self, ptp: &mut Ptp) -> anyhow::Result<FujiGrainEffect> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingGrainEffect)?;
let result = FujiGrainEffect::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_white_balance(&self, ptp: &mut Ptp) -> anyhow::Result<FujiWhiteBalance> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalance)?;
let result = FujiWhiteBalance::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_high_iso_nr(&self, ptp: &mut Ptp) -> anyhow::Result<FujiHighISONR> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingHighISONR)?;
let result = FujiHighISONR::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_highlight_tone(&self, ptp: &mut Ptp) -> anyhow::Result<FujiHighlightTone> {
let bytes =
self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingHighlightTone)?;
let result = FujiHighlightTone::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_shadow_tone(&self, ptp: &mut Ptp) -> anyhow::Result<FujiShadowTone> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingShadowTone)?;
let result = FujiShadowTone::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_color(&self, ptp: &mut Ptp) -> anyhow::Result<FujiColor> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColor)?;
let result = FujiColor::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_sharpness(&self, ptp: &mut Ptp) -> anyhow::Result<FujiSharpness> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingSharpness)?;
let result = FujiSharpness::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_clarity(&self, ptp: &mut Ptp) -> anyhow::Result<FujiClarity> {
let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingClarity)?;
let result = FujiClarity::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_wb_shift_red(&self, ptp: &mut Ptp) -> anyhow::Result<FujiWhiteBalanceShift> {
let bytes =
self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalanceRed)?;
let result = FujiWhiteBalanceShift::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_wb_shift_blue(&self, ptp: &mut Ptp) -> anyhow::Result<FujiWhiteBalanceShift> {
let bytes =
self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalanceBlue)?;
let result = FujiWhiteBalanceShift::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_wb_temperature(&self, ptp: &mut Ptp) -> anyhow::Result<FujiWhiteBalanceTemperature> {
let bytes = self.get_prop_value(
ptp,
DevicePropCode::FujiStillCustomSettingWhiteBalanceTemperature,
)?;
let result = FujiWhiteBalanceTemperature::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_color_chrome_effect(&self, ptp: &mut Ptp) -> anyhow::Result<FujiColorChromeEffect> {
let bytes =
self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColorChromeEffect)?;
let result = FujiColorChromeEffect::try_from_ptp(&bytes)?;
Ok(result)
}
fn get_color_chrome_fx_blue(&self, ptp: &mut Ptp) -> anyhow::Result<FujiColorChromeFXBlue> {
let bytes =
self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColorChromeFXBlue)?;
let result = FujiColorChromeFXBlue::try_from_ptp(&bytes)?;
Ok(result)
}
}