feat: usb mode, battery percentage

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-10-13 18:25:50 +01:00
parent f74328dfa8
commit 6bb4e6407a
7 changed files with 344 additions and 111 deletions

View File

@@ -1,3 +1,7 @@
use std::{error::Error, fmt};
use crate::usb;
use super::common::file::{Input, Output}; use super::common::file::{Input, Output};
use clap::Subcommand; use clap::Subcommand;
@@ -17,3 +21,36 @@ pub enum BackupCmd {
input_file: Input, input_file: Input,
}, },
} }
fn handle_export(
device_id: Option<&str>,
output: Output,
) -> Result<(), Box<dyn Error + Send + Sync>> {
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<dyn Error + Send + Sync>> {
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<dyn Error + Send + Sync>> {
match cmd {
BackupCmd::Export { output_file } => handle_export(device_id, output_file),
BackupCmd::Import { input_file } => handle_import(device_id, input_file),
}
}

View File

@@ -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)] #[derive(Debug, Clone)]
pub enum Input { pub enum Input {
@@ -17,6 +17,15 @@ impl FromStr for Input {
} }
} }
impl Input {
pub fn get_reader(&self) -> Result<Box<dyn io::Read>, Box<dyn Error + Send + Sync>> {
match self {
Input::Stdin => Ok(Box::new(io::stdin())),
Input::Path(path) => Ok(Box::new(File::open(path)?)),
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Output { pub enum Output {
Path(PathBuf), Path(PathBuf),
@@ -33,3 +42,12 @@ impl FromStr for Output {
} }
} }
} }
impl Output {
pub fn get_writer(&self) -> Result<Box<dyn io::Write>, Box<dyn Error + Send + Sync>> {
match self {
Output::Stdout => Ok(Box::new(io::stdout())),
Output::Path(path) => Ok(Box::new(File::create(path)?)),
}
}
}

View File

