Compare commits

...

2 Commits

Author SHA1 Message Date
0ef41cf4f8 chore: refactor session handling
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-13 23:25:09 +01:00
5e0a987386 chore: refactor ptp handling
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-13 20:39:22 +01:00
11 changed files with 367 additions and 268 deletions

1
Cargo.lock generated
View File

@@ -236,6 +236,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
name = "fujicli" name = "fujicli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"clap", "clap",
"libptp", "libptp",
"log", "log",

View File

@@ -14,6 +14,7 @@ lto = true
codegen-units = 1 codegen-units = 1
[dependencies] [dependencies]
anyhow = "1.0.100"
clap = { version = "4.5.48", features = ["derive", "wrap_help"] } clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
libptp = "0.6.5" libptp = "0.6.5"
log = "0.4.28" log = "0.4.28"

View File

@@ -1,5 +1,3 @@
use std::{error::Error, fmt};
use crate::usb; use crate::usb;
use super::common::file::{Input, Output}; use super::common::file::{Input, Output};
@@ -22,35 +20,32 @@ pub enum BackupCmd {
}, },
} }
fn handle_export( fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
device_id: Option<&str>,
output: Output,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let camera = usb::get_camera(device_id)?; let camera = usb::get_camera(device_id)?;
let mut ptp = camera.ptp_session()?;
let mut writer = output.get_writer()?; let mut writer = output.get_writer()?;
let backup = camera.export_backup(&mut ptp)?;
todo!(); writer.write_all(&backup)?;
Ok(()) Ok(())
} }
fn handle_import( fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> {
device_id: Option<&str>,
input: Input,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let camera = usb::get_camera(device_id)?; let camera = usb::get_camera(device_id)?;
let mut ptp = camera.ptp_session()?;
let mut reader = input.get_reader()?; let mut reader = input.get_reader()?;
let mut buffer = Vec::new();
todo!(); reader.read_to_end(&mut buffer)?;
camera.import_backup(&mut ptp, &buffer)?;
Ok(()) Ok(())
} }
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), Box<dyn Error + Send + Sync>> { pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> {
match cmd { match cmd {
BackupCmd::Export { output_file } => handle_export(device_id, output_file), BackupCmd::Export { output_file } => handle_export(device_id, &output_file),
BackupCmd::Import { input_file } => handle_import(device_id, input_file), BackupCmd::Import { input_file } => handle_import(device_id, &input_file),
} }
} }

View File

@@ -1,4 +1,4 @@
use std::{error::Error, fs::File, io, path::PathBuf, str::FromStr}; use std::{fs::File, io, path::PathBuf, str::FromStr};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Input { pub enum Input {
@@ -7,21 +7,21 @@ pub enum Input {
} }
impl FromStr for Input { impl FromStr for Input {
type Err = Box<dyn Error + Send + Sync>; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" { if s == "-" {
Ok(Input::Stdin) Ok(Self::Stdin)
} else { } else {
Ok(Input::Path(PathBuf::from(s))) Ok(Self::Path(PathBuf::from(s)))
} }
} }
} }
impl Input { impl Input {
pub fn get_reader(&self) -> Result<Box<dyn io::Read>, Box<dyn Error + Send + Sync>> { pub fn get_reader(&self) -> Result<Box<dyn io::Read>, anyhow::Error> {
match self { match self {
Input::Stdin => Ok(Box::new(io::stdin())), Self::Stdin => Ok(Box::new(io::stdin())),
Input::Path(path) => Ok(Box::new(File::open(path)?)), Self::Path(path) => Ok(Box::new(File::open(path)?)),
} }
} }
} }
@@ -33,21 +33,21 @@ pub enum Output {
} }
impl FromStr for Output { impl FromStr for Output {
type Err = Box<dyn Error + Send + Sync>; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" { if s == "-" {
Ok(Output::Stdout) Ok(Self::Stdout)
} else { } else {
Ok(Output::Path(PathBuf::from(s))) Ok(Self::Path(PathBuf::from(s)))
} }
} }
} }
impl Output { impl Output {
pub fn get_writer(&self) -> Result<Box<dyn io::Write>, Box<dyn Error + Send + Sync>> { pub fn get_writer(&self) -> Result<Box<dyn io::Write>, anyhow::Error> {
match self { match self {
Output::Stdout => Ok(Box::new(io::stdout())), Self::Stdout => Ok(Box::new(io::stdout())),
Output::Path(path) => Ok(Box::new(File::create(path)?)), Self::Path(path) => Ok(Box::new(File::create(path)?)),
} }
} }
} }

