diff --git a/src/camera/devices.rs b/src/camera/devices.rs
new file mode 100644
index 0000000..4513ff4
--- /dev/null
+++ b/src/camera/devices.rs
@@ -0,0 +1,63 @@
+use anyhow::bail;
+use rusb::GlobalContext;
+
+use super::CameraImpl;
+
+type ImplFactory
= fn() -> Box>;
+
+#[derive(Debug, Clone, Copy)]
+pub struct SupportedCamera {
+ pub name: &'static str,
+ pub vendor: u16,
+ pub product: u16,
+ pub impl_factory: ImplFactory,
+}
+
+impl SupportedCamera {
+ pub fn new_camera(&self, device: &rusb::Device
) -> anyhow::Result>> {
+ let descriptor = device.device_descriptor()?;
+
+ let matches =
+ descriptor.vendor_id() == self.vendor && descriptor.product_id() == self.product;
+
+ if !matches {
+ bail!(
+ "Device with vendor {:04x} and product {:04x} does not match {}",
+ descriptor.vendor_id(),
+ descriptor.product_id(),
+ self.name
+ );
+ }
+
+ Ok((self.impl_factory)())
+ }
+}
+
+macro_rules! default_camera_impl {
+ (
+ $const_name:ident,
+ $struct_name:ident,
+ $vendor:expr,
+ $product:expr,
+ $display_name:expr
+ ) => {
+ pub const $const_name: SupportedCamera = SupportedCamera {
+ name: $display_name,
+ vendor: $vendor,
+ product: $product,
+ impl_factory: || Box::new($struct_name {}),
+ };
+
+ pub struct $struct_name {}
+
+ impl crate::camera::CameraImpl for $struct_name {
+ fn supported_camera(&self) -> &'static SupportedCamera {
+ &$const_name
+ }
+ }
+ };
+}
+
+default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5");
+
+pub const SUPPORTED: &[SupportedCamera] = &[FUJIFILM_XT5];
diff --git a/src/camera/mod.rs b/src/camera/mod.rs
new file mode 100644
index 0000000..b9a5759
--- /dev/null
+++ b/src/camera/mod.rs
@@ -0,0 +1,283 @@
+pub mod devices;
+
+use std::{error::Error, fmt, io::Cursor, time::Duration};
+
+use anyhow::{anyhow, bail};
+use byteorder::{LittleEndian, WriteBytesExt};
+use devices::SupportedCamera;
+use libptp::{DeviceInfo, StandardCommandCode};
+use log::{debug, error};
+use rusb::GlobalContext;
+use serde::Serialize;
+
+#[derive(Debug)]
+pub struct UnsupportedFeatureError;
+
+impl fmt::Display for UnsupportedFeatureError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "feature is not supported for this device")
+ }
+}
+
+impl Error for UnsupportedFeatureError {}
+
+const SESSION: u32 = 1;
+
+pub struct Camera {
+ bus: u8,
+ address: u8,
+ ptp: libptp::Camera,
+ r#impl: Box>,
+}
+
+impl Camera {
+ pub fn from_device(device: &rusb::Device) -> anyhow::Result {
+ for supported_camera in devices::SUPPORTED {
+ if let Ok(r#impl) = supported_camera.new_camera(device) {
+ let bus = device.bus_number();
+ let address = device.address();
+ let mut ptp = libptp::Camera::new(device)?;
+
+ debug!("Opening session");
+ let () = r#impl.open_session(&mut ptp, SESSION)?;
+ debug!("Session opened");
+
+ return Ok(Self {
+ bus,
+ address,
+ ptp,
+ r#impl,
+ });
+ }
+ }
+
+ bail!("Device not supported");
+ }
+
+ pub fn name(&self) -> &'static str {
+ self.r#impl.supported_camera().name
+ }
+
+ pub fn vendor_id(&self) -> u16 {
+ self.r#impl.supported_camera().vendor
+ }
+
+ pub fn product_id(&self) -> u16 {
+ self.r#impl.supported_camera().product
+ }
+
+ pub fn connected_usb_id(&self) -> String {
+ format!("{}.{}", self.bus, self.address)
+ }
+
+ fn prop_value_as_scalar(data: &[u8]) -> anyhow::Result {
+ 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 {
+ let info = self.r#impl.get_info(&mut self.ptp)?;
+ Ok(info)
+ }
+
+ pub fn get_usb_mode(&mut self) -> anyhow::Result {
+ let data = self
+ .r#impl
+ .get_prop_value(&mut self.ptp, DevicePropCode::FujiUsbMode);
+
+ let result = Self::prop_value_as_scalar(&data?)?.into();
+ Ok(result)
+ }
+
+ pub fn get_battery_info(&mut self) -> anyhow::Result {
+ let data = self
+ .r#impl
+ .get_prop_value(&mut self.ptp, DevicePropCode::FujiBatteryInfo2);
+
+ let data = data?;
+ debug!("Raw battery data: {data:?}");
+
+ let utf16: Vec = data[1..]
+ .chunks(2)
+ .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
+ .take_while(|&c| c != 0)
+ .collect();
+
+ let utf8_string = String::from_utf16(&utf16)?;
+ debug!("Decoded UTF-16 string: {utf8_string}");
+
+ let percentage: u32 = utf8_string
+ .split(',')
+ .next()
+ .ok_or_else(|| anyhow!("Failed to parse battery percentage"))?
+ .parse()?;
+
+ Ok(percentage)
+ }
+
+ pub fn export_backup(&mut self) -> anyhow::Result> {
+ self.r#impl.export_backup(&mut self.ptp)
+ }
+
+ pub fn import_backup(&mut self, backup: &[u8]) -> anyhow::Result<()> {
+ self.r#impl.import_backup(&mut self.ptp, backup)
+ }
+}
+
+impl Drop for Camera {
+ fn drop(&mut self) {
+ debug!("Closing session");
+ if let Err(e) = self.r#impl.close_session(&mut self.ptp, SESSION) {
+ error!("Error closing session: {e}")
+ }
+ debug!("Session closed");
+ }
+}
+
+#[repr(u32)]
+#[derive(Debug, Clone, Copy)]
+pub enum DevicePropCode {
+ FujiUsbMode = 0xd16e,
+ FujiBatteryInfo2 = 0xD36B,
+}
+
+#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
+pub enum UsbMode {
+ RawConversion,
+ Unsupported,
+}
+
+impl From for UsbMode {
+ fn from(val: u32) -> Self {
+ match val {
+ 6 => Self::RawConversion,
+ _ => Self::Unsupported,
+ }
+ }
+}
+
+impl fmt::Display for UsbMode {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let s = match self {
+ Self::RawConversion => "USB RAW CONV./BACKUP RESTORE",
+ Self::Unsupported => "Unsupported USB Mode",
+ };
+ write!(f, "{s}")
+ }
+}
+
+pub trait CameraImpl {
+ fn supported_camera(&self) -> &'static SupportedCamera;
+
+ fn timeout(&self) -> Option {
+ None
+ }
+
+ fn open_session(&self, ptp: &mut libptp::Camera, session_id: u32) -> anyhow::Result<()> {
+ debug!("Sending OpenSession command");
+ _ = ptp.command(
+ StandardCommandCode::OpenSession,
+ &[session_id],
+ None,
+ self.timeout(),
+ )?;
+ Ok(())
+ }
+
+ fn close_session(&self, ptp: &mut libptp::Camera
, _: u32) -> anyhow::Result<()> {
+ debug!("Sending CloseSession command");
+ let _ = ptp.command(StandardCommandCode::CloseSession, &[], None, self.timeout())?;
+ Ok(())
+ }
+
+ fn get_info(&self, ptp: &mut libptp::Camera
) -> anyhow::Result {
+ debug!("Sending GetDeviceInfo command");
+ let response = ptp.command(
+ StandardCommandCode::GetDeviceInfo,
+ &[],
+ None,
+ self.timeout(),
+ )?;
+ debug!("Received response with {} bytes", response.len());
+ let info = DeviceInfo::decode(&response)?;
+ Ok(info)
+ }
+
+ fn get_prop_value(
+ &self,
+ ptp: &mut libptp::Camera,
+ prop: DevicePropCode,
+ ) -> anyhow::Result> {
+ debug!("Sending GetDevicePropValue command for property {prop:?}");
+ let response = ptp.command(
+ StandardCommandCode::GetDevicePropValue,
+ &[prop as u32],
+ None,
+ self.timeout(),
+ )?;
+ debug!("Received response with {} bytes", response.len());
+ Ok(response)
+ }
+
+ fn export_backup(&self, ptp: &mut libptp::Camera) -> anyhow::Result> {
+ const HANDLE: u32 = 0x0;
+
+ debug!("Sending GetObjectInfo command for backup");
+ let response = ptp.command(
+ StandardCommandCode::GetObjectInfo,
+ &[HANDLE],
+ None,
+ self.timeout(),
+ )?;
+ debug!("Received response with {} bytes", response.len());
+
+ debug!("Sending GetObject command for backup");
+ let response = ptp.command(
+ StandardCommandCode::GetObject,
+ &[HANDLE],
+ None,
+ self.timeout(),
+ )?;
+ debug!("Received response with {} bytes", response.len());
+
+ Ok(response)
+ }
+
+ fn import_backup(&self, ptp: &mut libptp::Camera, buffer: &[u8]) -> anyhow::Result<()> {
+ debug!("Preparing ObjectInfo header for backup");
+
+ let mut obj_info = vec![0u8; 1012];
+
+ let mut cursor = Cursor::new(&mut obj_info[..]);
+ cursor.write_u32::(0x0)?;
+ cursor.write_u16::(0x5000)?;
+ cursor.write_u16::(0x0)?;
+ cursor.write_u32::(u32::try_from(buffer.len())?)?;
+
+ debug!("Sending SendObjectInfo command for backup");
+ let response = ptp.command(
+ libptp::StandardCommandCode::SendObjectInfo,
+ &[0x0, 0x0],
+ Some(&obj_info),
+ self.timeout(),
+ )?;
+ debug!("Received response with {} bytes", response.len());
+
+ debug!("Sending SendObject command for backup");
+ let response = ptp.command(
+ libptp::StandardCommandCode::SendObject,
+ &[0x0],
+ Some(buffer),
+ self.timeout(),
+ )?;
+ debug!("Received response with {} bytes", response.len());
+
+ Ok(())
+ }
+}
diff --git a/src/cli/backup/mod.rs b/src/cli/backup/mod.rs
index 741310c..9511498 100644
--- a/src/cli/backup/mod.rs
+++ b/src/cli/backup/mod.rs
@@ -20,30 +20,28 @@ pub enum BackupCmd {
},
}
-fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
- let camera = usb::get_camera(device_id)?;
- let mut ptp = camera.ptp_session()?;
+fn handle_export(device_id: Option<&str>, output: &Output) -> anyhow::Result<()> {
+ let mut camera = usb::get_camera(device_id)?;
let mut writer = output.get_writer()?;
- let backup = camera.export_backup(&mut ptp)?;
+ let backup = camera.export_backup()?;
writer.write_all(&backup)?;
Ok(())
}
-fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> {
- let camera = usb::get_camera(device_id)?;
- let mut ptp = camera.ptp_session()?;
+fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> {
+ let mut camera = usb::get_camera(device_id)?;
let mut reader = input.get_reader()?;
- let mut buffer = Vec::new();
- reader.read_to_end(&mut buffer)?;
- camera.import_backup(&mut ptp, &buffer)?;
+ let mut backup = Vec::new();
+ reader.read_to_end(&mut backup)?;
+ camera.import_backup(&backup)?;
Ok(())
}
-pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> {
+pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> anyhow::Result<()> {
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 3b11bb3..b502aec 100644
--- a/src/cli/common/file.rs
+++ b/src/cli/common/file.rs
@@ -18,7 +18,7 @@ impl FromStr for Input {
}
impl Input {
- pub fn get_reader(&self) -> Result, anyhow::Error> {
+ pub fn get_reader(&self) -> anyhow::Result> {
match self {
Self::Stdin => Ok(Box::new(io::stdin())),
Self::Path(path) => Ok(Box::new(File::open(path)?)),
@@ -44,7 +44,7 @@ impl FromStr for Output {
}
impl Output {
- pub fn get_writer(&self) -> Result, anyhow::Error> {
+ pub fn get_writer(&self) -> anyhow::Result> {
match self {
Self::Stdout => Ok(Box::new(io::stdout())),
Self::Path(path) => Ok(Box::new(File::create(path)?)),
diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs
index 2c9a0bc..a669066 100644
--- a/src/cli/device/mod.rs
+++ b/src/cli/device/mod.rs
@@ -4,7 +4,7 @@ use clap::Subcommand;
use serde::Serialize;
use crate::{
- hardware::{CameraImpl, UsbMode},
+ camera::{Camera, UsbMode},
usb,
};
@@ -21,19 +21,19 @@ pub enum DeviceCmd {
#[derive(Serialize)]
pub struct CameraItemRepr {
- pub name: String,
- pub id: String,
+ pub name: &'static str,
+ pub usb_id: String,
pub vendor_id: String,
pub product_id: String,
}
-impl From<&Box> for CameraItemRepr {
- fn from(camera: &Box) -> Self {
+impl From<&Camera> for CameraItemRepr {
+ fn from(camera: &Camera) -> Self {
Self {
- id: camera.usb_id(),
- name: camera.id().name.to_string(),
- vendor_id: format!("0x{:04x}", camera.id().vendor),
- product_id: format!("0x{:04x}", camera.id().product),
+ 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()),
}
}
}
@@ -42,14 +42,14 @@ impl fmt::Display for CameraItemRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
- "{} ({}:{}) (ID: {})",
- self.name, self.vendor_id, self.product_id, self.id
+ "{} ({}:{}) (USB ID: {})",
+ self.name, self.vendor_id, self.product_id, self.usb_id
)
}
}
-fn handle_list(json: bool) -> Result<(), anyhow::Error> {
- let cameras: Vec = usb::get_connected_camers()?
+fn handle_list(json: bool) -> anyhow::Result<()> {
+ let cameras: Vec = usb::get_connected_cameras()?
.iter()
.map(std::convert::Into::into)
.collect();
@@ -88,7 +88,7 @@ pub struct CameraRepr {
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)?;
+ writeln!(f, "USB ID: {}", self.device.usb_id)?;
writeln!(
f,
"Vendor ID: {}, Product ID: {}",
@@ -103,15 +103,12 @@ impl fmt::Display for CameraRepr {
}
}
-fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> {
- let camera = usb::get_camera(device_id)?;
- let mut ptp = camera.ptp();
+fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
+ let mut camera = usb::get_camera(device_id)?;
- let info = camera.get_info(&mut ptp)?;
-
- let mut ptp = camera.open_session(ptp)?;
- let mode = camera.get_usb_mode(&mut ptp)?;
- let battery = camera.get_battery_info(&mut ptp)?;
+ let info = camera.get_info()?;
+ let mode = camera.get_usb_mode()?;
+ let battery = camera.get_battery_info()?;
let repr = CameraRepr {
device: (&camera).into(),
@@ -132,7 +129,7 @@ fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error>
Ok(())
}
-pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> {
+pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
match cmd {
DeviceCmd::List => handle_list(json),
DeviceCmd::Info => handle_info(json, device_id),
diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs
deleted file mode 100644
index 1b8d16d..0000000
--- a/src/hardware/mod.rs
+++ /dev/null
@@ -1,375 +0,0 @@
-use std::{
- fmt,
- io::Cursor,
- ops::{Deref, DerefMut},
- time::Duration,
-};
-
-use anyhow::bail;
-use byteorder::{LittleEndian, WriteBytesExt};
-use libptp::{DeviceInfo, StandardCommandCode};
-use log::{debug, error};
-use rusb::{DeviceDescriptor, GlobalContext};
-use serde::Serialize;
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct CameraId {
- pub name: &'static str,
- pub vendor: u16,
- pub product: u16,
-}
-
-type CameraFactory = fn(rusb::Device) -> Result, 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);
-
-#[repr(u32)]
-#[derive(Debug, Clone, Copy)]
-pub enum DevicePropCode {
- FujiUsbMode = 0xd16e,
- FujiBatteryInfo2 = 0xD36B,
-}
-
-#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
-pub enum UsbMode {
- RawConversion,
- Unsupported,
-}
-
-impl From for UsbMode {
- fn from(val: u32) -> Self {
- match val {
- 6 => Self::RawConversion,
- _ => Self::Unsupported,
- }
- }
-}
-
-impl fmt::Display for UsbMode {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- let s = match self {
- Self::RawConversion => "USB RAW CONV./BACKUP RESTORE",
- Self::Unsupported => "Unsupported USB Mode",
- };
- write!(f, "{s}")
- }
-}
-
-pub struct Ptp {
- ptp: libptp::Camera,
-}
-
-impl Deref for Ptp {
- type Target = libptp::Camera;
-
- fn deref(&self) -> &Self::Target {
- &self.ptp
- }
-}
-
-impl DerefMut for Ptp {
- fn deref_mut(&mut self) -> &mut Self::Target {
- &mut self.ptp
- }
-}
-
-impl From> for Ptp {
- fn from(ptp: libptp::Camera) -> Self {
- Self { ptp }
- }
-}
-
-type SessionCloseFn =
- Box) -> Result<(), anyhow::Error>>;
-
-pub struct PtpSession {
- ptp: libptp::Camera,
- session_id: u32,
- close_fn: Option,
-}
-
-impl Deref for PtpSession {
- type Target = libptp::Camera;
-
- 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() {
- if let Err(e) = close_fn(self.session_id, &mut self.ptp) {
- error!("Error closing session {}: {}", self.session_id, e);
- }
- }
- }
-}
-
-pub trait CameraImpl {
- fn id(&self) -> &'static CameraId;
-
- fn device(&self) -> &rusb::Device;
-
- 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;
-
- fn get_info(&self, ptp: &mut Ptp) -> 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)
- }
-
- fn next_session_id(&self) -> u32;
-
- fn open_session(&self, ptp: Ptp) -> Result {
- let session_id = self.next_session_id();
- let mut ptp = ptp.ptp;
-
- debug!("Opening session with id {session_id}");
- ptp.command(
- StandardCommandCode::OpenSession,
- &[session_id],
- None,
- Some(TIMEOUT),
- )?;
- debug!("Session {session_id} open");
-
- let close_fn: Option = Some(Box::new(move |_, ptp| {
- debug!("Closing session with id {session_id}");
- ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?;
- debug!("Session {session_id} closed");
- Ok(())
- }));
-
- Ok(PtpSession {
- ptp,
- session_id,
- close_fn,
- })
- }
-
- fn get_prop_value_raw(
- &self,
- ptp: &mut PtpSession,
- prop: DevicePropCode,
- ) -> Result, anyhow::Error> {
- debug!("Getting property {prop:?}");
-
- let response = ptp.command(
- StandardCommandCode::GetDevicePropValue,
- &[prop as u32],
- None,
- Some(TIMEOUT),
- )?;
-
- debug!("Received response with {} bytes", response.len());
-
- Ok(response)
- }
-
- fn get_prop_value_scalar(
- &self,
- ptp: &mut PtpSession,
- prop: DevicePropCode,
- ) -> Result {
- let data = self.get_prop_value_raw(ptp, prop)?;
-
- match data.len() {
- 1 => Ok(u32::from(data[0])),
- 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]])),
- n => bail!("Cannot parse property {prop:?} as scalar: {n} bytes"),
- }
- }
-
- fn get_usb_mode(&self, ptp: &mut PtpSession) -> Result {
- let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?;
- Ok(result.into())
- }
-
- fn get_battery_info(&self, ptp: &mut PtpSession) -> Result {
- let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?;
- debug!("Raw battery data: {data:?}");
-
- if data.len() < 3 {
- bail!("Battery info payload too short");
- }
-
- 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_else(|| anyhow::anyhow!("Failed to parse battery percentage"))?
- .parse()?;
-
- Ok(percentage)
- }
-
- fn export_backup(&self, ptp: &mut PtpSession) -> Result, anyhow::Error> {
- const HANDLE: u32 = 0x0;
-
- debug!("Getting object info for backup");
-
- 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 cursor = Cursor::new(&mut obj_info[..]);
- cursor.write_u32::(0x0)?;
- cursor.write_u16::(0x5000)?;
- cursor.write_u16::(0x0)?;
- cursor.write_u32::(u32::try_from(buffer.len())?)?;
-
- debug!("Sending ObjectInfo for backup");
-
- ptp.command(
- libptp::StandardCommandCode::SendObjectInfo,
- &[0x0, 0x0],
- Some(&obj_info),
- Some(TIMEOUT),
- )?;
-
- debug!("Sending backup payload ({} bytes)", buffer.len());
-
- ptp.command(
- libptp::StandardCommandCode::SendObject,
- &[],
- Some(buffer),
- Some(TIMEOUT),
- )?;
-
- Ok(())
- }
-}
-
-macro_rules! default_camera_impl {
- (
- $const_name:ident,
- $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,
- session_counter: std::sync::atomic::AtomicU32,
- }
-
- impl $struct_name {
- pub fn new_boxed(
- rusb_device: &rusb::Device,
- ) -> Result, anyhow::Error> {
- let session_counter = std::sync::atomic::AtomicU32::new(1);
-
- let handle = rusb_device.open()?;
- let device = handle.device();
-
- Ok(Box::new(Self {
- device,
- session_counter,
- }))
- }
- }
-
- impl CameraImpl for $struct_name {
- fn id(&self) -> &'static CameraId {
- &$const_name
- }
-
- fn device(&self) -> &rusb::Device {
- &self.device
- }
-
- fn ptp(&self) -> Ptp {
- libptp::Camera::new(&self.device).unwrap().into()
- }
-
- fn ptp_session(&self) -> Result {
- 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");
diff --git a/src/log.rs b/src/log.rs
index 396e3ac..786710e 100644
--- a/src/log.rs
+++ b/src/log.rs
@@ -6,7 +6,7 @@ use log4rs::{
encode::pattern::PatternEncoder,
};
-pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> {
+pub fn init(quiet: bool, verbose: bool) -> anyhow::Result<()> {
let level = if quiet {
LevelFilter::Warn
} else if verbose {
diff --git a/src/main.rs b/src/main.rs
index 62e449c..35d1d39 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,11 +5,11 @@ use clap::Parser;
use cli::Commands;
mod cli;
-mod hardware;
+mod camera;
mod log;
mod usb;
-fn main() -> Result<(), anyhow::Error> {
+fn main() -> anyhow::Result<()> {
let cli = cli::Cli::parse();
log::init(cli.quiet, cli.verbose)?;
diff --git a/src/usb/mod.rs b/src/usb/mod.rs
index 51ea467..13b6b51 100644
--- a/src/usb/mod.rs
+++ b/src/usb/mod.rs
@@ -1,28 +1,20 @@
use anyhow::{anyhow, bail};
-use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS};
+use crate::camera::Camera;
-pub fn get_connected_camers() -> Result>, anyhow::Error> {
+pub fn get_connected_cameras() -> anyhow::Result> {
let mut connected_cameras = Vec::new();
for device in rusb::devices()?.iter() {
- let Ok(descriptor) = device.device_descriptor() else {
- continue;
- };
-
- for camera in SUPPORTED_CAMERAS {
- if camera.matches_descriptor(&descriptor) {
- let camera = (camera.factory)(device)?;
- connected_cameras.push(camera);
- break;
- }
+ if let Ok(camera) = Camera::from_device(&device) {
+ connected_cameras.push(camera);
}
}
Ok(connected_cameras)
}
-pub fn get_connected_camera_by_id(id: &str) -> Result, anyhow::Error> {
+pub fn get_connected_camera_by_id(id: &str) -> anyhow::Result {
let parts: Vec<&str> = id.split('.').collect();
if parts.len() != 2 {
bail!("Invalid device id format: {id}");
@@ -33,26 +25,17 @@ pub fn get_connected_camera_by_id(id: &str) -> Result, anyho
for device in rusb::devices()?.iter() {
if device.bus_number() == bus && device.address() == address {
- let descriptor = device.device_descriptor()?;
-
- for camera in SUPPORTED_CAMERAS {
- if camera.matches_descriptor(&descriptor) {
- let camera = (camera.factory)(device)?;
- return Ok(camera);
- }
- }
-
- bail!("Device found at {id} but is not supported");
+ return Camera::from_device(&device);
}
}
bail!("No device found with id: {id}");
}
-pub fn get_camera(device_id: Option<&str>) -> Result, anyhow::Error> {
+pub fn get_camera(device_id: Option<&str>) -> anyhow::Result {
match device_id {
Some(id) => get_connected_camera_by_id(id),
- None => get_connected_camers()?
+ None => get_connected_cameras()?
.into_iter()
.next()
.ok_or_else(|| anyhow!("No supported devices connected.")),