diff --git a/Cargo.lock b/Cargo.lock index c89f8bf..953add4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,6 +241,8 @@ dependencies = [ "log", "log4rs", "rusb", + "serde", + "serde_json", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index f022a2d..bcddae2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,5 @@ libptp = "0.6.5" log = "0.4.28" log4rs = "1.4.0" rusb = "0.9.4" +serde = { version = "1.0.228", features = ["derive"] } +serde_json = "1.0.145" diff --git a/src/cli/backup.rs b/src/cli/backup/mod.rs similarity index 100% rename from src/cli/backup.rs rename to src/cli/backup/mod.rs diff --git a/src/cli/device.rs b/src/cli/device.rs deleted file mode 100644 index 096657f..0000000 --- a/src/cli/device.rs +++ /dev/null @@ -1,12 +0,0 @@ -use clap::Subcommand; - -#[derive(Subcommand, Debug)] -pub enum DeviceCmd { - /// List devices - #[command(alias = "l")] - List, - - /// Dump device info - #[command(alias = "i")] - Info, -} diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs new file mode 100644 index 0000000..11b9be0 --- /dev/null +++ b/src/cli/device/mod.rs @@ -0,0 +1,139 @@ +use std::{error::Error, fmt}; + +use clap::Subcommand; +use serde::Serialize; + +use crate::usb; + +#[derive(Subcommand, Debug)] +pub enum DeviceCmd { + /// List devices + #[command(alias = "l")] + List, + + /// Dump device info + #[command(alias = "i")] + Info, +} + +#[derive(Serialize)] +pub struct DeviceItemRepr { + 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 { + id: device.id(), + name: device.name(), + vendor_id: format!("0x{:04x}", device.vendor_id()), + product_id: format!("0x{:04x}", device.product_id()), + } + } +} + +impl fmt::Display for DeviceItemRepr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{} ({}:{}) (ID: {})", + self.name, self.vendor_id, self.product_id, self.id + ) + } +} + +pub fn handle_list(json: bool) -> Result<(), Box> { + let devices: Vec = usb::get_connected_devices()? + .iter() + .map(|d| d.into()) + .collect(); + + if json { + println!("{}", serde_json::to_string_pretty(&devices)?); + return Ok(()); + } + + if devices.is_empty() { + println!("No supported devices connected."); + return Ok(()); + } + + println!("Connected devices:"); + for d in devices { + println!("- {}", d); + } + + Ok(()) +} + +#[derive(Serialize)] +pub struct DeviceRepr { + #[serde(flatten)] + pub device: DeviceItemRepr, + + pub manufacturer: String, + pub model: String, + pub device_version: String, + pub serial_number: String, +} + +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 { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Name: {}", self.device.name)?; + writeln!(f, "ID: {}", self.device.id)?; + writeln!( + f, + "Vendor ID: {}, Product ID: {}", + self.device.vendor_id, self.device.product_id + )?; + writeln!(f, "Manufacturer: {}", self.manufacturer)?; + writeln!(f, "Model: {}", self.model)?; + writeln!(f, "Device Version: {}", self.device_version)?; + write!(f, "Serial Number: {}", self.serial_number) + } +} + +pub fn handle_info( + json: bool, + device_id: Option<&str>, +) -> Result<(), Box> { + let device = usb::get_device(device_id)?; + + let mut camera = device.camera()?; + let info = device.model.get_device_info(&mut camera)?; + let repr = DeviceRepr::from_info(&device, &info); + + if json { + println!("{}", serde_json::to_string_pretty(&repr)?); + return Ok(()); + } + + println!("{}", repr); + Ok(()) +} + +pub fn handle( + cmd: DeviceCmd, + json: bool, + device_id: Option<&str>, +) -> Result<(), Box> { + match cmd { + DeviceCmd::List => handle_list(json), + DeviceCmd::Info => handle_info(json, device_id), + } +} diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 228fd5e..80e574d 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,8 +1,9 @@ -mod backup; mod common; -mod device; -mod render; -mod simulation; + +pub mod backup; +pub mod device; +pub mod render; +pub mod simulation; use clap::{Parser, Subcommand}; diff --git a/src/cli/render.rs b/src/cli/render/mod.rs similarity index 100% rename from src/cli/render.rs rename to src/cli/render/mod.rs diff --git a/src/cli/simulation.rs b/src/cli/simulation/mod.rs similarity index 100% rename from src/cli/simulation.rs rename to src/cli/simulation/mod.rs diff --git a/src/hardware/common.rs b/src/hardware/common.rs new file mode 100644 index 0000000..465c019 --- /dev/null +++ b/src/hardware/common.rs @@ -0,0 +1,3 @@ +use std::time::Duration; + +pub const TIMEOUT: Duration = Duration::from_millis(500); diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs new file mode 100644 index 0000000..b0e4adb --- /dev/null +++ b/src/hardware/mod.rs @@ -0,0 +1,36 @@ +use std::error::Error; + +use libptp::{DeviceInfo, StandardCommandCode}; +use log::debug; +use rusb::GlobalContext; + +mod common; +mod xt5; + +pub trait Camera { + fn vendor_id(&self) -> u16; + fn product_id(&self) -> u16; + fn name(&self) -> &'static str; + + fn get_device_info( + &self, + camera: &mut libptp::Camera, + ) -> Result> { + debug!("Using default GetDeviceInfo command for {}", self.name()); + + let response = camera.command( + StandardCommandCode::GetDeviceInfo, + &[], + None, + Some(common::TIMEOUT), + )?; + + debug!("Received response with {} bytes", response.len()); + + let device_info = DeviceInfo::decode(&response)?; + + Ok(device_info) + } +} + +pub const SUPPORTED_MODELS: &[&dyn Camera] = &[&xt5::FujifilmXT5]; diff --git a/src/hardware/xt5.rs b/src/hardware/xt5.rs new file mode 100644 index 0000000..cebd065 --- /dev/null +++ b/src/hardware/xt5.rs @@ -0,0 +1,18 @@ +use super::Camera; + +#[derive(Debug)] +pub struct FujifilmXT5; + +impl Camera for FujifilmXT5 { + fn vendor_id(&self) -> u16 { + 0x04cb + } + + fn product_id(&self) -> u16 { + 0x02fc + } + + fn name(&self) -> &'static str { + "FUJIFILM X-T5" + } +} diff --git a/src/log.rs b/src/log.rs index d2a58d0..5adce50 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,3 +1,5 @@ +use std::error::Error; + use log::LevelFilter; use log4rs::{ Config, @@ -6,7 +8,7 @@ use log4rs::{ encode::pattern::PatternEncoder, }; -pub fn init(quiet: bool, verbose: bool) -> Result<(), Box> { +pub fn init(quiet: bool, verbose: bool) -> Result<(), Box> { let level = if quiet { LevelFilter::Warn } else if verbose { diff --git a/src/main.rs b/src/main.rs index 325aa15..cfc5f1e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,14 +1,23 @@ -use clap::Parser; -mod cli; -mod log; +use std::error::Error; -fn main() -> Result<(), Box> { +use clap::Parser; +use cli::Commands; + +mod cli; +mod hardware; +mod log; +mod usb; + +fn main() -> Result<(), Box> { let cli = cli::Cli::parse(); log::init(cli.quiet, cli.verbose)?; match cli.command { - _ => {} + Commands::Device(device_cmd) => { + cli::device::handle(device_cmd, cli.json, cli.device.as_deref())? + } + _ => todo!(), } Ok(()) diff --git a/src/usb/mod.rs b/src/usb/mod.rs new file mode 100644 index 0000000..01c2ab3 --- /dev/null +++ b/src/usb/mod.rs @@ -0,0 +1,108 @@ +use std::error::Error; + +use rusb::GlobalContext; + +use crate::hardware::SUPPORTED_MODELS; + +#[derive(Clone)] +pub struct Device { + pub model: &'static dyn crate::hardware::Camera, + pub 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) + } + + 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.model.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 get_connected_devices() -> Result, Box> { + let mut connected_devices = Vec::new(); + + for device in rusb::devices()?.iter() { + let descriptor = match device.device_descriptor() { + Ok(d) => d, + Err(_) => 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, + rusb_device: device, + }; + + connected_devices.push(connected_device); + break; + } + } + } + + Ok(connected_devices) +} + +pub fn get_connected_device_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()); + } + + let bus: u8 = parts[0].parse()?; + let address: u8 = parts[1].parse()?; + + for device in rusb::devices()?.iter() { + if device.bus_number() == bus && device.address() == address { + let descriptor = device.device_descriptor()?; + + for model in SUPPORTED_MODELS.iter() { + if descriptor.vendor_id() == model.vendor_id() + && descriptor.product_id() == model.product_id() + { + return Ok(Device { + model: *model, + rusb_device: device, + }); + } + } + + return Err(format!("Device found at {} but is not supported", id).into()); + } + } + + Err(format!("No device found with id: {}", id).into()) +} + +pub fn get_device(device_id: Option<&str>) -> Result> { + match device_id { + Some(id) => get_connected_device_by_id(id), + None => get_connected_devices()? + .into_iter() + .next() + .ok_or_else(|| "No supported devices connected.".into()), + } +}