View File

@@ -1,5 +1,4 @@
use std::error::Error; use anyhow::bail;
use clap::Args; use clap::Args;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -9,18 +8,18 @@ pub enum SimulationSelector {
} }
impl std::str::FromStr for SimulationSelector { impl std::str::FromStr for SimulationSelector {
type Err = Box<dyn Error + Send + Sync>; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(slot) = s.parse::<u8>() { if let Ok(slot) = s.parse::<u8>() {
return Ok(SimulationSelector::Slot(slot)); return Ok(Self::Slot(slot));
} }
if s.is_empty() { if s.is_empty() {
Err("Simulation name cannot be empty".into()) bail!("Simulation name cannot be empty")
} else {
Ok(SimulationSelector::Name(s.to_string()))
} }
Ok(Self::Name(s.to_string()))
} }
} }

View File

@@ -1,11 +1,14 @@
use std::{error::Error, fmt}; use std::fmt;
use clap::Subcommand; use clap::Subcommand;
use serde::Serialize; use serde::Serialize;
use crate::{hardware::FujiUsbMode, usb}; use crate::{
hardware::{CameraImpl, UsbMode},
usb,
};
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug, Clone, Copy)]
pub enum DeviceCmd { pub enum DeviceCmd {
/// List cameras /// List cameras
#[command(alias = "l")] #[command(alias = "l")]
@@ -24,13 +27,13 @@ pub struct CameraItemRepr {
pub product_id: String, pub product_id: String,
} }
impl From<&usb::Camera> for CameraItemRepr { impl From<&Box<dyn CameraImpl>> for CameraItemRepr {
fn from(device: &usb::Camera) -> Self { fn from(camera: &Box<dyn CameraImpl>) -> Self {
CameraItemRepr { Self {
id: device.id(), id: camera.usb_id(),
name: device.name(), name: camera.id().name.to_string(),
vendor_id: format!("0x{:04x}", device.vendor_id()), vendor_id: format!("0x{:04x}", camera.id().vendor),
product_id: format!("0x{:04x}", device.product_id()), product_id: format!("0x{:04x}", camera.id().product),
} }
} }
} }
@@ -45,10 +48,10 @@ impl fmt::Display for CameraItemRepr {
} }
} }
fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> { fn handle_list(json: bool) -> Result<(), anyhow::Error> {
let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()? let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()?
.iter() .iter()
.map(|d| d.into()) .map(std::convert::Into::into)
.collect(); .collect();
if json { if json {
@@ -63,7 +66,7 @@ fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
println!("Connected cameras:"); println!("Connected cameras:");
for d in cameras { for d in cameras {
println!("- {}", d); println!("- {d}");
} }
Ok(()) Ok(())
@@ -78,7 +81,7 @@ pub struct CameraRepr {
pub model: String, pub model: String,
pub device_version: String, pub device_version: String,
pub serial_number: String, pub serial_number: String,
pub mode: FujiUsbMode, pub mode: UsbMode,
pub battery: u32, pub battery: u32,
} }
@@ -100,19 +103,22 @@ impl fmt::Display for CameraRepr {
} }
} }
fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box<dyn Error + Send + Sync>> { fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> {
let camera = usb::get_camera(device_id)?; let camera = usb::get_camera(device_id)?;
let mut ptp = camera.ptp();
let info = camera.get_info()?; let info = camera.get_info(&mut ptp)?;
let mode = camera.get_fuji_usb_mode()?;
let battery = camera.get_fuji_battery_info()?; let mut ptp = camera.open_session(ptp)?;
let mode = camera.get_usb_mode(&mut ptp)?;
let battery = camera.get_battery_info(&mut ptp)?;
let repr = CameraRepr { let repr = CameraRepr {
device: (&camera).into(), device: (&camera).into(),
manufacturer: info.Manufacturer.clone(), manufacturer: info.Manufacturer.clone(),
model: info.Model.clone(), model: info.Model.clone(),
device_version: info.DeviceVersion.clone(), device_version: info.DeviceVersion.clone(),
serial_number: info.SerialNumber.clone(), serial_number: info.SerialNumber,
mode, mode,
battery, battery,
}; };
@@ -122,15 +128,11 @@ fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box<dyn Error
return Ok(()); return Ok(());
} }
println!("{}", repr); println!("{repr}");
Ok(()) Ok(())
} }
pub fn handle( pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> {
cmd: DeviceCmd,
json: bool,
device_id: Option<&str>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
match cmd { match cmd {
DeviceCmd::List => handle_list(json), DeviceCmd::List => handle_list(json),
DeviceCmd::Info => handle_info(json, device_id), DeviceCmd::Info => handle_info(json, device_id),

View File

@@ -1,11 +1,39 @@
use std::{error::Error, fmt, time::Duration}; use std::{
fmt,
ops::{Deref, DerefMut},
time::Duration,
};
use anyhow::bail;
use libptp::{DeviceInfo, StandardCommandCode}; use libptp::{DeviceInfo, StandardCommandCode};
use log::debug; use log::{debug, error};
use rusb::{DeviceDescriptor, GlobalContext}; use rusb::{DeviceDescriptor, GlobalContext};
use serde::Serialize; use serde::Serialize;
mod xt5; #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CameraId {
pub name: &'static str,
pub vendor: u16,
pub product: u16,
}
type CameraFactory = fn(rusb::Device<GlobalContext>) -> Result<Box<dyn CameraImpl>, anyhow::Error>;
pub struct SupportedCamera {
pub id: CameraId,
pub factory: CameraFactory,
}
pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera {
id: FUJIFILM_XT5,
factory: |d| FujifilmXT5::new_boxed(&d),
}];
impl SupportedCamera {
pub fn matches_descriptor(&self, descriptor: &DeviceDescriptor) -> bool {
descriptor.vendor_id() == self.id.vendor && descriptor.product_id() == self.id.product
}
}
pub const TIMEOUT: Duration = Duration::from_millis(500); pub const TIMEOUT: Duration = Duration::from_millis(500);
@@ -13,86 +41,159 @@ pub const TIMEOUT: Duration = Duration::from_millis(500);
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub enum DevicePropCode { pub enum DevicePropCode {
FujiUsbMode = 0xd16e, FujiUsbMode = 0xd16e,
FujiBatteryInfo1 = 0xD36A,
FujiBatteryInfo2 = 0xD36B, FujiBatteryInfo2 = 0xD36B,
} }
#[derive(Debug, Clone, Copy, Serialize)] #[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
pub enum FujiUsbMode { pub enum UsbMode {
RawConversion, // mode == 6 RawConversion, // mode == 6
Unsupported, Unsupported,
} }
impl From<u32> for FujiUsbMode { impl From<u32> for UsbMode {
fn from(val: u32) -> Self { fn from(val: u32) -> Self {
match val { match val {
6 => FujiUsbMode::RawConversion, 6 => Self::RawConversion,
_ => FujiUsbMode::Unsupported, _ => Self::Unsupported,
} }
} }
} }
impl fmt::Display for FujiUsbMode { impl fmt::Display for UsbMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self { let s = match self {
FujiUsbMode::RawConversion => "USB RAW CONV./BACKUP RESTORE", Self::RawConversion => "USB RAW CONV./BACKUP RESTORE",
FujiUsbMode::Unsupported => "Unsupported USB Mode", Self::Unsupported => "Unsupported USB Mode",
}; };
write!(f, "{}", s) write!(f, "{s}")
}
}
pub struct Ptp {
ptp: libptp::Camera<GlobalContext>,
}
impl Deref for Ptp {
type Target = libptp::Camera<GlobalContext>;
fn deref(&self) -> &Self::Target {
&self.ptp
}
}
impl DerefMut for Ptp {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.ptp
}
}
impl From<libptp::Camera<GlobalContext>> for Ptp {
fn from(ptp: libptp::Camera<GlobalContext>) -> Self {
Self { ptp }
}
}
type SessionCloseFn =
Box<dyn FnOnce(u32, &mut libptp::Camera<GlobalContext>) -> Result<(), anyhow::Error>>;
pub struct PtpSession {
ptp: libptp::Camera<GlobalContext>,
session_id: u32,
close_fn: Option<SessionCloseFn>,
}
impl Deref for PtpSession {
type Target = libptp::Camera<GlobalContext>;
fn deref(&self) -> &Self::Target {
&self.ptp
}
}
impl DerefMut for PtpSession {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.ptp
}
}
impl Drop for PtpSession {
fn drop(&mut self) {
if let Some(close_fn) = self.close_fn.take() {
// take ownership
if let Err(e) = close_fn(self.session_id, &mut self.ptp) {
error!("Error closing session {}: {}", self.session_id, e);
}
}
} }
} }
pub trait CameraImpl { pub trait CameraImpl {
fn name(&self) -> &'static str; fn id(&self) -> &'static CameraId;
fn device(&self) -> &rusb::Device<rusb::GlobalContext>;
fn usb_id(&self) -> String {
let bus = self.device().bus_number();
let address = self.device().address();
format!("{bus}.{address}")
}
fn ptp(&self) -> Ptp;
fn ptp_session(&self) -> Result<PtpSession, anyhow::Error>;
fn get_info(&self, ptp: &mut Ptp) -> Result<DeviceInfo, anyhow::Error> {
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)
}
fn next_session_id(&self) -> u32; fn next_session_id(&self) -> u32;
fn open_session( fn open_session(&self, ptp: Ptp) -> Result<PtpSession, anyhow::Error> {
&self, let session_id = self.next_session_id();
ptp: &mut libptp::Camera<GlobalContext>, let mut ptp = ptp.ptp;
session_id: u32,
) -> Result<(), Box<dyn Error + Send + Sync>> { debug!("Opening session with id {session_id}");
debug!("Opening new session with id {}", session_id);
ptp.command( ptp.command(
StandardCommandCode::OpenSession, StandardCommandCode::OpenSession,
&[session_id], &[session_id],
None, None,
Some(TIMEOUT), Some(TIMEOUT),
)?; )?;
debug!("Session {session_id} open");
Ok(()) let close_fn: Option<SessionCloseFn> = Some(Box::new(move |_, ptp| {
} debug!("Closing session with id {session_id}");
fn close_session(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
debug!("Closing session");
ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?; ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?;
debug!("Session {session_id} closed");
Ok(()) Ok(())
}));
Ok(PtpSession {
ptp,
session_id,
close_fn,
})
} }
fn get_prop_value_raw( fn get_prop_value_raw(
&self, &self,
ptp: &mut libptp::Camera<GlobalContext>, ptp: &mut PtpSession,
prop: DevicePropCode, prop: DevicePropCode,
) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> { ) -> Result<Vec<u8>, anyhow::Error> {
let session_id = self.next_session_id(); debug!("Getting property {prop:?}");
self.open_session(ptp, session_id)?;
debug!("Getting property {:?}", prop);
let response = ptp.command( let response = ptp.command(
StandardCommandCode::GetDevicePropValue, StandardCommandCode::GetDevicePropValue,
&[prop as u32], &[prop as u32],
None, None,
Some(TIMEOUT), Some(TIMEOUT),
); )?;
self.close_session(ptp)?;
let response = response?;
debug!("Received response with {} bytes", response.len()); debug!("Received response with {} bytes", response.len());
Ok(response) Ok(response)
@@ -100,36 +201,30 @@ pub trait CameraImpl {
fn get_prop_value_scalar( fn get_prop_value_scalar(
&self, &self,
ptp: &mut libptp::Camera<GlobalContext>, ptp: &mut PtpSession,
prop: DevicePropCode, prop: DevicePropCode,
) -> Result<u32, Box<dyn Error + Send + Sync>> { ) -> Result<u32, anyhow::Error> {
let data = self.get_prop_value_raw(ptp, prop)?; let data = self.get_prop_value_raw(ptp, prop)?;
match data.len() { match data.len() {
1 => Ok(data[0] as u32), 1 => Ok(u32::from(data[0])),
2 => Ok(u16::from_le_bytes([data[0], data[1]]) as u32), 2 => Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))),
4 => Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])), 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()), n => bail!("Cannot parse property {prop:?} as scalar: {n} bytes"),
} }
} }
fn get_fuji_usb_mode( fn get_usb_mode(&self, ptp: &mut PtpSession) -> Result<UsbMode, anyhow::Error> {
&self,
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<FujiUsbMode, Box<dyn Error + Send + Sync>> {
let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?; let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?;
Ok(result.into()) Ok(result.into())
} }
fn get_fuji_battery_info( fn get_battery_info(&self, ptp: &mut PtpSession) -> Result<u32, anyhow::Error> {
&self,
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<u32, Box<dyn Error + Send + Sync>> {
let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?; let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?;
debug!("Raw battery data: {:?}", data); debug!("Raw battery data: {data:?}");
if data.len() < 3 { if data.len() < 3 {
return Err("Battery info payload too short".into()); bail!("Battery info payload too short");
} }
let utf16: Vec<u16> = data[1..] let utf16: Vec<u16> = data[1..]
@@ -138,57 +233,153 @@ pub trait CameraImpl {
.take_while(|&c| c != 0) .take_while(|&c| c != 0)
.collect(); .collect();
debug!("Decoded UTF-16 units: {:?}", utf16); debug!("Decoded UTF-16 units: {utf16:?}");
let utf8_string = String::from_utf16(&utf16)?; let utf8_string = String::from_utf16(&utf16)?;
debug!("Decoded UTF-16 string: {}", utf8_string); debug!("Decoded UTF-16 string: {utf8_string}");
let percentage: u32 = utf8_string let percentage: u32 = utf8_string
.split(',') .split(',')
.next() .next()
.ok_or("Failed to parse battery percentage")? .ok_or_else(|| anyhow::anyhow!("Failed to parse battery percentage"))?
.parse()?; .parse()?;
Ok(percentage) Ok(percentage)
} }
fn get_info( fn export_backup(&self, ptp: &mut PtpSession) -> Result<Vec<u8>, anyhow::Error> {
&self, const HANDLE: u32 = 0x0;
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<DeviceInfo, Box<dyn Error + Send + Sync>> {
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)?; debug!("Getting object info for backup");
Ok(info)
let info = ptp.command(
StandardCommandCode::GetObjectInfo,
&[HANDLE],
None,
Some(TIMEOUT),
)?;
debug!("Got object info, {} bytes", info.len());
debug!("Downloading backup object");
let object = ptp.command(
StandardCommandCode::GetObject,
&[HANDLE],
None,
Some(TIMEOUT),
)?;
debug!("Downloaded backup object ({} bytes)", object.len());
Ok(object)
}
fn import_backup(&self, ptp: &mut PtpSession, buffer: &[u8]) -> Result<(), anyhow::Error> {
todo!("This is currently broken");
debug!("Preparing ObjectInfo header for backup");
let mut obj_info = vec![0u8; 1088];
let mut offset = 0;
let padding0: u32 = 0x0;
let object_format: u16 = 0x5000;
let padding1: u16 = 0x0;
obj_info[offset..offset + size_of::<u32>()].copy_from_slice(&padding0.to_le_bytes());
offset += size_of::<u32>();
obj_info[offset..offset + size_of::<u16>()].copy_from_slice(&object_format.to_le_bytes());
offset += size_of::<u16>();
obj_info[offset..offset + size_of::<u16>()].copy_from_slice(&padding1.to_le_bytes());
offset += size_of::<u16>();
obj_info[offset..offset + size_of::<u32>()]
.copy_from_slice(&u32::try_from(buffer.len())?.to_le_bytes());
let param0: u32 = 0x0;
let param1: u32 = 0x0;
debug!("Sending ObjectInfo for backup");
ptp.command(
libptp::StandardCommandCode::SendObjectInfo,
&[param0, param1],
Some(&obj_info),
Some(TIMEOUT),
)?;
debug!("Sending backup payload ({} bytes)", buffer.len());
ptp.command(
libptp::StandardCommandCode::SendObject,
&[],
Some(buffer),
Some(TIMEOUT),
)?;
Ok(())
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] macro_rules! default_camera_impl {
pub struct CameraId { (
pub vendor: u16, $const_name:ident,
pub product: u16, $struct_name:ident,
$vendor:expr,
$product:expr,
$display_name:expr
) => {
pub const $const_name: CameraId = CameraId {
name: $display_name,
vendor: $vendor,
product: $product,
};
pub struct $struct_name {
device: rusb::Device<rusb::GlobalContext>,
session_counter: std::sync::atomic::AtomicU32,
} }
pub struct SupportedCamera { impl $struct_name {
pub id: CameraId, pub fn new_boxed(
pub factory: fn() -> Box<dyn CameraImpl>, rusb_device: &rusb::Device<rusb::GlobalContext>,
} ) -> Result<Box<dyn CameraImpl>, anyhow::Error> {
let session_counter = std::sync::atomic::AtomicU32::new(1);
pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera { let handle = rusb_device.open()?;
id: xt5::FUJIFILM_XT5, let device = handle.device();
factory: || Box::new(xt5::FujifilmXT5::new()),
}];
impl From<&SupportedCamera> for Box<dyn CameraImpl> { Ok(Box::new(Self {
fn from(camera: &SupportedCamera) -> Self { device,
(camera.factory)() session_counter,
}))
} }
} }
impl SupportedCamera { impl CameraImpl for $struct_name {
pub fn matches_descriptor(&self, descriptor: &DeviceDescriptor) -> bool { fn id(&self) -> &'static CameraId {
descriptor.vendor_id() == self.id.vendor && descriptor.product_id() == self.id.product &$const_name
}
fn device(&self) -> &rusb::Device<rusb::GlobalContext> {
&self.device
}
fn ptp(&self) -> Ptp {
libptp::Camera::new(&self.device).unwrap().into()
}
fn ptp_session(&self) -> Result<PtpSession, anyhow::Error> {
let ptp = self.ptp();
self.open_session(ptp)
}
fn next_session_id(&self) -> u32 {
self.session_counter
.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
} }
} }
};
}
default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5");

View File

@@ -1,31 +0,0 @@
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 {
session_counter: AtomicU32,
}
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)
}
}

