chore: refactor impl

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-10-15 20:04:16 +01:00
parent 2e9fb61762
commit 943f22c074
9 changed files with 388 additions and 439 deletions

63
src/camera/devices.rs Normal file
View File

@@ -0,0 +1,63 @@
use anyhow::bail;
use rusb::GlobalContext;
use super::CameraImpl;
type ImplFactory<P> = fn() -> Box<dyn CameraImpl<P>>;
#[derive(Debug, Clone, Copy)]
pub struct SupportedCamera<P: rusb::UsbContext> {
pub name: &'static str,
pub vendor: u16,
pub product: u16,
pub impl_factory: ImplFactory<P>,
}
impl<P: rusb::UsbContext> SupportedCamera<P> {
pub fn new_camera(&self, device: &rusb::Device<P>) -> anyhow::Result<Box<dyn CameraImpl<P>>> {
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<GlobalContext> = SupportedCamera {
name: $display_name,
vendor: $vendor,
product: $product,
impl_factory: || Box::new($struct_name {}),
};
pub struct $struct_name {}
impl crate::camera::CameraImpl<GlobalContext> for $struct_name {
fn supported_camera(&self) -> &'static SupportedCamera<GlobalContext> {
&$const_name
}
}
};
}
default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5");
pub const SUPPORTED: &[SupportedCamera<GlobalContext>] = &[FUJIFILM_XT5];

283
src/camera/mod.rs Normal file
View File

@@ -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<GlobalContext>,
r#impl: Box<dyn CameraImpl<GlobalContext>>,
}
impl Camera {
pub fn from_device(device: &rusb::Device<GlobalContext>) -> anyhow::Result<Self> {
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<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)
}
pub fn get_usb_mode(&mut self) -> anyhow::Result<UsbMode> {
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<u32> {
let data = self
.r#impl
.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 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<Vec<u8>> {
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<u32> 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<P: rusb::UsbContext> {
fn supported_camera(&self) -> &'static SupportedCamera<P>;
fn timeout(&self) -> Option<Duration> {
None
}
fn open_session(&self, ptp: &mut libptp::Camera<P>, 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<P>, _: u32) -> anyhow::Result<()> {
debug!("Sending CloseSession command");
let _ = ptp.command(StandardCommandCode::CloseSession, &[], None, self.timeout())?;
Ok(())
}
fn get_info(&self, ptp: &mut libptp::Camera<P>) -> anyhow::Result<DeviceInfo> {
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<P>,
prop: DevicePropCode,
) -> anyhow::Result<Vec<u8>> {
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<P>) -> anyhow::Result<Vec<u8>> {
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<P>, 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::<LittleEndian>(0x0)?;
cursor.write_u16::<LittleEndian>(0x5000)?;
cursor.write_u16::<LittleEndian>(0x0)?;
cursor.write_u32::<LittleEndian>(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(())
}
}

View File

@@ -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),

View File

@@ -18,7 +18,7 @@ impl FromStr for Input {
}
impl Input {
pub fn get_reader(&self) -> Result<Box<dyn io::Read>, anyhow::Error> {
pub fn get_reader(&self) -> anyhow::Result<Box<dyn io::Read>> {
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<Box<dyn io::Write>, anyhow::Error> {
pub fn get_writer(&self) -> anyhow::Result<Box<dyn io::Write>> {
match self {
Self::Stdout => Ok(Box::new(io::stdout())),
Self::Path(path) => Ok(Box::new(File::create(path)?)),

View File

@@ -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<dyn CameraImpl>> for CameraItemRepr {
fn from(camera: &Box<dyn CameraImpl>) -> 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<CameraItemRepr> = usb::get_connected_camers()?
fn handle_list(json: bool) -> anyhow::Result<()> {
let cameras: Vec<CameraItemRepr> = 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),

View File

@@ -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<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);
#[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<u32> 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<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() {
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<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 open_session(&self, ptp: Ptp) -> Result<PtpSession, anyhow::Error> {
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<SessionCloseFn> = 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<Vec<u8>, 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<u32, anyhow::Error> {
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<UsbMode, anyhow::Error> {
let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?;
Ok(result.into())
}
fn get_battery_info(&self, ptp: &mut PtpSession) -> Result<u32, anyhow::Error> {
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<u16> = 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<Vec<u8>, 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::<LittleEndian>(0x0)?;
cursor.write_u16::<LittleEndian>(0x5000)?;
cursor.write_u16::<LittleEndian>(0x0)?;
cursor.write_u32::<LittleEndian>(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<rusb::GlobalContext>,
session_counter: std::sync::atomic::AtomicU32,
}
impl $struct_name {
pub fn new_boxed(
rusb_device: &rusb::Device<rusb::GlobalContext>,
) -> Result<Box<dyn CameraImpl>, 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<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

@@ -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 {

View File

@@ -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)?;

View File

@@ -1,28 +1,20 @@
use anyhow::{anyhow, bail};
use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS};
use crate::camera::Camera;
pub fn get_connected_camers() -> Result<Vec<Box<dyn crate::hardware::CameraImpl>>, anyhow::Error> {
pub fn get_connected_cameras() -> anyhow::Result<Vec<Camera>> {
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<Box<dyn CameraImpl>, anyhow::Error> {
pub fn get_connected_camera_by_id(id: &str) -> anyhow::Result<Camera> {
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<Box<dyn CameraImpl>, 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<Box<dyn CameraImpl>, anyhow::Error> {
pub fn get_camera(device_id: Option<&str>) -> anyhow::Result<Camera> {
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.")),