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 clap::Subcommand;
@@ -17,3 +21,36 @@ pub enum BackupCmd {
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)]
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)]
pub enum Output {
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 serde::Serialize;
use crate::usb;
use crate::{hardware::FujiUsbMode, usb};
#[derive(Subcommand, Debug)]
pub enum DeviceCmd {
/// List devices
/// List cameras
#[command(alias = "l")]
List,
/// Dump device info
/// Get camera info
#[command(alias = "i")]
Info,
}
#[derive(Serialize)]
pub struct DeviceItemRepr {
pub struct CameraItemRepr {
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 {
impl From<&usb::Camera> for CameraItemRepr {
fn from(device: &usb::Camera) -> Self {
CameraItemRepr {
id: device.id(),
name: device.name(),
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 {
write!(
f,
@@ -45,24 +45,24 @@ impl fmt::Display for DeviceItemRepr {
}
}
pub fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
let devices: Vec<DeviceItemRepr> = usb::get_connected_devices()?
fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()?
.iter()
.map(|d| d.into())
.collect();
if json {
println!("{}", serde_json::to_string_pretty(&devices)?);
println!("{}", serde_json::to_string_pretty(&cameras)?);
return Ok(());
}
if devices.is_empty() {
println!("No supported devices connected.");
if cameras.is_empty() {
println!("No supported cameras connected.");
return Ok(());
}
println!("Connected devices:");
for d in devices {
println!("Connected cameras:");
for d in cameras {
println!("- {}", d);
}
@@ -70,29 +70,19 @@ pub fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
}
#[derive(Serialize)]
pub struct DeviceRepr {
pub struct CameraRepr {
#[serde(flatten)]
pub device: DeviceItemRepr,
pub device: CameraItemRepr,
pub manufacturer: String,
pub model: String,
pub device_version: String,
pub serial_number: String,
pub mode: FujiUsbMode,
pub battery: u32,
}
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 {
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)?;
@@ -103,20 +93,29 @@ impl fmt::Display for DeviceRepr {
)?;
writeln!(f, "Manufacturer: {}", self.manufacturer)?;
writeln!(f, "Model: {}", self.model)?;
writeln!(f, "Device Version: {}", self.device_version)?;
write!(f, "Serial Number: {}", self.serial_number)
writeln!(f, "Version: {}", self.device_version)?;
writeln!(f, "Serial Number: {}", self.serial_number)?;
writeln!(f, "Mode: {}", self.mode)?;
write!(f, "Battery: {}%", self.battery)
}
}
pub fn handle_info(
json: bool,
device_id: Option<&str>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let device = usb::get_device(device_id)?;
fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box<dyn Error + Send + Sync>> {
let camera = usb::get_camera(device_id)?;
let mut camera = device.camera()?;
let info = device.model.get_device_info(&mut camera)?;
let repr = DeviceRepr::from_info(&device, &info);
let info = camera.get_info()?;
let mode = camera.get_fuji_usb_mode()?;
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 {
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 log::debug;
use rusb::GlobalContext;
use rusb::{DeviceDescriptor, GlobalContext};
use serde::Serialize;
mod common;
mod xt5;
pub trait Camera {
fn vendor_id(&self) -> u16;
fn product_id(&self) -> u16;
fn name(&self) -> &'static str;
pub const TIMEOUT: Duration = Duration::from_millis(500);
fn get_device_info(
&self,
camera: &mut libptp::Camera<GlobalContext>,
) -> Result<DeviceInfo, Box<dyn Error + Send + Sync>> {
debug!("Using default GetDeviceInfo command for {}", self.name());
#[repr(u32)]
#[derive(Debug, Clone, Copy)]
pub enum DevicePropCode {
FujiUsbMode = 0xd16e,
FujiBatteryInfo1 = 0xD36A,
FujiBatteryInfo2 = 0xD36B,
}
let response = camera.command(
StandardCommandCode::GetDeviceInfo,
&[],
None,
Some(common::TIMEOUT),
)?;
#[derive(Debug, Clone, Copy, Serialize)]
pub enum FujiUsbMode {
RawConversion, // mode == 6
Unsupported,
}
debug!("Received response with {} bytes", response.len());
let device_info = DeviceInfo::decode(&response)?;
Ok(device_info)
impl From<u32> for FujiUsbMode {
fn from(val: u32) -> Self {
match val {
6 => FujiUsbMode::RawConversion,
_ => 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)]
pub struct FujifilmXT5;
pub struct FujifilmXT5 {
session_counter: AtomicU32,
}
impl Camera for FujifilmXT5 {
fn vendor_id(&self) -> u16 {
0x04cb
}
fn product_id(&self) -> u16 {
0x02fc
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,23 +1,16 @@
use std::error::Error;
use libptp::DeviceInfo;
use rusb::GlobalContext;
use crate::hardware::SUPPORTED_MODELS;
use crate::hardware::{FujiUsbMode, SUPPORTED_CAMERAS};
#[derive(Clone)]
pub struct Device {
pub model: &'static dyn crate::hardware::Camera,
pub rusb_device: rusb::Device<GlobalContext>,
pub struct Camera {
camera_impl: Box<dyn crate::hardware::CameraImpl>,
rusb_device: rusb::Device<GlobalContext>,
}
impl Device {
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)
}
impl Camera {
pub fn id(&self) -> String {
let bus = self.rusb_device.bus_number();
let address = self.rusb_device.address();
@@ -25,7 +18,7 @@ impl Device {
}
pub fn name(&self) -> String {
self.model.name().to_string()
self.camera_impl.name().to_string()
}
pub fn vendor_id(&self) -> u16 {
@@ -37,10 +30,32 @@ impl Device {
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_devices() -> Result<Vec<Device>, Box<dyn Error + Send + Sync>> {
let mut connected_devices = Vec::new();
pub fn get_connected_camers() -> Result<Vec<Camera>, Box<dyn Error + Send + Sync>> {
let mut connected_cameras = Vec::new();
for device in rusb::devices()?.iter() {
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,
};
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,
for camera in SUPPORTED_CAMERAS.iter() {
if camera.matches_descriptor(&descriptor) {
let camera = Camera {
camera_impl: camera.into(),
rusb_device: device,
};
connected_devices.push(connected_device);
connected_cameras.push(camera);
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();
if parts.len() != 2 {
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 {
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,
for camera in SUPPORTED_CAMERAS.iter() {
if camera.matches_descriptor(&descriptor) {
return Ok(Camera {
camera_impl: camera.into(),
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())
}
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 {
Some(id) => get_connected_device_by_id(id),
None => get_connected_devices()?
Some(id) => get_connected_camera_by_id(id),
None => get_connected_camers()?
.into_iter()
.next()
.ok_or_else(|| "No supported devices connected.".into()),