View File

@@ -1,5 +1,3 @@
use std::error::Error;
use log::LevelFilter; use log::LevelFilter;
use log4rs::{ use log4rs::{
Config, Config,
@@ -8,7 +6,7 @@ use log4rs::{
encode::pattern::PatternEncoder, encode::pattern::PatternEncoder,
}; };
pub fn init(quiet: bool, verbose: bool) -> Result<(), Box<dyn Error + Send + Sync>> { pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> {
let level = if quiet { let level = if quiet {
LevelFilter::Warn LevelFilter::Warn
} else if verbose { } else if verbose {

View File

@@ -1,4 +1,5 @@
use std::error::Error; #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_docs_in_private_items)]
use clap::Parser; use clap::Parser;
use cli::Commands; use cli::Commands;
@@ -8,15 +9,16 @@ mod hardware;
mod log; mod log;
mod usb; mod usb;
fn main() -> Result<(), Box<dyn Error + Send + Sync>> { fn main() -> Result<(), anyhow::Error> {
let cli = cli::Cli::parse(); let cli = cli::Cli::parse();
log::init(cli.quiet, cli.verbose)?; log::init(cli.quiet, cli.verbose)?;
let device_id = cli.device.as_deref();
match cli.command { match cli.command {
Commands::Device(device_cmd) => { Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?,
cli::device::handle(device_cmd, cli.json, cli.device.as_deref())? Commands::Backup(backup_cmd) => cli::backup::handle(backup_cmd, device_id)?,
}
_ => todo!(), _ => todo!(),
} }

View File

@@ -1,75 +1,18 @@
use std::error::Error; use anyhow::{anyhow, bail};
use libptp::DeviceInfo; use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS};
use rusb::GlobalContext;
use crate::hardware::{FujiUsbMode, SUPPORTED_CAMERAS}; pub fn get_connected_camers() -> Result<Vec<Box<dyn crate::hardware::CameraImpl>>, anyhow::Error> {
pub struct Camera {
camera_impl: Box<dyn crate::hardware::CameraImpl>,
rusb_device: rusb::Device<GlobalContext>,
}
impl Camera {
pub fn id(&self) -> String {
let bus = self.rusb_device.bus_number();
let address = self.rusb_device.address();
format!("{}.{}", bus, address)
}
pub fn name(&self) -> String {
self.camera_impl.name().to_string()
}
pub fn vendor_id(&self) -> u16 {
let descriptor = self.rusb_device.device_descriptor().unwrap();
descriptor.vendor_id()
}
pub fn product_id(&self) -> u16 {
let descriptor = self.rusb_device.device_descriptor().unwrap();
descriptor.product_id()
}
pub fn ptp(&self) -> Result<libptp::Camera<GlobalContext>, Box<dyn Error + Send + Sync>> {
let handle = self.rusb_device.open()?;
let device = handle.device();
let ptp = libptp::Camera::new(&device)?;
Ok(ptp)
}
pub fn get_info(&self) -> Result<DeviceInfo, Box<dyn Error + Send + Sync>> {
let mut ptp = self.ptp()?;
self.camera_impl.get_info(&mut ptp)
}
pub fn get_fuji_usb_mode(&self) -> Result<FujiUsbMode, Box<dyn Error + Send + Sync>> {
let mut ptp = self.ptp()?;
self.camera_impl.get_fuji_usb_mode(&mut ptp)
}
pub fn get_fuji_battery_info(&self) -> Result<u32, Box<dyn Error + Send + Sync>> {
let mut ptp = self.ptp()?;
self.camera_impl.get_fuji_battery_info(&mut ptp)
}
}
pub fn get_connected_camers() -> Result<Vec<Camera>, Box<dyn Error + Send + Sync>> {
let mut connected_cameras = Vec::new(); let mut connected_cameras = Vec::new();
for device in rusb::devices()?.iter() { for device in rusb::devices()?.iter() {
let descriptor = match device.device_descriptor() { let Ok(descriptor) = device.device_descriptor() else {
Ok(d) => d, continue;
Err(_) => continue,
}; };
for camera in SUPPORTED_CAMERAS.iter() { for camera in SUPPORTED_CAMERAS {
if camera.matches_descriptor(&descriptor) { if camera.matches_descriptor(&descriptor) {
let camera = Camera { let camera = (camera.factory)(device)?;
camera_impl: camera.into(),
rusb_device: device,
};
connected_cameras.push(camera); connected_cameras.push(camera);
break; break;
} }
@@ -79,10 +22,10 @@ pub fn get_connected_camers() -> Result<Vec<Camera>, Box<dyn Error + Send + Sync
Ok(connected_cameras) Ok(connected_cameras)
} }
pub fn get_connected_camera_by_id(id: &str) -> Result<Camera, Box<dyn Error + Send + Sync>> { pub fn get_connected_camera_by_id(id: &str) -> Result<Box<dyn CameraImpl>, anyhow::Error> {
let parts: Vec<&str> = id.split('.').collect(); let parts: Vec<&str> = id.split('.').collect();
if parts.len() != 2 { if parts.len() != 2 {
return Err(format!("Invalid device id format: {}", id).into()); bail!("Invalid device id format: {id}");
} }
let bus: u8 = parts[0].parse()?; let bus: u8 = parts[0].parse()?;
@@ -92,28 +35,26 @@ pub fn get_connected_camera_by_id(id: &str) -> Result<Camera, Box<dyn Error + Se
if device.bus_number() == bus && device.address() == address { if device.bus_number() == bus && device.address() == address {
let descriptor = device.device_descriptor()?; let descriptor = device.device_descriptor()?;
for camera in SUPPORTED_CAMERAS.iter() { for camera in SUPPORTED_CAMERAS {
if camera.matches_descriptor(&descriptor) { if camera.matches_descriptor(&descriptor) {
return Ok(Camera { let camera = (camera.factory)(device)?;
camera_impl: camera.into(), return Ok(camera);
rusb_device: device,
});
} }
} }
return Err(format!("Device found at {} but is not supported", id).into()); bail!("Device found at {id} but is not supported");
} }
} }
Err(format!("No device found with id: {}", id).into()) bail!("No device found with id: {id}");
} }
pub fn get_camera(device_id: Option<&str>) -> Result<Camera, Box<dyn Error + Send + Sync>> { pub fn get_camera(device_id: Option<&str>) -> Result<Box<dyn CameraImpl>, anyhow::Error> {
match device_id { match device_id {
Some(id) => get_connected_camera_by_id(id), Some(id) => get_connected_camera_by_id(id),
None => get_connected_camers()? None => get_connected_camers()?
.into_iter() .into_iter()
.next() .next()
.ok_or_else(|| "No supported devices connected.".into()), .ok_or_else(|| anyhow!("No supported devices connected.")),
} }
} }