diff --git a/src/cli/backup/mod.rs b/src/cli/backup/mod.rs index 3380b86..9228296 100644 --- a/src/cli/backup/mod.rs +++ b/src/cli/backup/mod.rs @@ -1,3 +1,7 @@ +use std::{error::Error, fmt}; + +use crate::usb; + use super::common::file::{Input, Output}; use clap::Subcommand; @@ -17,3 +21,36 @@ pub enum BackupCmd { input_file: Input, }, } + +fn handle_export( + device_id: Option<&str>, + output: Output, +) -> Result<(), Box> { + let camera = usb::get_camera(device_id)?; + + let mut writer = output.get_writer()?; + + todo!(); + + Ok(()) +} + +fn handle_import( + device_id: Option<&str>, + input: Input, +) -> Result<(), Box> { + let camera = usb::get_camera(device_id)?; + + let mut reader = input.get_reader()?; + + todo!(); + + Ok(()) +} + +pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), Box> { + match cmd { + BackupCmd::Export { output_file } => handle_export(device_id, output_file), + BackupCmd::Import { input_file } => handle_import(device_id, input_file), + } +} diff --git a/src/cli/common/file.rs b/src/cli/common/file.rs index 2a0c403..b2f7e3b 100644 --- a/src/cli/common/file.rs +++ b/src/cli/common/file.rs @@ -1,4 +1,4 @@ -use std::{error::Error, path::PathBuf, str::FromStr}; +use std::{error::Error, fs::File, io, path::PathBuf, str::FromStr}; #[derive(Debug, Clone)] pub enum Input { @@ -17,6 +17,15 @@ impl FromStr for Input { } } +impl Input { + pub fn get_reader(&self) -> Result, Box> { + match self { + Input::Stdin => Ok(Box::new(io::stdin())), + Input::Path(path) => Ok(Box::new(File::open(path)?)), + } + } +} + #[derive(Debug, Clone)] pub enum Output { Path(PathBuf), @@ -33,3 +42,12 @@ impl FromStr for Output { } } } + +impl Output { + pub fn get_writer(&self) -> Result, Box> { + match self { + Output::Stdout => Ok(Box::new(io::stdout())), + Output::Path(path) => Ok(Box::new(File::create(path)?)), + } + } +} diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs index 11b9be0..1dfa494 100644 --- a/src/cli/device/mod.rs +++ b/src/cli/device/mod.rs @@ -3,30 +3,30 @@ use std::{error::Error, fmt}; use clap::Subcommand; use serde::Serialize; -use crate::usb; +use crate::{hardware::FujiUsbMode, usb}; #[derive(Subcommand, Debug)] pub enum DeviceCmd { - /// List devices + /// List cameras #[command(alias = "l")] List, - /// Dump device info + /// Get camera info #[command(alias = "i")] Info, } #[derive(Serialize)] -pub struct DeviceItemRepr { +pub struct CameraItemRepr { pub name: String, pub id: String, pub vendor_id: String, pub product_id: String, } -impl From<&usb::Device> for DeviceItemRepr { - fn from(device: &usb::Device) -> Self { - DeviceItemRepr { +impl From<&usb::Camera> for CameraItemRepr { + fn from(device: &usb::Camera) -> Self { + CameraItemRepr { id: device.id(), name: device.name(), vendor_id: format!("0x{:04x}", device.vendor_id()), @@ -35,7 +35,7 @@ impl From<&usb::Device> for DeviceItemRepr { } } -impl fmt::Display for DeviceItemRepr { +impl fmt::Display for CameraItemRepr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!( f, @@ -45,24 +45,24 @@ impl fmt::Display for DeviceItemRepr { } } -pub fn handle_list(json: bool) -> Result<(), Box> { - let devices: Vec = usb::get_connected_devices()? +fn handle_list(json: bool) -> Result<(), Box> { + let cameras: Vec = usb::get_connected_camers()? .iter() .map(|d| d.into()) .collect(); if json { - println!("{}", serde_json::to_string_pretty(&devices)?); + println!("{}", serde_json::to_string_pretty(&cameras)?); return Ok(()); } - if devices.is_empty() { - println!("No supported devices connected."); + if cameras.is_empty() { + println!("No supported cameras connected."); return Ok(()); } - println!("Connected devices:"); - for d in devices { + println!("Connected cameras:"); + for d in cameras { println!("- {}", d); } @@ -70,29 +70,19 @@ pub fn handle_list(json: bool) -> Result<(), Box> { } #[derive(Serialize)] -pub struct DeviceRepr { +pub struct CameraRepr { #[serde(flatten)] - pub device: DeviceItemRepr, + pub device: CameraItemRepr, pub manufacturer: String, pub model: String, pub device_version: String, pub serial_number: String, + pub mode: FujiUsbMode, + pub battery: u32, } -impl DeviceRepr { - pub fn from_info(device: &usb::Device, info: &libptp::DeviceInfo) -> Self { - DeviceRepr { - device: device.into(), - manufacturer: info.Manufacturer.clone(), - model: info.Model.clone(), - device_version: info.DeviceVersion.clone(), - serial_number: info.SerialNumber.clone(), - } - } -} - -impl fmt::Display for DeviceRepr { +impl fmt::Display for CameraRepr { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { writeln!(f, "Name: {}", self.device.name)?; writeln!(f, "ID: {}", self.device.id)?; @@ -103,20 +93,29 @@ impl fmt::Display for DeviceRepr { )?; writeln!(f, "Manufacturer: {}", self.manufacturer)?; writeln!(f, "Model: {}", self.model)?; - writeln!(f, "Device Version: {}", self.device_version)?; - write!(f, "Serial Number: {}", self.serial_number) + writeln!(f, "Version: {}", self.device_version)?; + writeln!(f, "Serial Number: {}", self.serial_number)?; + writeln!(f, "Mode: {}", self.mode)?; + write!(f, "Battery: {}%", self.battery) } } -pub fn handle_info( - json: bool, - device_id: Option<&str>, -) -> Result<(), Box> { - let device = usb::get_device(device_id)?; +fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box> { + let camera = usb::get_camera(device_id)?; - let mut camera = device.camera()?; - let info = device.model.get_device_info(&mut camera)?; - let repr = DeviceRepr::from_info(&device, &info); + let info = camera.get_info()?; + let mode = camera.get_fuji_usb_mode()?; + let battery = camera.get_fuji_battery_info()?; + + let repr = CameraRepr { + device: (&camera).into(), + manufacturer: info.Manufacturer.clone(), + model: info.Model.clone(), + device_version: info.DeviceVersion.clone(), + serial_number: info.SerialNumber.clone(), + mode, + battery, + }; if json { println!("{}", serde_json::to_string_pretty(&repr)?); diff --git a/src/hardware/common.rs b/src/hardware/common.rs deleted file mode 100644 index 465c019..0000000 --- a/src/hardware/common.rs +++ /dev/null @@ -1,3 +0,0 @@ -use std::time::Duration; - -pub const TIMEOUT: Duration = Duration::from_millis(500); diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs index b0e4adb..e1c5367 100644 --- a/src/hardware/mod.rs +++ b/src/hardware/mod.rs @@ -1,36 +1,194 @@ -use std::error::Error; +use std::{error::Error, fmt, time::Duration}; use libptp::{DeviceInfo, StandardCommandCode}; use log::debug; -use rusb::GlobalContext; +use rusb::{DeviceDescriptor, GlobalContext}; +use serde::Serialize; -mod common; mod xt5; -pub trait Camera { - fn vendor_id(&self) -> u16; - fn product_id(&self) -> u16; - fn name(&self) -> &'static str; +pub const TIMEOUT: Duration = Duration::from_millis(500); - fn get_device_info( - &self, - camera: &mut libptp::Camera, - ) -> Result> { - debug!("Using default GetDeviceInfo command for {}", self.name()); +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum DevicePropCode { + FujiUsbMode = 0xd16e, + FujiBatteryInfo1 = 0xD36A, + FujiBatteryInfo2 = 0xD36B, +} - let response = camera.command( - StandardCommandCode::GetDeviceInfo, - &[], - None, - Some(common::TIMEOUT), - )?; +#[derive(Debug, Clone, Copy, Serialize)] +pub enum FujiUsbMode { + RawConversion, // mode == 6 + Unsupported, +} - debug!("Received response with {} bytes", response.len()); - - let device_info = DeviceInfo::decode(&response)?; - - Ok(device_info) +impl From for FujiUsbMode { + fn from(val: u32) -> Self { + match val { + 6 => FujiUsbMode::RawConversion, + _ => FujiUsbMode::Unsupported, + } } } -pub const SUPPORTED_MODELS: &[&dyn Camera] = &[&xt5::FujifilmXT5]; +impl fmt::Display for FujiUsbMode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + FujiUsbMode::RawConversion => "USB RAW CONV./BACKUP RESTORE", + FujiUsbMode::Unsupported => "Unsupported USB Mode", + }; + write!(f, "{}", s) + } +} + +pub trait CameraImpl { + fn name(&self) -> &'static str; + + fn next_session_id(&self) -> u32; + + fn open_session( + &self, + ptp: &mut libptp::Camera, + session_id: u32, + ) -> Result<(), Box> { + debug!("Opening new session with id {}", session_id); + ptp.command( + StandardCommandCode::OpenSession, + &[session_id], + None, + Some(TIMEOUT), + )?; + + Ok(()) + } + + fn close_session( + &self, + ptp: &mut libptp::Camera, + ) -> Result<(), Box> { + debug!("Closing session"); + ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?; + + Ok(()) + } + + fn get_prop_value_raw( + &self, + ptp: &mut libptp::Camera, + prop: DevicePropCode, + ) -> Result, Box> { + let session_id = self.next_session_id(); + self.open_session(ptp, session_id)?; + + debug!("Getting property {:?}", prop); + + let response = ptp.command( + StandardCommandCode::GetDevicePropValue, + &[prop as u32], + None, + Some(TIMEOUT), + ); + + self.close_session(ptp)?; + + let response = response?; + debug!("Received response with {} bytes", response.len()); + + Ok(response) + } + + fn get_prop_value_scalar( + &self, + ptp: &mut libptp::Camera, + prop: DevicePropCode, + ) -> Result> { + let data = self.get_prop_value_raw(ptp, prop)?; + + match data.len() { + 1 => Ok(data[0] as u32), + 2 => Ok(u16::from_le_bytes([data[0], data[1]]) as u32), + 4 => Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])), + n => Err(format!("Cannot parse property {:?} as scalar: {} bytes", prop, n).into()), + } + } + + fn get_fuji_usb_mode( + &self, + ptp: &mut libptp::Camera, + ) -> Result> { + let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?; + Ok(result.into()) + } + + fn get_fuji_battery_info( + &self, + ptp: &mut libptp::Camera, + ) -> Result> { + let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?; + debug!("Raw battery data: {:?}", data); + + if data.len() < 3 { + return Err("Battery info payload too short".into()); + } + + let utf16: Vec = data[1..] + .chunks(2) + .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) + .take_while(|&c| c != 0) + .collect(); + + debug!("Decoded UTF-16 units: {:?}", utf16); + + let utf8_string = String::from_utf16(&utf16)?; + debug!("Decoded UTF-16 string: {}", utf8_string); + + let percentage: u32 = utf8_string + .split(',') + .next() + .ok_or("Failed to parse battery percentage")? + .parse()?; + + Ok(percentage) + } + + fn get_info( + &self, + ptp: &mut libptp::Camera, + ) -> Result> { + debug!("Sending GetDeviceInfo command"); + let response = ptp.command(StandardCommandCode::GetDeviceInfo, &[], None, Some(TIMEOUT))?; + debug!("Received response with {} bytes", response.len()); + + let info = DeviceInfo::decode(&response)?; + Ok(info) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CameraId { + pub vendor: u16, + pub product: u16, +} + +pub struct SupportedCamera { + pub id: CameraId, + pub factory: fn() -> Box, +} + +pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera { + id: xt5::FUJIFILM_XT5, + factory: || Box::new(xt5::FujifilmXT5::new()), +}]; + +impl From<&SupportedCamera> for Box { + fn from(camera: &SupportedCamera) -> Self { + (camera.factory)() + } +} + +impl SupportedCamera { + pub fn matches_descriptor(&self, descriptor: &DeviceDescriptor) -> bool { + descriptor.vendor_id() == self.id.vendor && descriptor.product_id() == self.id.product + } +} diff --git a/src/hardware/xt5.rs b/src/hardware/xt5.rs index cebd065..86e30cf 100644 --- a/src/hardware/xt5.rs +++ b/src/hardware/xt5.rs @@ -1,18 +1,31 @@ -use super::Camera; +use std::sync::atomic::{AtomicU32, Ordering}; + +use super::{CameraId, CameraImpl}; + +pub const FUJIFILM_XT5: CameraId = CameraId { + vendor: 0x04cb, + product: 0x02fc, +}; #[derive(Debug)] -pub struct FujifilmXT5; +pub struct FujifilmXT5 { + session_counter: AtomicU32, +} -impl Camera for FujifilmXT5 { - fn vendor_id(&self) -> u16 { - 0x04cb - } - - fn product_id(&self) -> u16 { - 0x02fc +impl FujifilmXT5 { + pub fn new() -> Self { + Self { + session_counter: AtomicU32::new(1), + } } +} +impl CameraImpl for FujifilmXT5 { fn name(&self) -> &'static str { "FUJIFILM X-T5" } + + fn next_session_id(&self) -> u32 { + self.session_counter.fetch_add(1, Ordering::SeqCst) + } } diff --git a/src/usb/mod.rs b/src/usb/mod.rs index 01c2ab3..00dfa0a 100644 --- a/src/usb/mod.rs +++ b/src/usb/mod.rs @@ -1,23 +1,16 @@ use std::error::Error; +use libptp::DeviceInfo; use rusb::GlobalContext; -use crate::hardware::SUPPORTED_MODELS; +use crate::hardware::{FujiUsbMode, SUPPORTED_CAMERAS}; -#[derive(Clone)] -pub struct Device { - pub model: &'static dyn crate::hardware::Camera, - pub rusb_device: rusb::Device, +pub struct Camera { + camera_impl: Box, + rusb_device: rusb::Device, } -impl Device { - pub fn camera(&self) -> Result, Box> { - let handle = self.rusb_device.open()?; - let device = handle.device(); - let camera = libptp::Camera::new(&device)?; - Ok(camera) - } - +impl Camera { pub fn id(&self) -> String { let bus = self.rusb_device.bus_number(); let address = self.rusb_device.address(); @@ -25,7 +18,7 @@ impl Device { } pub fn name(&self) -> String { - self.model.name().to_string() + self.camera_impl.name().to_string() } pub fn vendor_id(&self) -> u16 { @@ -37,10 +30,32 @@ impl Device { let descriptor = self.rusb_device.device_descriptor().unwrap(); descriptor.product_id() } + + pub fn ptp(&self) -> Result, Box> { + let handle = self.rusb_device.open()?; + let device = handle.device(); + let ptp = libptp::Camera::new(&device)?; + Ok(ptp) + } + + pub fn get_info(&self) -> Result> { + let mut ptp = self.ptp()?; + self.camera_impl.get_info(&mut ptp) + } + + pub fn get_fuji_usb_mode(&self) -> Result> { + let mut ptp = self.ptp()?; + self.camera_impl.get_fuji_usb_mode(&mut ptp) + } + + pub fn get_fuji_battery_info(&self) -> Result> { + let mut ptp = self.ptp()?; + self.camera_impl.get_fuji_battery_info(&mut ptp) + } } -pub fn get_connected_devices() -> Result, Box> { - let mut connected_devices = Vec::new(); +pub fn get_connected_camers() -> Result, Box> { + let mut connected_cameras = Vec::new(); for device in rusb::devices()?.iter() { let descriptor = match device.device_descriptor() { @@ -48,25 +63,23 @@ pub fn get_connected_devices() -> Result, Box continue, }; - for model in SUPPORTED_MODELS.iter() { - if descriptor.vendor_id() == model.vendor_id() - && descriptor.product_id() == model.product_id() - { - let connected_device = Device { - model: *model, + for camera in SUPPORTED_CAMERAS.iter() { + if camera.matches_descriptor(&descriptor) { + let camera = Camera { + camera_impl: camera.into(), rusb_device: device, }; - connected_devices.push(connected_device); + connected_cameras.push(camera); break; } } } - Ok(connected_devices) + Ok(connected_cameras) } -pub fn get_connected_device_by_id(id: &str) -> Result> { +pub fn get_connected_camera_by_id(id: &str) -> Result> { let parts: Vec<&str> = id.split('.').collect(); if parts.len() != 2 { return Err(format!("Invalid device id format: {}", id).into()); @@ -79,12 +92,10 @@ pub fn get_connected_device_by_id(id: &str) -> Result Result) -> Result> { +pub fn get_camera(device_id: Option<&str>) -> Result> { match device_id { - Some(id) => get_connected_device_by_id(id), - None => get_connected_devices()? + Some(id) => get_connected_camera_by_id(id), + None => get_connected_camers()? .into_iter() .next() .ok_or_else(|| "No supported devices connected.".into()),