chore: update README
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -236,6 +236,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
|||||||
name = "fujicli"
|
name = "fujicli"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"libptp",
|
"libptp",
|
||||||
"log",
|
"log",
|
||||||
|
@@ -14,6 +14,7 @@ lto = true
|
|||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
anyhow = "1.0.100"
|
||||||
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
|
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
|
||||||
libptp = "0.6.5"
|
libptp = "0.6.5"
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
|
20
README.md
20
README.md
@@ -1,3 +1,21 @@
|
|||||||
# fujicli
|
# fujicli
|
||||||
|
|
||||||
A CLI that performs on-device image rendering similar to Fujifilm X RAW Studio
|
A CLI to manage Fujifilm devices, simulations, backups, and rendering.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
This tool has only been tested with the **Fujifilm X-T5**, as it is the sole camera I own. While the underlying PTP commands may be compatible with other models, **compatibility is not guaranteed**.
|
||||||
|
|
||||||
|
**Use this software at your own risk.** I am not responsible for any damage, loss of data, or other adverse outcomes - physical or psychological - to your camera or equipment resulting from the use of this program.
|
||||||
|
|
||||||
|
This project is currently under heavy development. Contributions are welcome. If you own a different Fujifilm camera, testing and reporting compatibility is highly appreciated.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
This project builds upon the following fantastic reverse-engineering efforts:
|
||||||
|
|
||||||
|
* [fujihack](https://github.com/fujihack/fujihack)
|
||||||
|
* [fudge](https://github.com/petabyt/fudge)
|
||||||
|
* [libpict](https://github.com/petabyt/libpict)
|
||||||
|
* [fp](https://github.com/petabyt/fp)
|
||||||
|
* [libgphoto2](https://github.com/gphoto/libgphoto2)
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use crate::usb;
|
use crate::usb;
|
||||||
|
|
||||||
use super::common::file::{Input, Output};
|
use super::common::file::{Input, Output};
|
||||||
@@ -22,35 +20,32 @@ pub enum BackupCmd {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_export(
|
fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
|
||||||
device_id: Option<&str>,
|
|
||||||
output: Output,
|
|
||||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
|
||||||
let camera = usb::get_camera(device_id)?;
|
let camera = usb::get_camera(device_id)?;
|
||||||
|
let mut ptp = camera.ptp_session()?;
|
||||||
|
|
||||||
let mut writer = output.get_writer()?;
|
let mut writer = output.get_writer()?;
|
||||||
|
let backup = camera.export_backup(&mut ptp)?;
|
||||||
todo!();
|
writer.write_all(&backup)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_import(
|
fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> {
|
||||||
device_id: Option<&str>,
|
|
||||||
input: Input,
|
|
||||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
|
||||||
let camera = usb::get_camera(device_id)?;
|
let camera = usb::get_camera(device_id)?;
|
||||||
|
let mut ptp = camera.ptp_session()?;
|
||||||
|
|
||||||
let mut reader = input.get_reader()?;
|
let mut reader = input.get_reader()?;
|
||||||
|
let mut buffer = Vec::new();
|
||||||
todo!();
|
reader.read_to_end(&mut buffer)?;
|
||||||
|
camera.import_backup(&mut ptp, &buffer)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), Box<dyn Error + Send + Sync>> {
|
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> {
|
||||||
match cmd {
|
match cmd {
|
||||||
BackupCmd::Export { output_file } => handle_export(device_id, output_file),
|
BackupCmd::Export { output_file } => handle_export(device_id, &output_file),
|
||||||
BackupCmd::Import { input_file } => handle_import(device_id, input_file),
|
BackupCmd::Import { input_file } => handle_import(device_id, &input_file),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
use std::{error::Error, fs::File, io, path::PathBuf, str::FromStr};
|
use std::{fs::File, io, path::PathBuf, str::FromStr};
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum Input {
|
pub enum Input {
|
||||||
@@ -7,21 +7,21 @@ pub enum Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Input {
|
impl FromStr for Input {
|
||||||
type Err = Box<dyn Error + Send + Sync>;
|
type Err = anyhow::Error;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if s == "-" {
|
if s == "-" {
|
||||||
Ok(Input::Stdin)
|
Ok(Self::Stdin)
|
||||||
} else {
|
} else {
|
||||||
Ok(Input::Path(PathBuf::from(s)))
|
Ok(Self::Path(PathBuf::from(s)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Input {
|
impl Input {
|
||||||
pub fn get_reader(&self) -> Result<Box<dyn io::Read>, Box<dyn Error + Send + Sync>> {
|
pub fn get_reader(&self) -> Result<Box<dyn io::Read>, anyhow::Error> {
|
||||||
match self {
|
match self {
|
||||||
Input::Stdin => Ok(Box::new(io::stdin())),
|
Self::Stdin => Ok(Box::new(io::stdin())),
|
||||||
Input::Path(path) => Ok(Box::new(File::open(path)?)),
|
Self::Path(path) => Ok(Box::new(File::open(path)?)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,21 +33,21 @@ pub enum Output {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Output {
|
impl FromStr for Output {
|
||||||
type Err = Box<dyn Error + Send + Sync>;
|
type Err = anyhow::Error;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if s == "-" {
|
if s == "-" {
|
||||||
Ok(Output::Stdout)
|
Ok(Self::Stdout)
|
||||||
} else {
|
} else {
|
||||||
Ok(Output::Path(PathBuf::from(s)))
|
Ok(Self::Path(PathBuf::from(s)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Output {
|
impl Output {
|
||||||
pub fn get_writer(&self) -> Result<Box<dyn io::Write>, Box<dyn Error + Send + Sync>> {
|
pub fn get_writer(&self) -> Result<Box<dyn io::Write>, anyhow::Error> {
|
||||||
match self {
|
match self {
|
||||||
Output::Stdout => Ok(Box::new(io::stdout())),
|
Self::Stdout => Ok(Box::new(io::stdout())),
|
||||||
Output::Path(path) => Ok(Box::new(File::create(path)?)),
|
Self::Path(path) => Ok(Box::new(File::create(path)?)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
use std::error::Error;
|
use anyhow::bail;
|
||||||
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
@@ -9,18 +8,18 @@ pub enum SimulationSelector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for SimulationSelector {
|
impl std::str::FromStr for SimulationSelector {
|
||||||
type Err = Box<dyn Error + Send + Sync>;
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
if let Ok(slot) = s.parse::<u8>() {
|
if let Ok(slot) = s.parse::<u8>() {
|
||||||
return Ok(SimulationSelector::Slot(slot));
|
return Ok(Self::Slot(slot));
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.is_empty() {
|
if s.is_empty() {
|
||||||
Err("Simulation name cannot be empty".into())
|
bail!("Simulation name cannot be empty")
|
||||||
} else {
|
|
||||||
Ok(SimulationSelector::Name(s.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(Self::Name(s.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
use std::{error::Error, fmt};
|
use std::fmt;
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
hardware::{CameraImpl, FujiUsbMode},
|
hardware::{CameraImpl, UsbMode},
|
||||||
usb,
|
usb,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug, Clone, Copy)]
|
||||||
pub enum DeviceCmd {
|
pub enum DeviceCmd {
|
||||||
/// List cameras
|
/// List cameras
|
||||||
#[command(alias = "l")]
|
#[command(alias = "l")]
|
||||||
@@ -29,7 +29,7 @@ pub struct CameraItemRepr {
|
|||||||
|
|
||||||
impl From<&Box<dyn CameraImpl>> for CameraItemRepr {
|
impl From<&Box<dyn CameraImpl>> for CameraItemRepr {
|
||||||
fn from(camera: &Box<dyn CameraImpl>) -> Self {
|
fn from(camera: &Box<dyn CameraImpl>) -> Self {
|
||||||
CameraItemRepr {
|
Self {
|
||||||
id: camera.usb_id(),
|
id: camera.usb_id(),
|
||||||
name: camera.id().name.to_string(),
|
name: camera.id().name.to_string(),
|
||||||
vendor_id: format!("0x{:04x}", camera.id().vendor),
|
vendor_id: format!("0x{:04x}", camera.id().vendor),
|
||||||
@@ -48,10 +48,10 @@ impl fmt::Display for CameraItemRepr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
|
fn handle_list(json: bool) -> Result<(), anyhow::Error> {
|
||||||
let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()?
|
let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|d| d.into())
|
.map(std::convert::Into::into)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if json {
|
if json {
|
||||||
@@ -66,7 +66,7 @@ fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
|
|||||||
|
|
||||||
println!("Connected cameras:");
|
println!("Connected cameras:");
|
||||||
for d in cameras {
|
for d in cameras {
|
||||||
println!("- {}", d);
|
println!("- {d}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -81,7 +81,7 @@ pub struct CameraRepr {
|
|||||||
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 mode: UsbMode,
|
||||||
pub battery: u32,
|
pub battery: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,19 +103,22 @@ impl fmt::Display for CameraRepr {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box<dyn Error + Send + Sync>> {
|
fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> {
|
||||||
let mut camera = usb::get_camera(device_id)?;
|
let camera = usb::get_camera(device_id)?;
|
||||||
|
let mut ptp = camera.ptp();
|
||||||
|
|
||||||
let info = camera.get_info()?;
|
let info = camera.get_info(&mut ptp)?;
|
||||||
let mode = camera.get_fuji_usb_mode()?;
|
|
||||||
let battery = camera.get_fuji_battery_info()?;
|
let mut ptp = camera.open_session(ptp)?;
|
||||||
|
let mode = camera.get_usb_mode(&mut ptp)?;
|
||||||
|
let battery = camera.get_battery_info(&mut ptp)?;
|
||||||
|
|
||||||
let repr = CameraRepr {
|
let repr = CameraRepr {
|
||||||
device: (&camera).into(),
|
device: (&camera).into(),
|
||||||
manufacturer: info.Manufacturer.clone(),
|
manufacturer: info.Manufacturer.clone(),
|
||||||
model: info.Model.clone(),
|
model: info.Model.clone(),
|
||||||
device_version: info.DeviceVersion.clone(),
|
device_version: info.DeviceVersion.clone(),
|
||||||
serial_number: info.SerialNumber.clone(),
|
serial_number: info.SerialNumber,
|
||||||
mode,
|
mode,
|
||||||
battery,
|
battery,
|
||||||
};
|
};
|
||||||
@@ -125,15 +128,11 @@ fn handle_info(json: bool, device_id: Option<&str>) -> Result<(), Box<dyn Error
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{}", repr);
|
println!("{repr}");
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(
|
pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> Result<(), anyhow::Error> {
|
||||||
cmd: DeviceCmd,
|
|
||||||
json: bool,
|
|
||||||
device_id: Option<&str>,
|
|
||||||
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
|
||||||
match cmd {
|
match cmd {
|
||||||
DeviceCmd::List => handle_list(json),
|
DeviceCmd::List => handle_list(json),
|
||||||
DeviceCmd::Info => handle_info(json, device_id),
|
DeviceCmd::Info => handle_info(json, device_id),
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
use std::{error::Error, fmt, time::Duration};
|
use std::{
|
||||||
|
fmt,
|
||||||
|
ops::{Deref, DerefMut},
|
||||||
|
time::Duration,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
use libptp::{DeviceInfo, StandardCommandCode};
|
use libptp::{DeviceInfo, StandardCommandCode};
|
||||||
use log::debug;
|
use log::{debug, error};
|
||||||
use rusb::{DeviceDescriptor, GlobalContext};
|
use rusb::{DeviceDescriptor, GlobalContext};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
mod xt5;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct CameraId {
|
pub struct CameraId {
|
||||||
pub name: &'static str,
|
pub name: &'static str,
|
||||||
@@ -14,16 +17,16 @@ pub struct CameraId {
|
|||||||
pub product: u16,
|
pub product: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CameraFactory = fn(rusb::Device<GlobalContext>) -> Result<Box<dyn CameraImpl>, anyhow::Error>;
|
||||||
|
|
||||||
pub struct SupportedCamera {
|
pub struct SupportedCamera {
|
||||||
pub id: CameraId,
|
pub id: CameraId,
|
||||||
pub factory: fn(
|
pub factory: CameraFactory,
|
||||||
rusb::Device<GlobalContext>,
|
|
||||||
) -> Result<Box<dyn CameraImpl>, Box<dyn Error + Send + Sync>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera {
|
pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera {
|
||||||
id: xt5::FUJIFILM_XT5,
|
id: FUJIFILM_XT5,
|
||||||
factory: |d| xt5::FujifilmXT5::new_boxed(d),
|
factory: |d| FujifilmXT5::new_boxed(&d),
|
||||||
}];
|
}];
|
||||||
|
|
||||||
impl SupportedCamera {
|
impl SupportedCamera {
|
||||||
@@ -41,43 +44,106 @@ pub enum DevicePropCode {
|
|||||||
FujiBatteryInfo2 = 0xD36B,
|
FujiBatteryInfo2 = 0xD36B,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize)]
|
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
||||||
pub enum FujiUsbMode {
|
pub enum UsbMode {
|
||||||
RawConversion, // mode == 6
|
RawConversion,
|
||||||
Unsupported,
|
Unsupported,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<u32> for FujiUsbMode {
|
impl From<u32> for UsbMode {
|
||||||
fn from(val: u32) -> Self {
|
fn from(val: u32) -> Self {
|
||||||
match val {
|
match val {
|
||||||
6 => FujiUsbMode::RawConversion,
|
6 => Self::RawConversion,
|
||||||
_ => FujiUsbMode::Unsupported,
|
_ => Self::Unsupported,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for FujiUsbMode {
|
impl fmt::Display for UsbMode {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let s = match self {
|
let s = match self {
|
||||||
FujiUsbMode::RawConversion => "USB RAW CONV./BACKUP RESTORE",
|
Self::RawConversion => "USB RAW CONV./BACKUP RESTORE",
|
||||||
FujiUsbMode::Unsupported => "Unsupported USB Mode",
|
Self::Unsupported => "Unsupported USB Mode",
|
||||||
};
|
};
|
||||||
write!(f, "{}", s)
|
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 {
|
pub trait CameraImpl {
|
||||||
fn id(&self) -> &'static CameraId;
|
fn id(&self) -> &'static CameraId;
|
||||||
|
|
||||||
fn usb_id(&self) -> String;
|
fn device(&self) -> &rusb::Device<rusb::GlobalContext>;
|
||||||
|
|
||||||
fn ptp(&self) -> libptp::Camera<GlobalContext>;
|
fn usb_id(&self) -> String {
|
||||||
|
let bus = self.device().bus_number();
|
||||||
|
let address = self.device().address();
|
||||||
|
format!("{bus}.{address}")
|
||||||
|
}
|
||||||
|
|
||||||
fn get_info(&self) -> Result<DeviceInfo, Box<dyn Error + Send + Sync>> {
|
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");
|
debug!("Sending GetDeviceInfo command");
|
||||||
let response =
|
let response = ptp.command(StandardCommandCode::GetDeviceInfo, &[], None, Some(TIMEOUT))?;
|
||||||
self.ptp()
|
|
||||||
.command(StandardCommandCode::GetDeviceInfo, &[], None, Some(TIMEOUT))?;
|
|
||||||
debug!("Received response with {} bytes", response.len());
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
let info = DeviceInfo::decode(&response)?;
|
let info = DeviceInfo::decode(&response)?;
|
||||||
@@ -86,46 +152,47 @@ pub trait CameraImpl {
|
|||||||
|
|
||||||
fn next_session_id(&self) -> u32;
|
fn next_session_id(&self) -> u32;
|
||||||
|
|
||||||
fn open_session(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
fn open_session(&self, ptp: Ptp) -> Result<PtpSession, anyhow::Error> {
|
||||||
let session_id = self.next_session_id();
|
let session_id = self.next_session_id();
|
||||||
|
let mut ptp = ptp.ptp;
|
||||||
|
|
||||||
debug!("Opening new session with id {}", session_id);
|
debug!("Opening session with id {session_id}");
|
||||||
self.ptp().command(
|
ptp.command(
|
||||||
StandardCommandCode::OpenSession,
|
StandardCommandCode::OpenSession,
|
||||||
&[session_id],
|
&[session_id],
|
||||||
None,
|
None,
|
||||||
Some(TIMEOUT),
|
Some(TIMEOUT),
|
||||||
)?;
|
)?;
|
||||||
|
debug!("Session {session_id} open");
|
||||||
|
|
||||||
Ok(())
|
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(())
|
||||||
|
}));
|
||||||
|
|
||||||
fn close_session(&self) -> Result<(), Box<dyn Error + Send + Sync>> {
|
Ok(PtpSession {
|
||||||
debug!("Closing session");
|
ptp,
|
||||||
self.ptp()
|
session_id,
|
||||||
.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?;
|
close_fn,
|
||||||
|
})
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_prop_value_raw(
|
fn get_prop_value_raw(
|
||||||
&self,
|
&self,
|
||||||
|
ptp: &mut PtpSession,
|
||||||
prop: DevicePropCode,
|
prop: DevicePropCode,
|
||||||
) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
|
) -> Result<Vec<u8>, anyhow::Error> {
|
||||||
self.open_session()?;
|
debug!("Getting property {prop:?}");
|
||||||
|
|
||||||
debug!("Getting property {:?}", prop);
|
let response = ptp.command(
|
||||||
|
|
||||||
let response = self.ptp().command(
|
|
||||||
StandardCommandCode::GetDevicePropValue,
|
StandardCommandCode::GetDevicePropValue,
|
||||||
&[prop as u32],
|
&[prop as u32],
|
||||||
None,
|
None,
|
||||||
Some(TIMEOUT),
|
Some(TIMEOUT),
|
||||||
);
|
)?;
|
||||||
|
|
||||||
self.close_session()?;
|
|
||||||
|
|
||||||
let response = response?;
|
|
||||||
debug!("Received response with {} bytes", response.len());
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
@@ -133,29 +200,30 @@ pub trait CameraImpl {
|
|||||||
|
|
||||||
fn get_prop_value_scalar(
|
fn get_prop_value_scalar(
|
||||||
&self,
|
&self,
|
||||||
|
ptp: &mut PtpSession,
|
||||||
prop: DevicePropCode,
|
prop: DevicePropCode,
|
||||||
) -> Result<u32, Box<dyn Error + Send + Sync>> {
|
) -> Result<u32, anyhow::Error> {
|
||||||
let data = self.get_prop_value_raw(prop)?;
|
let data = self.get_prop_value_raw(ptp, prop)?;
|
||||||
|
|
||||||
match data.len() {
|
match data.len() {
|
||||||
1 => Ok(data[0] as u32),
|
1 => Ok(u32::from(data[0])),
|
||||||
2 => Ok(u16::from_le_bytes([data[0], data[1]]) as u32),
|
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]])),
|
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()),
|
n => bail!("Cannot parse property {prop:?} as scalar: {n} bytes"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_fuji_usb_mode(&self) -> Result<FujiUsbMode, Box<dyn Error + Send + Sync>> {
|
fn get_usb_mode(&self, ptp: &mut PtpSession) -> Result<UsbMode, anyhow::Error> {
|
||||||
let result = self.get_prop_value_scalar(DevicePropCode::FujiUsbMode)?;
|
let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?;
|
||||||
Ok(result.into())
|
Ok(result.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_fuji_battery_info(&self) -> Result<u32, Box<dyn Error + Send + Sync>> {
|
fn get_battery_info(&self, ptp: &mut PtpSession) -> Result<u32, anyhow::Error> {
|
||||||
let data = self.get_prop_value_raw(DevicePropCode::FujiBatteryInfo2)?;
|
let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?;
|
||||||
debug!("Raw battery data: {:?}", data);
|
debug!("Raw battery data: {data:?}");
|
||||||
|
|
||||||
if data.len() < 3 {
|
if data.len() < 3 {
|
||||||
return Err("Battery info payload too short".into());
|
bail!("Battery info payload too short");
|
||||||
}
|
}
|
||||||
|
|
||||||
let utf16: Vec<u16> = data[1..]
|
let utf16: Vec<u16> = data[1..]
|
||||||
@@ -164,17 +232,153 @@ pub trait CameraImpl {
|
|||||||
.take_while(|&c| c != 0)
|
.take_while(|&c| c != 0)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
debug!("Decoded UTF-16 units: {:?}", utf16);
|
debug!("Decoded UTF-16 units: {utf16:?}");
|
||||||
|
|
||||||
let utf8_string = String::from_utf16(&utf16)?;
|
let utf8_string = String::from_utf16(&utf16)?;
|
||||||
debug!("Decoded UTF-16 string: {}", utf8_string);
|
debug!("Decoded UTF-16 string: {utf8_string}");
|
||||||
|
|
||||||
let percentage: u32 = utf8_string
|
let percentage: u32 = utf8_string
|
||||||
.split(',')
|
.split(',')
|
||||||
.next()
|
.next()
|
||||||
.ok_or("Failed to parse battery percentage")?
|
.ok_or_else(|| anyhow::anyhow!("Failed to parse battery percentage"))?
|
||||||
.parse()?;
|
.parse()?;
|
||||||
|
|
||||||
Ok(percentage)
|
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 offset = 0;
|
||||||
|
|
||||||
|
let padding0: u32 = 0x0;
|
||||||
|
let object_format: u16 = 0x5000;
|
||||||
|
let padding1: u16 = 0x0;
|
||||||
|
|
||||||
|
obj_info[offset..offset + size_of::<u32>()].copy_from_slice(&padding0.to_le_bytes());
|
||||||
|
offset += size_of::<u32>();
|
||||||
|
obj_info[offset..offset + size_of::<u16>()].copy_from_slice(&object_format.to_le_bytes());
|
||||||
|
offset += size_of::<u16>();
|
||||||
|
obj_info[offset..offset + size_of::<u16>()].copy_from_slice(&padding1.to_le_bytes());
|
||||||
|
offset += size_of::<u16>();
|
||||||
|
obj_info[offset..offset + size_of::<u32>()]
|
||||||
|
.copy_from_slice(&u32::try_from(buffer.len())?.to_le_bytes());
|
||||||
|
|
||||||
|
let param0: u32 = 0x0;
|
||||||
|
let param1: u32 = 0x0;
|
||||||
|
|
||||||
|
debug!("Sending ObjectInfo for backup");
|
||||||
|
|
||||||
|
ptp.command(
|
||||||
|
libptp::StandardCommandCode::SendObjectInfo,
|
||||||
|
&[param0, param1],
|
||||||
|
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");
|
||||||
|
@@ -1,55 +0,0 @@
|
|||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
sync::atomic::{AtomicU32, Ordering},
|
|
||||||
};
|
|
||||||
|
|
||||||
use rusb::GlobalContext;
|
|
||||||
|
|
||||||
use super::{CameraId, CameraImpl};
|
|
||||||
|
|
||||||
pub const FUJIFILM_XT5: CameraId = CameraId {
|
|
||||||
name: "FUJIFILM XT-5",
|
|
||||||
vendor: 0x04cb,
|
|
||||||
product: 0x02fc,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub struct FujifilmXT5 {
|
|
||||||
device: rusb::Device<GlobalContext>,
|
|
||||||
session_counter: AtomicU32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FujifilmXT5 {
|
|
||||||
pub fn new_boxed(
|
|
||||||
rusb_device: rusb::Device<GlobalContext>,
|
|
||||||
) -> Result<Box<dyn CameraImpl>, Box<dyn Error + Send + Sync>> {
|
|
||||||
let session_counter = AtomicU32::new(1);
|
|
||||||
|
|
||||||
let handle = rusb_device.open()?;
|
|
||||||
let device = handle.device();
|
|
||||||
|
|
||||||
Ok(Box::new(Self {
|
|
||||||
session_counter,
|
|
||||||
device,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CameraImpl for FujifilmXT5 {
|
|
||||||
fn id(&self) -> &'static CameraId {
|
|
||||||
&FUJIFILM_XT5
|
|
||||||
}
|
|
||||||
|
|
||||||
fn usb_id(&self) -> String {
|
|
||||||
let bus = self.device.bus_number();
|
|
||||||
let address = self.device.address();
|
|
||||||
format!("{}.{}", bus, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ptp(&self) -> libptp::Camera<rusb::GlobalContext> {
|
|
||||||
libptp::Camera::new(&self.device).unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_session_id(&self) -> u32 {
|
|
||||||
self.session_counter.fetch_add(1, Ordering::SeqCst)
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,5 +1,3 @@
|
|||||||
use std::error::Error;
|
|
||||||
|
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use log4rs::{
|
use log4rs::{
|
||||||
Config,
|
Config,
|
||||||
@@ -8,7 +6,7 @@ use log4rs::{
|
|||||||
encode::pattern::PatternEncoder,
|
encode::pattern::PatternEncoder,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(quiet: bool, verbose: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
|
pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> {
|
||||||
let level = if quiet {
|
let level = if quiet {
|
||||||
LevelFilter::Warn
|
LevelFilter::Warn
|
||||||
} else if verbose {
|
} else if verbose {
|
||||||
|
12
src/main.rs
12
src/main.rs
@@ -1,4 +1,5 @@
|
|||||||
use std::error::Error;
|
#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
|
||||||
|
#![allow(clippy::missing_docs_in_private_items)]
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::Commands;
|
use cli::Commands;
|
||||||
@@ -8,15 +9,16 @@ mod hardware;
|
|||||||
mod log;
|
mod log;
|
||||||
mod usb;
|
mod usb;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
fn main() -> Result<(), anyhow::Error> {
|
||||||
let cli = cli::Cli::parse();
|
let cli = cli::Cli::parse();
|
||||||
|
|
||||||
log::init(cli.quiet, cli.verbose)?;
|
log::init(cli.quiet, cli.verbose)?;
|
||||||
|
|
||||||
|
let device_id = cli.device.as_deref();
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Commands::Device(device_cmd) => {
|
Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?,
|
||||||
cli::device::handle(device_cmd, cli.json, cli.device.as_deref())?
|
Commands::Backup(backup_cmd) => cli::backup::handle(backup_cmd, device_id)?,
|
||||||
}
|
|
||||||
_ => todo!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,18 +1,16 @@
|
|||||||
use std::error::Error;
|
use anyhow::{anyhow, bail};
|
||||||
|
|
||||||
use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS};
|
use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS};
|
||||||
|
|
||||||
pub fn get_connected_camers()
|
pub fn get_connected_camers() -> Result<Vec<Box<dyn crate::hardware::CameraImpl>>, anyhow::Error> {
|
||||||
-> Result<Vec<Box<dyn crate::hardware::CameraImpl>>, Box<dyn Error + Send + Sync>> {
|
|
||||||
let mut connected_cameras = 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 Ok(descriptor) = device.device_descriptor() else {
|
||||||
Ok(d) => d,
|
continue;
|
||||||
Err(_) => continue,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for camera in SUPPORTED_CAMERAS.iter() {
|
for camera in SUPPORTED_CAMERAS {
|
||||||
if camera.matches_descriptor(&descriptor) {
|
if camera.matches_descriptor(&descriptor) {
|
||||||
let camera = (camera.factory)(device)?;
|
let camera = (camera.factory)(device)?;
|
||||||
connected_cameras.push(camera);
|
connected_cameras.push(camera);
|
||||||
@@ -24,12 +22,10 @@ pub fn get_connected_camers()
|
|||||||
Ok(connected_cameras)
|
Ok(connected_cameras)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_connected_camera_by_id(
|
pub fn get_connected_camera_by_id(id: &str) -> Result<Box<dyn CameraImpl>, anyhow::Error> {
|
||||||
id: &str,
|
|
||||||
) -> Result<Box<dyn CameraImpl>, 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());
|
bail!("Invalid device id format: {id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
let bus: u8 = parts[0].parse()?;
|
let bus: u8 = parts[0].parse()?;
|
||||||
@@ -39,28 +35,26 @@ pub fn get_connected_camera_by_id(
|
|||||||
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 camera in SUPPORTED_CAMERAS.iter() {
|
for camera in SUPPORTED_CAMERAS {
|
||||||
if camera.matches_descriptor(&descriptor) {
|
if camera.matches_descriptor(&descriptor) {
|
||||||
let camera = (camera.factory)(device)?;
|
let camera = (camera.factory)(device)?;
|
||||||
return Ok(camera);
|
return Ok(camera);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Err(format!("Device found at {} but is not supported", id).into());
|
bail!("Device found at {id} but is not supported");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(format!("No device found with id: {}", id).into())
|
bail!("No device found with id: {id}");
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_camera(
|
pub fn get_camera(device_id: Option<&str>) -> Result<Box<dyn CameraImpl>, anyhow::Error> {
|
||||||
device_id: Option<&str>,
|
|
||||||
) -> Result<Box<dyn CameraImpl>, Box<dyn Error + Send + Sync>> {
|
|
||||||
match device_id {
|
match device_id {
|
||||||
Some(id) => get_connected_camera_by_id(id),
|
Some(id) => get_connected_camera_by_id(id),
|
||||||
None => get_connected_camers()?
|
None => get_connected_camers()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.ok_or_else(|| "No supported devices connected.".into()),
|
.ok_or_else(|| anyhow!("No supported devices connected.")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user