@@ -3,30 +3,30 @@ use std::{error::Error, fmt};
use clap::Subcommand; use clap::Subcommand;
use serde::Serialize; use serde::Serialize;
use crate::usb; use crate::{hardware::FujiUsbMode, usb};
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
pub enum DeviceCmd { pub enum DeviceCmd {
/// List devices /// List cameras
#[command(alias = "l")] #[command(alias = "l")]
List, List,
/// Dump device info /// Get camera info
#[command(alias = "i")] #[command(alias = "i")]
Info, Info,
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct DeviceItemRepr { pub struct CameraItemRepr {
pub name: String, pub name: String,
pub id: String, pub id: String,
pub vendor_id: String, pub vendor_id: String,
pub product_id: String, pub product_id: String,
} }
impl From<&usb::Device> for DeviceItemRepr { impl From<&usb::Camera> for CameraItemRepr {
fn from(device: &usb::Device) -> Self { fn from(device: &usb::Camera) -> Self {
DeviceItemRepr { CameraItemRepr {
id: device.id(), id: device.id(),
name: device.name(), name: device.name(),
vendor_id: format!("0x{:04x}", device.vendor_id()), 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!( write!(
f, f,
@@ -45,24 +45,24 @@ impl fmt::Display for DeviceItemRepr {
} }
} }
pub fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> { fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
let devices: Vec<DeviceItemRepr> = usb::get_connected_devices()? let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()?
.iter() .iter()
.map(|d| d.into()) .map(|d| d.into())
.collect(); .collect();
if json { if json {
println!("{}", serde_json::to_string_pretty(&devices)?); println!("{}", serde_json::to_string_pretty(&cameras)?);
return Ok(()); return Ok(());
} }
if devices.is_empty() { if cameras.is_empty() {
println!("No supported devices connected."); println!("No supported cameras connected.");
return Ok(()); return Ok(());
} }
println!("Connected devices:"); println!("Connected cameras:");
for d in devices { for d in cameras {
println!("- {}", d); println!("- {}", d);
} }
@@ -70,29 +70,19 @@ pub fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct DeviceRepr { pub struct CameraRepr {
#[serde(flatten)] #[serde(flatten)]
pub device: DeviceItemRepr, pub device: CameraItemRepr,
pub manufacturer: String, pub manufacturer: String,
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 battery: u32,
} }
impl DeviceRepr { impl fmt::Display for CameraRepr {
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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Name: {}", self.device.name)?; writeln!(f, "Name: {}", self.device.name)?;
writeln!(f, "ID: {}", self.device.id)?; writeln!(f, "ID: {}", self.device.id)?;
@@ -103,20 +93,29 @@ impl fmt::Display for DeviceRepr {
)?; )?;
writeln!(f, "Manufacturer: {}", self.manufacturer)?; writeln!(f, "Manufacturer: {}", self.manufacturer)?;
writeln!(f, "Model: {}", self.model)?; writeln!(f, "Model: {}", self.model)?;
writeln!(f, "Device Version: {}", self.device_version)?; writeln!(f, "Version: {}", self.device_version)?;
write!(f, "Serial Number: {}", self.serial_number) writeln!(f, "Serial Number: {}", self.serial_number)?;
writeln!(f, "Mode: {}", self.mode)?;
write!(f, "Battery: {}%", self.battery)
} }
} }
pub fn handle_info( fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box<dyn Error + Send + Sync>> {
json: bool, let camera = usb::get_camera(device_id)?;
device_id: Option<&str>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let device = usb::get_device(device_id)?;
let mut camera = device.camera()?; let info = camera.get_info()?;
let info = device.model.get_device_info(&mut camera)?; let mode = camera.get_fuji_usb_mode()?;
let repr = DeviceRepr::from_info(&device, &info); 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 { if json {
println!("{}", serde_json::to_string_pretty(&repr)?); println!("{}", serde_json::to_string_pretty(&repr)?);

View File

@@ -1,3 +0,0 @@
use std::time::Duration;
pub const TIMEOUT: Duration = Duration::from_millis(500);

View File

@@ -1,36 +1,194 @@
use std::error::Error; use std::{error::Error, fmt, time::Duration};
use libptp::{DeviceInfo, StandardCommandCode}; use libptp::{DeviceInfo, StandardCommandCode};
use log::debug; use log::debug;
use rusb::GlobalContext; use rusb::{DeviceDescriptor, GlobalContext};
use serde::Serialize;
mod common;
mod xt5; mod xt5;
pub trait Camera { pub const TIMEOUT: Duration = Duration::from_millis(500);
fn vendor_id(&self) -> u16;
fn product_id(&self) -> u16;
fn name(&self) -> &'static str;
fn get_device_info( #[repr(u32)]
&self, #[derive(Debug, Clone, Copy)]
camera: &mut libptp::Camera<GlobalContext>, pub enum DevicePropCode {
) -> Result<DeviceInfo, Box<dyn Error + Send + Sync>> { FujiUsbMode = 0xd16e,
debug!("Using default GetDeviceInfo command for {}", self.name()); FujiBatteryInfo1 = 0xD36A,
FujiBatteryInfo2 = 0xD36B,
}
let response = camera.command( #[derive(Debug, Clone, Copy, Serialize)]
StandardCommandCode::GetDeviceInfo, pub enum FujiUsbMode {
&[], RawConversion, // mode == 6
None, Unsupported,
Some(common::TIMEOUT), }
)?;
debug!("Received response with {} bytes", response.len()); impl From<u32> for FujiUsbMode {
fn from(val: u32) -> Self {
let device_info = DeviceInfo::decode(&response)?; match val {
6 => FujiUsbMode::RawConversion,
Ok(device_info) _ => 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<GlobalContext>,
session_id: u32,
) -> Result<(), Box<dyn Error + Send + Sync>> {
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<GlobalContext>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
debug!("Closing session");
ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?;
Ok(())
}
fn get_prop_value_raw(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
prop: DevicePropCode,
) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
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<GlobalContext>,
prop: DevicePropCode,
) -> Result<u32, Box<dyn Error + Send + Sync>> {
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<GlobalContext>,
) -> Result<FujiUsbMode, Box<dyn Error + Send + Sync>> {
let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?;
Ok(result.into())
}
fn get_fuji_battery_info(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<u32, Box<dyn Error + Send + Sync>> {
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<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("Failed to parse battery percentage")?
.parse()?;
Ok(percentage)
}
fn get_info(
&self,
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)?;
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<dyn CameraImpl>,
}
pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera {
id: xt5::FUJIFILM_XT5,
factory: || Box::new(xt5::FujifilmXT5::new()),
}];
impl From<&SupportedCamera> for Box<dyn CameraImpl> {
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
}
}

View File

@@ -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)] #[derive(Debug)]
pub struct FujifilmXT5; pub struct FujifilmXT5 {
session_counter: AtomicU32,
}
impl Camera for FujifilmXT5 { impl FujifilmXT5 {
fn vendor_id(&self) -> u16 { pub fn new() -> Self {
0x04cb Self {
} session_counter: AtomicU32::new(1),
}
fn product_id(&self) -> u16 {
0x02fc
} }
}
impl CameraImpl for FujifilmXT5 {
fn name(&self) -> &'static str { fn name(&self) -> &'static str {
"FUJIFILM X-T5" "FUJIFILM X-T5"
} }
fn next_session_id(&self) -> u32 {
self.session_counter.fetch_add(1, Ordering::SeqCst)
}
} }

View File

@@ -1,23 +1,16 @@
use std::error::Error; use std::error::Error;
use libptp::DeviceInfo;
use rusb::GlobalContext; use rusb::GlobalContext;
use crate::hardware::SUPPORTED_MODELS; use crate::hardware::{FujiUsbMode, SUPPORTED_CAMERAS};
#[derive(Clone)] pub struct Camera {
pub struct Device { camera_impl: Box<dyn crate::hardware::CameraImpl>,
pub model: &'static dyn crate::hardware::Camera, rusb_device: rusb::Device<GlobalContext>,
pub rusb_device: rusb::Device<GlobalContext>,
} }
impl Device { impl Camera {
pub fn camera(&self) -> Result<libptp::Camera<GlobalContext>, Box<dyn Error + Send + Sync>> {
let handle = self.rusb_device.open()?;
let device = handle.device();
let camera = libptp::Camera::new(&device)?;
Ok(camera)
}
pub fn id(&self) -> String { pub fn id(&self) -> String {
let bus = self.rusb_device.bus_number(); let bus = self.rusb_device.bus_number();
let address = self.rusb_device.address(); let address = self.rusb_device.address();
@@ -25,7 +18,7 @@ impl Device {
} }
pub fn name(&self) -> String { pub fn name(&self) -> String {
self.model.name().to_string() self.camera_impl.name().to_string()
} }
pub fn vendor_id(&self) -> u16 { pub fn vendor_id(&self) -> u16 {
@@ -37,10 +30,32 @@ impl Device {
let descriptor = self.rusb_device.device_descriptor().unwrap(); let descriptor = self.rusb_device.device_descriptor().unwrap();
descriptor.product_id() 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_devices() -> Result<Vec<Device>, Box<dyn Error + Send + Sync>> { pub fn get_connected_camers() -> Result<Vec<Camera>, Box<dyn Error + Send + Sync>> {
let mut connected_devices = 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 descriptor = match device.device_descriptor() {
@@ -48,25 +63,23 @@ pub fn get_connected_devices() -> Result<Vec<Device>, Box<dyn Error + Send + Syn
Err(_) => continue, Err(_) => continue,
}; };
for model in SUPPORTED_MODELS.iter() { for camera in SUPPORTED_CAMERAS.iter() {
if descriptor.vendor_id() == model.vendor_id() if camera.matches_descriptor(&descriptor) {
&& descriptor.product_id() == model.product_id() let camera = Camera {
{ camera_impl: camera.into(),
let connected_device = Device {
model: *model,
rusb_device: device, rusb_device: device,
}; };
connected_devices.push(connected_device); connected_cameras.push(camera);
break; break;
} }
} }
} }
Ok(connected_devices) Ok(connected_cameras)
} }
pub fn get_connected_device_by_id(id: &str) -> Result<Device, Box<dyn Error + Send + Sync>> { pub fn get_connected_camera_by_id(id: &str) -> Result<Camera, Box<dyn Error + Send + Sync>> {
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()); return Err(format!("Invalid device id format: {}", id).into());
@@ -79,12 +92,10 @@ pub fn get_connected_device_by_id(id: &str) -> Result<Device, 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 model in SUPPORTED_MODELS.iter() { for camera in SUPPORTED_CAMERAS.iter() {
if descriptor.vendor_id() == model.vendor_id() if camera.matches_descriptor(&descriptor) {
&& descriptor.product_id() == model.product_id() return Ok(Camera {
{ camera_impl: camera.into(),
return Ok(Device {
model: *model,
rusb_device: device, rusb_device: device,
}); });
} }
@@ -97,10 +108,10 @@ pub fn get_connected_device_by_id(id: &str) -> Result<Device, Box<dyn Error + Se
Err(format!("No device found with id: {}", id).into()) Err(format!("No device found with id: {}", id).into())
} }
pub fn get_device(device_id: Option<&str>) -> Result<Device, Box<dyn Error + Send + Sync>> { pub fn get_camera(device_id: Option<&str>) -> Result<Camera, Box<dyn Error + Send + Sync>> {
match device_id { match device_id {
Some(id) => get_connected_device_by_id(id), Some(id) => get_connected_camera_by_id(id),
None => get_connected_devices()? None => get_connected_camers()?
.into_iter() .into_iter()
.next() .next()
.ok_or_else(|| "No supported devices connected.".into()), .ok_or_else(|| "No supported devices connected.".into()),