Compare commits
5 Commits
sdk
...
7c43e0f7ab
Author | SHA1 | Date | |
---|---|---|---|
7c43e0f7ab
|
|||
4825b699a6
|
|||
1f26a91dcd
|
|||
943f22c074
|
|||
2e9fb61762
|
13
Cargo.lock
generated
13
Cargo.lock
generated
@@ -237,8 +237,8 @@ name = "fujicli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"byteorder",
|
||||
"clap",
|
||||
"libptp",
|
||||
"log",
|
||||
"log4rs",
|
||||
"rusb",
|
||||
@@ -338,17 +338,6 @@ version = "0.2.177"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
|
||||
[[package]]
|
||||
name = "libptp"
|
||||
version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e6b84822d9579c3adb36bcea61c396dc2596a95ca03a0ffd69636fc85ccc4e2"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"log",
|
||||
"rusb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libusb1-sys"
|
||||
version = "0.7.0"
|
||||
|
@@ -15,8 +15,8 @@ codegen-units = 1
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
byteorder = "1.5.0"
|
||||
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
|
||||
libptp = "0.6.5"
|
||||
log = "0.4.28"
|
||||
log4rs = "1.4.0"
|
||||
rusb = "0.9.4"
|
||||
|
63
src/camera/devices/mod.rs
Normal file
63
src/camera/devices/mod.rs
Normal 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];
|
13
src/camera/error.rs
Normal file
13
src/camera/error.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[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 {}
|
291
src/camera/mod.rs
Normal file
291
src/camera/mod.rs
Normal file
@@ -0,0 +1,291 @@
|
||||
pub mod devices;
|
||||
pub mod error;
|
||||
pub mod ptp;
|
||||
|
||||
use std::{io::Cursor, time::Duration};
|
||||
|
||||
use anyhow::{anyhow, bail};
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use devices::SupportedCamera;
|
||||
use log::{debug, error};
|
||||
use ptp::{
|
||||
Ptp,
|
||||
enums::{CommandCode, PropCode, UsbMode},
|
||||
structs::DeviceInfo,
|
||||
};
|
||||
use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE};
|
||||
|
||||
const SESSION: u32 = 1;
|
||||
|
||||
pub struct Camera {
|
||||
r#impl: Box<dyn CameraImpl<GlobalContext>>,
|
||||
ptp: Ptp,
|
||||
}
|
||||
|
||||
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 config_descriptor = device.active_config_descriptor()?;
|
||||
|
||||
let interface_descriptor = config_descriptor
|
||||
.interfaces()
|
||||
.flat_map(|i| i.descriptors())
|
||||
.find(|x| x.class_code() == LIBUSB_CLASS_IMAGE)
|
||||
.ok_or(rusb::Error::NotFound)?;
|
||||
|
||||
let interface = interface_descriptor.interface_number();
|
||||
debug!("Found interface {interface}");
|
||||
|
||||
let handle = device.open()?;
|
||||
handle.claim_interface(interface)?;
|
||||
|
||||
let bulk_in = Self::find_endpoint(
|
||||
&interface_descriptor,
|
||||
rusb::Direction::In,
|
||||
rusb::TransferType::Bulk,
|
||||
)?;
|
||||
let bulk_out = Self::find_endpoint(
|
||||
&interface_descriptor,
|
||||
rusb::Direction::Out,
|
||||
rusb::TransferType::Bulk,
|
||||
)?;
|
||||
|
||||
let transaction_id = 0;
|
||||
|
||||
let chunk_size = r#impl.chunk_size();
|
||||
|
||||
let mut ptp = Ptp {
|
||||
bus,
|
||||
address,
|
||||
interface,
|
||||
bulk_in,
|
||||
bulk_out,
|
||||
handle,
|
||||
transaction_id,
|
||||
chunk_size,
|
||||
};
|
||||
|
||||
debug!("Opening session");
|
||||
let () = r#impl.open_session(&mut ptp, SESSION)?;
|
||||
debug!("Session opened");
|
||||
|
||||
return Ok(Self { r#impl, ptp });
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Device not supported");
|
||||
}
|
||||
|
||||
fn find_endpoint(
|
||||
interface_descriptor: &rusb::InterfaceDescriptor<'_>,
|
||||
direction: rusb::Direction,
|
||||
transfer_type: rusb::TransferType,
|
||||
) -> Result<u8, rusb::Error> {
|
||||
interface_descriptor
|
||||
.endpoint_descriptors()
|
||||
.find(|ep| ep.direction() == direction && ep.transfer_type() == transfer_type)
|
||||
.map(|x| x.address())
|
||||
.ok_or(rusb::Error::NotFound)
|
||||
}
|
||||
|
||||
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.ptp.bus, self.ptp.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, PropCode::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, PropCode::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");
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CameraImpl<P: rusb::UsbContext> {
|
||||
fn supported_camera(&self) -> &'static SupportedCamera<P>;
|
||||
|
||||
fn timeout(&self) -> Duration {
|
||||
Duration::default()
|
||||
}
|
||||
|
||||
fn chunk_size(&self) -> usize {
|
||||
1024 * 1024
|
||||
}
|
||||
|
||||
fn open_session(&self, ptp: &mut Ptp, session_id: u32) -> anyhow::Result<()> {
|
||||
debug!("Sending OpenSession command");
|
||||
_ = ptp.send(
|
||||
CommandCode::OpenSession,
|
||||
Some(&[session_id]),
|
||||
None,
|
||||
true,
|
||||
self.timeout(),
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_session(&self, ptp: &mut Ptp, _: u32) -> anyhow::Result<()> {
|
||||
debug!("Sending CloseSession command");
|
||||
_ = ptp.send(CommandCode::CloseSession, None, None, true, self.timeout())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_info(&self, ptp: &mut Ptp) -> anyhow::Result<DeviceInfo> {
|
||||
debug!("Sending GetDeviceInfo command");
|
||||
let response = ptp.send(CommandCode::GetDeviceInfo, None, None, true, self.timeout())?;
|
||||
debug!("Received response with {} bytes", response.len());
|
||||
let info = DeviceInfo::try_from(response.as_slice())?;
|
||||
Ok(info)
|
||||
}
|
||||
|
||||
fn get_prop_value(&self, ptp: &mut Ptp, prop: PropCode) -> anyhow::Result<Vec<u8>> {
|
||||
debug!("Sending GetDevicePropValue command for property {prop:?}");
|
||||
let response = ptp.send(
|
||||
CommandCode::GetDevicePropValue,
|
||||
Some(&[prop as u32]),
|
||||
None,
|
||||
true,
|
||||
self.timeout(),
|
||||
)?;
|
||||
debug!("Received response with {} bytes", response.len());
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn export_backup(&self, ptp: &mut Ptp) -> anyhow::Result<Vec<u8>> {
|
||||
const HANDLE: u32 = 0x0;
|
||||
|
||||
debug!("Sending GetObjectInfo command for backup");
|
||||
let response = ptp.send(
|
||||
CommandCode::GetObjectInfo,
|
||||
Some(&[HANDLE]),
|
||||
None,
|
||||
true,
|
||||
self.timeout(),
|
||||
)?;
|
||||
debug!("Received response with {} bytes", response.len());
|
||||
|
||||
debug!("Sending GetObject command for backup");
|
||||
let response = ptp.send(
|
||||
CommandCode::GetObject,
|
||||
Some(&[HANDLE]),
|
||||
None,
|
||||
true,
|
||||
self.timeout(),
|
||||
)?;
|
||||
debug!("Received response with {} bytes", response.len());
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> {
|
||||
debug!("Preparing ObjectInfo header for backup");
|
||||
|
||||
let mut header1 = vec![0u8; 1012];
|
||||
let mut cursor = Cursor::new(&mut header1[..]);
|
||||
cursor.write_u32::<LittleEndian>(0x0)?;
|
||||
cursor.write_u16::<LittleEndian>(0x5000)?;
|
||||
cursor.write_u16::<LittleEndian>(0x0)?;
|
||||
cursor.write_u32::<LittleEndian>(u32::try_from(buffer.len())?)?;
|
||||
|
||||
let header2 = vec![0u8; 64];
|
||||
|
||||
debug!("Sending SendObjectInfo command for backup");
|
||||
let response = ptp.send_many(
|
||||
CommandCode::SendObjectInfo,
|
||||
Some(&[0x0, 0x0]),
|
||||
Some(&[&header1, &header2]),
|
||||
true,
|
||||
self.timeout(),
|
||||
)?;
|
||||
debug!("Received response with {} bytes", response.len());
|
||||
|
||||
debug!("Sending SendObject command for backup");
|
||||
let response = ptp.send(
|
||||
CommandCode::SendObject,
|
||||
Some(&[0x0]),
|
||||
Some(buffer),
|
||||
true,
|
||||
self.timeout(),
|
||||
)?;
|
||||
debug!("Received response with {} bytes", response.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
205
src/camera/ptp/enums.rs
Normal file
205
src/camera/ptp/enums.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::bail;
|
||||
use serde::Serialize;
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum CommandCode {
|
||||
GetDeviceInfo = 0x1001,
|
||||
OpenSession = 0x1002,
|
||||
CloseSession = 0x1003,
|
||||
GetObjectInfo = 0x1008,
|
||||
GetObject = 0x1009,
|
||||
SendObjectInfo = 0x100C,
|
||||
SendObject = 0x100D,
|
||||
GetDevicePropValue = 0x1015,
|
||||
SetDevicePropValue = 0x1016,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for CommandCode {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x1001 => Ok(Self::GetDeviceInfo),
|
||||
0x1002 => Ok(Self::OpenSession),
|
||||
0x1003 => Ok(Self::CloseSession),
|
||||
0x1008 => Ok(Self::GetObjectInfo),
|
||||
0x1009 => Ok(Self::GetObject),
|
||||
0x100C => Ok(Self::SendObjectInfo),
|
||||
0x100D => Ok(Self::SendObject),
|
||||
0x1015 => Ok(Self::GetDevicePropValue),
|
||||
0x1016 => Ok(Self::SetDevicePropValue),
|
||||
v => bail!("Unknown command code '{v:x?}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ResponseCode {
|
||||
Undefined = 0x2000,
|
||||
Ok = 0x2001,
|
||||
GeneralError = 0x2002,
|
||||
SessionNotOpen = 0x2003,
|
||||
InvalidTransactionId = 0x2004,
|
||||
OperationNotSupported = 0x2005,
|
||||
ParameterNotSupported = 0x2006,
|
||||
IncompleteTransfer = 0x2007,
|
||||
InvalidStorageId = 0x2008,
|
||||
InvalidObjectHandle = 0x2009,
|
||||
DevicePropNotSupported = 0x200A,
|
||||
InvalidObjectFormatCode = 0x200B,
|
||||
StoreFull = 0x200C,
|
||||
ObjectWriteProtected = 0x200D,
|
||||
StoreReadOnly = 0x200E,
|
||||
AccessDenied = 0x200F,
|
||||
NoThumbnailPresent = 0x2010,
|
||||
SelfTestFailed = 0x2011,
|
||||
PartialDeletion = 0x2012,
|
||||
StoreNotAvailable = 0x2013,
|
||||
SpecificationByFormatUnsupported = 0x2014,
|
||||
NoValidObjectInfo = 0x2015,
|
||||
InvalidCodeFormat = 0x2016,
|
||||
UnknownVendorCode = 0x2017,
|
||||
CaptureAlreadyTerminated = 0x2018,
|
||||
DeviceBusy = 0x2019,
|
||||
InvalidParentObject = 0x201A,
|
||||
InvalidDevicePropFormat = 0x201B,
|
||||
InvalidDevicePropValue = 0x201C,
|
||||
InvalidParameter = 0x201D,
|
||||
SessionAlreadyOpen = 0x201E,
|
||||
TransactionCancelled = 0x201F,
|
||||
SpecificationOfDestinationUnsupported = 0x2020,
|
||||
}
|
||||
|
||||
impl std::convert::TryFrom<u16> for ResponseCode {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x2000 => Ok(Self::Undefined),
|
||||
0x2001 => Ok(Self::Ok),
|
||||
0x2002 => Ok(Self::GeneralError),
|
||||
0x2003 => Ok(Self::SessionNotOpen),
|
||||
0x2004 => Ok(Self::InvalidTransactionId),
|
||||
0x2005 => Ok(Self::OperationNotSupported),
|
||||
0x2006 => Ok(Self::ParameterNotSupported),
|
||||
0x2007 => Ok(Self::IncompleteTransfer),
|
||||
0x2008 => Ok(Self::InvalidStorageId),
|
||||
0x2009 => Ok(Self::InvalidObjectHandle),
|
||||
0x200A => Ok(Self::DevicePropNotSupported),
|
||||
0x200B => Ok(Self::InvalidObjectFormatCode),
|
||||
0x200C => Ok(Self::StoreFull),
|
||||
0x200D => Ok(Self::ObjectWriteProtected),
|
||||
0x200E => Ok(Self::StoreReadOnly),
|
||||
0x200F => Ok(Self::AccessDenied),
|
||||
0x2010 => Ok(Self::NoThumbnailPresent),
|
||||
0x2011 => Ok(Self::SelfTestFailed),
|
||||
0x2012 => Ok(Self::PartialDeletion),
|
||||
0x2013 => Ok(Self::StoreNotAvailable),
|
||||
0x2014 => Ok(Self::SpecificationByFormatUnsupported),
|
||||
0x2015 => Ok(Self::NoValidObjectInfo),
|
||||
0x2016 => Ok(Self::InvalidCodeFormat),
|
||||
0x2017 => Ok(Self::UnknownVendorCode),
|
||||
0x2018 => Ok(Self::CaptureAlreadyTerminated),
|
||||
0x2019 => Ok(Self::DeviceBusy),
|
||||
0x201A => Ok(Self::InvalidParentObject),
|
||||
0x201B => Ok(Self::InvalidDevicePropFormat),
|
||||
0x201C => Ok(Self::InvalidDevicePropValue),
|
||||
0x201D => Ok(Self::InvalidParameter),
|
||||
0x201E => Ok(Self::SessionAlreadyOpen),
|
||||
0x201F => Ok(Self::TransactionCancelled),
|
||||
0x2020 => Ok(Self::SpecificationOfDestinationUnsupported),
|
||||
v => bail!("Unknown response code '{v:x?}'"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u16)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ContainerCode {
|
||||
Command(CommandCode),
|
||||
Response(ResponseCode),
|
||||
}
|
||||
|
||||
impl From<ContainerCode> for u16 {
|
||||
fn from(code: ContainerCode) -> Self {
|
||||
match code {
|
||||
ContainerCode::Command(cmd) => cmd as Self,
|
||||
ContainerCode::Response(resp) => resp as Self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for ContainerCode {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
if let Ok(cmd) = CommandCode::try_from(value) {
|
||||
return Ok(Self::Command(cmd));
|
||||
}
|
||||
|
||||
if let Ok(resp) = ResponseCode::try_from(value) {
|
||||
return Ok(Self::Response(resp));
|
||||
}
|
||||
|
||||
bail!("Unknown container code '{value:x?}'");
|
||||
}
|
||||
}
|
||||
|
||||
#[repr(u32)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PropCode {
|
||||
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}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum ContainerType {
|
||||
Command = 1,
|
||||
Data = 2,
|
||||
Response = 3,
|
||||
Event = 4,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for ContainerType {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
1 => Ok(Self::Command),
|
||||
2 => Ok(Self::Data),
|
||||
3 => Ok(Self::Response),
|
||||
4 => Ok(Self::Event),
|
||||
v => bail!("Invalid message type '{v}'"),
|
||||
}
|
||||
}
|
||||
}
|
53
src/camera/ptp/error.rs
Normal file
53
src/camera/ptp/error.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use std::{fmt, io};
|
||||
|
||||
use crate::camera::ptp::enums::ResponseCode;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
Response(u16),
|
||||
Malformed(String),
|
||||
Usb(rusb::Error),
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Response(r) => {
|
||||
let name = ResponseCode::try_from(r)
|
||||
.map_or_else(|_| "Unknown".to_string(), |c| format!("{c:?}"));
|
||||
write!(f, "{name} (0x{r:04x})")
|
||||
}
|
||||
Self::Usb(ref e) => write!(f, "USB error: {e}"),
|
||||
Self::Io(ref e) => write!(f, "IO error: {e}"),
|
||||
Self::Malformed(ref e) => write!(f, "{e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ::std::error::Error for Error {
|
||||
fn cause(&self) -> Option<&dyn (::std::error::Error)> {
|
||||
match *self {
|
||||
Self::Usb(ref e) => Some(e),
|
||||
Self::Io(ref e) => Some(e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rusb::Error> for Error {
|
||||
fn from(e: rusb::Error) -> Self {
|
||||
Self::Usb(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(e: io::Error) -> Self {
|
||||
match e.kind() {
|
||||
io::ErrorKind::UnexpectedEof => {
|
||||
Self::Malformed("Unexpected end of message".to_string())
|
||||
}
|
||||
_ => Self::Io(e),
|
||||
}
|
||||
}
|
||||
}
|
274
src/camera/ptp/mod.rs
Normal file
274
src/camera/ptp/mod.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
pub mod enums;
|
||||
pub mod error;
|
||||
pub mod read;
|
||||
pub mod structs;
|
||||
|
||||
use std::{cmp::min, time::Duration};
|
||||
|
||||
use anyhow::bail;
|
||||
use byteorder::{LittleEndian, WriteBytesExt};
|
||||
use enums::{CommandCode, ContainerCode, ContainerType, ResponseCode};
|
||||
use log::{debug, error, trace};
|
||||
use rusb::GlobalContext;
|
||||
use structs::ContainerInfo;
|
||||
|
||||
pub struct Ptp {
|
||||
pub bus: u8,
|
||||
pub address: u8,
|
||||
pub interface: u8,
|
||||
pub bulk_in: u8,
|
||||
pub bulk_out: u8,
|
||||
pub handle: rusb::DeviceHandle<GlobalContext>,
|
||||
pub transaction_id: u32,
|
||||
pub chunk_size: usize,
|
||||
}
|
||||
|
||||
impl Ptp {
|
||||
pub fn send(
|
||||
&mut self,
|
||||
code: CommandCode,
|
||||
params: Option<&[u32]>,
|
||||
data: Option<&[u8]>,
|
||||
transaction: bool,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let (params, transaction_id) = self.prepare_send(params, transaction);
|
||||
self.send_header(code, params, transaction_id, timeout)?;
|
||||
if let Some(data) = data {
|
||||
self.write(ContainerType::Data, code, data, transaction_id, timeout)?;
|
||||
}
|
||||
self.receive_response(timeout)
|
||||
}
|
||||
|
||||
pub fn send_many(
|
||||
&mut self,
|
||||
code: CommandCode,
|
||||
params: Option<&[u32]>,
|
||||
data: Option<&[&[u8]]>,
|
||||
transaction: bool,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<Vec<u8>> {
|
||||
let (params, transaction_id) = self.prepare_send(params, transaction);
|
||||
self.send_header(code, params, transaction_id, timeout)?;
|
||||
if let Some(data) = data {
|
||||
self.write_many(ContainerType::Data, code, data, transaction_id, timeout)?;
|
||||
}
|
||||
self.receive_response(timeout)
|
||||
}
|
||||
|
||||
fn prepare_send<'a>(
|
||||
&mut self,
|
||||
params: Option<&'a [u32]>,
|
||||
transaction: bool,
|
||||
) -> (&'a [u32], Option<u32>) {
|
||||
let params = params.unwrap_or_default();
|
||||
let transaction_id = if transaction {
|
||||
let transaction_id = Some(self.transaction_id);
|
||||
self.transaction_id += 1;
|
||||
transaction_id
|
||||
} else {
|
||||
None
|
||||
};
|
||||
(params, transaction_id)
|
||||
}
|
||||
|
||||
fn send_header(
|
||||
&self,
|
||||
code: CommandCode,
|
||||
params: &[u32],
|
||||
transaction_id: Option<u32>,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut payload = Vec::with_capacity(params.len() * 4);
|
||||
for p in params {
|
||||
payload.write_u32::<LittleEndian>(*p).ok();
|
||||
}
|
||||
|
||||
trace!(
|
||||
"Sending PTP command: {:?}, transaction: {:?}, parameters ({} bytes): {:x?}",
|
||||
code,
|
||||
transaction_id,
|
||||
payload.len(),
|
||||
payload,
|
||||
);
|
||||
self.write(
|
||||
ContainerType::Command,
|
||||
code,
|
||||
&payload,
|
||||
transaction_id,
|
||||
timeout,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn receive_response(&self, timeout: Duration) -> anyhow::Result<Vec<u8>> {
|
||||
let mut response = Vec::new();
|
||||
loop {
|
||||
let (container, payload) = self.read(timeout)?;
|
||||
match container.kind {
|
||||
ContainerType::Data => {
|
||||
trace!("Response received: data ({} bytes)", payload.len());
|
||||
response = payload;
|
||||
}
|
||||
ContainerType::Response => {
|
||||
trace!("Response received: code {:?}", container.code);
|
||||
match container.code {
|
||||
ContainerCode::Command(_) | ContainerCode::Response(ResponseCode::Ok) => {}
|
||||
ContainerCode::Response(code) => {
|
||||
bail!(error::Error::Response(code as u16));
|
||||
}
|
||||
}
|
||||
|
||||
trace!(
|
||||
"Command completed successfully, response payload of {} bytes",
|
||||
response.len(),
|
||||
);
|
||||
return Ok(response);
|
||||
}
|
||||
_ => {
|
||||
debug!("Ignoring unexpected container type: {:?}", container.kind);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn write(
|
||||
&self,
|
||||
kind: ContainerType,
|
||||
code: CommandCode,
|
||||
payload: &[u8],
|
||||
transaction_id: Option<u32>,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
let container_info = ContainerInfo::new(kind, code, transaction_id, payload.len())?;
|
||||
let mut buffer: Vec<u8> = container_info.try_into()?;
|
||||
|
||||
let first_chunk_len = min(payload.len(), self.chunk_size - container_info.len());
|
||||
buffer.extend_from_slice(&payload[..first_chunk_len]);
|
||||
|
||||
trace!(
|
||||
"Writing PTP {kind:?} container, code: {code:?}, transaction: {transaction_id:?}, first payload chunk ({first_chunk_len} bytes)",
|
||||
);
|
||||
self.handle.write_bulk(self.bulk_out, &buffer, timeout)?;
|
||||
|
||||
for chunk in payload[first_chunk_len..].chunks(self.chunk_size) {
|
||||
trace!("Writing additional payload chunk ({} bytes)", chunk.len(),);
|
||||
self.handle.write_bulk(self.bulk_out, chunk, timeout)?;
|
||||
}
|
||||
|
||||
trace!(
|
||||
"Write completed for code {:?}, total payload of {} bytes",
|
||||
code,
|
||||
payload.len()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_many(
|
||||
&self,
|
||||
kind: ContainerType,
|
||||
code: CommandCode,
|
||||
parts: &[&[u8]],
|
||||
transaction_id: Option<u32>,
|
||||
timeout: Duration,
|
||||
) -> anyhow::Result<()> {
|
||||
if parts.is_empty() {
|
||||
return self.write(kind, code, &[], transaction_id, timeout);
|
||||
}
|
||||
|
||||
if parts.len() == 1 {
|
||||
return self.write(kind, code, parts[0], transaction_id, timeout);
|
||||
}
|
||||
|
||||
let total_len: usize = parts.iter().map(|c| c.len()).sum();
|
||||
let container_info = ContainerInfo::new(kind, code, transaction_id, total_len)?;
|
||||
let mut buffer: Vec<u8> = container_info.try_into()?;
|
||||
|
||||
let first = parts[0];
|
||||
let first_part_chunk_len = min(first.len(), self.chunk_size - container_info.len());
|
||||
buffer.extend_from_slice(&first[..first_part_chunk_len]);
|
||||
|
||||
trace!(
|
||||
"Writing PTP {kind:?} container, code: {code:?}, transaction: {transaction_id:?}, first payload part chunk ({first_part_chunk_len} bytes)",
|
||||
);
|
||||
self.handle.write_bulk(self.bulk_out, &buffer, timeout)?;
|
||||
|
||||
for chunk in first[first_part_chunk_len..].chunks(self.chunk_size) {
|
||||
trace!(
|
||||
"Writing additional payload part chunk ({} bytes)",
|
||||
chunk.len(),
|
||||
);
|
||||
self.handle.write_bulk(self.bulk_out, chunk, timeout)?;
|
||||
}
|
||||
|
||||
for part in &parts[1..] {
|
||||
trace!("Writing additional payload part");
|
||||
for chunk in part.chunks(self.chunk_size) {
|
||||
trace!(
|
||||
"Writing additional payload part chunk ({} bytes)",
|
||||
chunk.len(),
|
||||
);
|
||||
self.handle.write_bulk(self.bulk_out, chunk, timeout)?;
|
||||
}
|
||||
trace!(
|
||||
"Write completed for part, total payload of {} bytes",
|
||||
part.len()
|
||||
);
|
||||
}
|
||||
|
||||
trace!("Write completed for code {code:?}, total payload of {total_len} bytes");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read(&self, timeout: Duration) -> anyhow::Result<(ContainerInfo, Vec<u8>)> {
|
||||
let mut stack_buf = [0u8; 8 * 1024];
|
||||
|
||||
let n = self
|
||||
.handle
|
||||
.read_bulk(self.bulk_in, &mut stack_buf, timeout)?;
|
||||
let buf = &stack_buf[..n];
|
||||
trace!("Read chunk ({n} bytes)");
|
||||
|
||||
let container_info = ContainerInfo::try_from(buf)?;
|
||||
|
||||
let payload_len = container_info.payload_len();
|
||||
if payload_len == 0 {
|
||||
trace!("No payload in container");
|
||||
return Ok((container_info, Vec::new()));
|
||||
}
|
||||
|
||||
let mut payload = Vec::with_capacity(payload_len);
|
||||
if buf.len() > ContainerInfo::SIZE {
|
||||
payload.extend_from_slice(&buf[ContainerInfo::SIZE..]);
|
||||
}
|
||||
|
||||
while payload.len() < payload_len {
|
||||
let remaining = payload_len - payload.len();
|
||||
let mut chunk = vec![0u8; min(remaining, self.chunk_size)];
|
||||
let n = self.handle.read_bulk(self.bulk_in, &mut chunk, timeout)?;
|
||||
trace!("Read additional chunk ({n} bytes)");
|
||||
if n == 0 {
|
||||
break;
|
||||
}
|
||||
payload.extend_from_slice(&chunk[..n]);
|
||||
}
|
||||
|
||||
trace!(
|
||||
"Finished reading container, total payload of {} bytes",
|
||||
payload.len(),
|
||||
);
|
||||
|
||||
Ok((container_info, payload))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Ptp {
|
||||
fn drop(&mut self) {
|
||||
debug!("Releasing interface");
|
||||
if let Err(e) = self.handle.release_interface(self.interface) {
|
||||
error!("Error releasing interface: {e}");
|
||||
}
|
||||
debug!("Interface released");
|
||||
}
|
||||
}
|
127
src/camera/ptp/read.rs
Normal file
127
src/camera/ptp/read.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(clippy::redundant_closure_for_method_calls)]
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use anyhow::bail;
|
||||
use byteorder::{LittleEndian, ReadBytesExt};
|
||||
|
||||
pub trait Read: ReadBytesExt {
|
||||
fn read_ptp_u8(&mut self) -> anyhow::Result<u8> {
|
||||
Ok(self.read_u8()?)
|
||||
}
|
||||
|
||||
fn read_ptp_i8(&mut self) -> anyhow::Result<i8> {
|
||||
Ok(self.read_i8()?)
|
||||
}
|
||||
|
||||
fn read_ptp_u16(&mut self) -> anyhow::Result<u16> {
|
||||
Ok(self.read_u16::<LittleEndian>()?)
|
||||
}
|
||||
|
||||
fn read_ptp_i16(&mut self) -> anyhow::Result<i16> {
|
||||
Ok(self.read_i16::<LittleEndian>()?)
|
||||
}
|
||||
|
||||
fn read_ptp_u32(&mut self) -> anyhow::Result<u32> {
|
||||
Ok(self.read_u32::<LittleEndian>()?)
|
||||
}
|
||||
|
||||
fn read_ptp_i32(&mut self) -> anyhow::Result<i32> {
|
||||
Ok(self.read_i32::<LittleEndian>()?)
|
||||
}
|
||||
|
||||
fn read_ptp_u64(&mut self) -> anyhow::Result<u64> {
|
||||
Ok(self.read_u64::<LittleEndian>()?)
|
||||
}
|
||||
|
||||
fn read_ptp_i64(&mut self) -> anyhow::Result<i64> {
|
||||
Ok(self.read_i64::<LittleEndian>()?)
|
||||
}
|
||||
|
||||
fn read_ptp_u128(&mut self) -> anyhow::Result<u128> {
|
||||
Ok(self.read_u128::<LittleEndian>()?)
|
||||
}
|
||||
|
||||
fn read_ptp_i128(&mut self) -> anyhow::Result<i128> {
|
||||
Ok(self.read_i128::<LittleEndian>()?)
|
||||
}
|
||||
|
||||
fn read_ptp_vec<T: Sized, U: Fn(&mut Self) -> anyhow::Result<T>>(
|
||||
&mut self,
|
||||
func: U,
|
||||
) -> anyhow::Result<Vec<T>> {
|
||||
let len = self.read_u32::<LittleEndian>()? as usize;
|
||||
(0..len).map(|_| func(self)).collect()
|
||||
}
|
||||
|
||||
fn read_ptp_u8_vec(&mut self) -> anyhow::Result<Vec<u8>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_u8())
|
||||
}
|
||||
|
||||
fn read_ptp_i8_vec(&mut self) -> anyhow::Result<Vec<i8>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_i8())
|
||||
}
|
||||
|
||||
fn read_ptp_u16_vec(&mut self) -> anyhow::Result<Vec<u16>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_u16())
|
||||
}
|
||||
|
||||
fn read_ptp_i16_vec(&mut self) -> anyhow::Result<Vec<i16>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_i16())
|
||||
}
|
||||
|
||||
fn read_ptp_u32_vec(&mut self) -> anyhow::Result<Vec<u32>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_u32())
|
||||
}
|
||||
|
||||
fn read_ptp_i32_vec(&mut self) -> anyhow::Result<Vec<i32>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_i32())
|
||||
}
|
||||
|
||||
fn read_ptp_u64_vec(&mut self) -> anyhow::Result<Vec<u64>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_u64())
|
||||
}
|
||||
|
||||
fn read_ptp_i64_vec(&mut self) -> anyhow::Result<Vec<i64>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_i64())
|
||||
}
|
||||
|
||||
fn read_ptp_u128_vec(&mut self) -> anyhow::Result<Vec<u128>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_u128())
|
||||
}
|
||||
|
||||
fn read_ptp_i128_vec(&mut self) -> anyhow::Result<Vec<i128>> {
|
||||
self.read_ptp_vec(|cur| cur.read_ptp_i128())
|
||||
}
|
||||
|
||||
fn read_ptp_str(&mut self) -> anyhow::Result<String> {
|
||||
let len = self.read_u8()?;
|
||||
if len > 0 {
|
||||
let data: Vec<u16> = (0..(len - 1))
|
||||
.map(|_| self.read_u16::<LittleEndian>())
|
||||
.collect::<std::result::Result<_, _>>()?;
|
||||
self.read_u16::<LittleEndian>()?;
|
||||
Ok(String::from_utf16(&data)?)
|
||||
} else {
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn expect_end(&mut self) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
impl<T: AsRef<[u8]>> Read for Cursor<T> {
|
||||
fn expect_end(&mut self) -> anyhow::Result<()> {
|
||||
let len = self.get_ref().as_ref().len();
|
||||
if len as u64 != self.position() {
|
||||
bail!(super::error::Error::Malformed(format!(
|
||||
"Response {} bytes, expected {} bytes",
|
||||
len,
|
||||
self.position()
|
||||
)))
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
133
src/camera/ptp/structs.rs
Normal file
133
src/camera/ptp/structs.rs
Normal file
@@ -0,0 +1,133 @@
|
||||
use std::io::Cursor;
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
use super::{
|
||||
enums::{CommandCode, ContainerCode, ContainerType},
|
||||
read::Read,
|
||||
};
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug)]
|
||||
pub struct DeviceInfo {
|
||||
pub version: u16,
|
||||
pub vendor_ex_id: u32,
|
||||
pub vendor_ex_version: u16,
|
||||
pub vendor_extension_desc: String,
|
||||
pub functional_mode: u16,
|
||||
pub operations_supported: Vec<u16>,
|
||||
pub events_supported: Vec<u16>,
|
||||
pub device_properties_supported: Vec<u16>,
|
||||
pub capture_formats: Vec<u16>,
|
||||
pub image_formats: Vec<u16>,
|
||||
pub manufacturer: String,
|
||||
pub model: String,
|
||||
pub device_version: String,
|
||||
pub serial_number: String,
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for DeviceInfo {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
|
||||
let mut cur = Cursor::new(buf);
|
||||
|
||||
Ok(Self {
|
||||
version: cur.read_ptp_u16()?,
|
||||
vendor_ex_id: cur.read_ptp_u32()?,
|
||||
vendor_ex_version: cur.read_ptp_u16()?,
|
||||
vendor_extension_desc: cur.read_ptp_str()?,
|
||||
functional_mode: cur.read_ptp_u16()?,
|
||||
operations_supported: cur.read_ptp_u16_vec()?,
|
||||
events_supported: cur.read_ptp_u16_vec()?,
|
||||
device_properties_supported: cur.read_ptp_u16_vec()?,
|
||||
capture_formats: cur.read_ptp_u16_vec()?,
|
||||
image_formats: cur.read_ptp_u16_vec()?,
|
||||
manufacturer: cur.read_ptp_str()?,
|
||||
model: cur.read_ptp_str()?,
|
||||
device_version: cur.read_ptp_str()?,
|
||||
serial_number: cur.read_ptp_str()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ContainerInfo {
|
||||
pub total_len: u32,
|
||||
pub kind: ContainerType,
|
||||
pub code: ContainerCode,
|
||||
pub transaction_id: Option<u32>,
|
||||
}
|
||||
|
||||
impl ContainerInfo {
|
||||
const BASE_SIZE: usize = size_of::<u32>() + size_of::<u16>() + size_of::<u16>();
|
||||
pub const SIZE: usize = Self::BASE_SIZE + size_of::<u32>();
|
||||
|
||||
pub fn new(
|
||||
kind: ContainerType,
|
||||
code: CommandCode,
|
||||
transaction_id: Option<u32>,
|
||||
payload_len: usize,
|
||||
) -> anyhow::Result<Self> {
|
||||
let mut total_len = if transaction_id.is_some() {
|
||||
Self::SIZE
|
||||
} else {
|
||||
Self::BASE_SIZE
|
||||
};
|
||||
total_len += payload_len;
|
||||
|
||||
Ok(Self {
|
||||
total_len: u32::try_from(total_len)?,
|
||||
kind,
|
||||
code: ContainerCode::Command(code),
|
||||
transaction_id,
|
||||
})
|
||||
}
|
||||
|
||||
pub const fn len(&self) -> usize {
|
||||
if self.transaction_id.is_some() {
|
||||
Self::SIZE
|
||||
} else {
|
||||
Self::BASE_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn payload_len(&self) -> usize {
|
||||
self.total_len as usize - self.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&[u8]> for ContainerInfo {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
|
||||
let mut r = Cursor::new(bytes);
|
||||
|
||||
let total_len = r.read_u32::<LittleEndian>()?;
|
||||
let kind = ContainerType::try_from(r.read_u16::<LittleEndian>()?)?;
|
||||
let code = ContainerCode::try_from(r.read_u16::<LittleEndian>()?)?;
|
||||
let transaction_id = Some(r.read_u32::<LittleEndian>()?);
|
||||
|
||||
Ok(Self {
|
||||
total_len,
|
||||
kind,
|
||||
code,
|
||||
transaction_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<ContainerInfo> for Vec<u8> {
|
||||
type Error = anyhow::Error;
|
||||
|
||||
fn try_from(val: ContainerInfo) -> Result<Self, Self::Error> {
|
||||
let mut buf = Self::with_capacity(val.len());
|
||||
buf.write_u32::<LittleEndian>(val.total_len)?;
|
||||
buf.write_u16::<LittleEndian>(val.kind as u16)?;
|
||||
buf.write_u16::<LittleEndian>(val.code.into())?;
|
||||
if let Some(transaction_id) = val.transaction_id {
|
||||
buf.write_u32::<LittleEndian>(transaction_id)?;
|
||||
}
|
||||
Ok(buf)
|
||||
}
|
||||
}
|
@@ -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),
|
||||
|
@@ -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)?)),
|
||||
|
@@ -1,27 +1,4 @@
|
||||
use anyhow::bail;
|
||||
use clap::Args;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SimulationSelector {
|
||||
Slot(u8),
|
||||
Name(String),
|
||||
}
|
||||
|
||||
impl std::str::FromStr for SimulationSelector {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
if let Ok(slot) = s.parse::<u8>() {
|
||||
return Ok(Self::Slot(slot));
|
||||
}
|
||||
|
||||
if s.is_empty() {
|
||||
bail!("Simulation name cannot be empty")
|
||||
}
|
||||
|
||||
Ok(Self::Name(s.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Args, Debug)]
|
||||
pub struct FilmSimulationOptions {}
|
||||
|
@@ -4,7 +4,7 @@ use clap::Subcommand;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
hardware::{CameraImpl, UsbMode},
|
||||
camera::{Camera, ptp::enums::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,22 +103,19 @@ 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(),
|
||||
manufacturer: info.Manufacturer.clone(),
|
||||
model: info.Model.clone(),
|
||||
device_version: info.DeviceVersion.clone(),
|
||||
serial_number: info.SerialNumber,
|
||||
manufacturer: info.manufacturer.clone(),
|
||||
model: info.model.clone(),
|
||||
device_version: info.device_version.clone(),
|
||||
serial_number: info.serial_number,
|
||||
mode,
|
||||
battery,
|
||||
};
|
||||
@@ -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),
|
||||
|
@@ -5,7 +5,7 @@ pub mod device;
|
||||
pub mod render;
|
||||
pub mod simulation;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
use clap::{ArgAction, Parser, Subcommand};
|
||||
|
||||
use backup::BackupCmd;
|
||||
use device::DeviceCmd;
|
||||
@@ -23,13 +23,9 @@ pub struct Cli {
|
||||
#[arg(long, short = 'j', global = true)]
|
||||
pub json: bool,
|
||||
|
||||
/// Only log warnings and errors
|
||||
#[arg(long, short = 'q', global = true, conflicts_with = "verbose")]
|
||||
pub quiet: bool,
|
||||
|
||||
/// Log extra debugging information
|
||||
#[arg(long, short = 'v', global = true, conflicts_with = "quiet")]
|
||||
pub verbose: bool,
|
||||
/// Log extra debugging information (multiple instances increase verbosity)
|
||||
#[arg(long, short = 'v', action = ArgAction::Count, global = true)]
|
||||
pub verbose: u8,
|
||||
|
||||
/// Manually specify target device
|
||||
#[arg(long, short = 'd', global = true)]
|
||||
|
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
||||
|
||||
use super::common::{
|
||||
file::{Input, Output},
|
||||
film::{FilmSimulationOptions, SimulationSelector},
|
||||
film::FilmSimulationOptions,
|
||||
};
|
||||
use clap::Args;
|
||||
|
||||
@@ -10,7 +10,7 @@ use clap::Args;
|
||||
pub struct RenderCmd {
|
||||
/// Simulation number or name
|
||||
#[arg(long, conflicts_with = "simulation_file")]
|
||||
simulation: Option<SimulationSelector>,
|
||||
simulation: Option<u8>,
|
||||
|
||||
/// Path to exported simulation
|
||||
#[arg(long, conflicts_with = "simulation")]
|
||||
|
@@ -1,6 +1,6 @@
|
||||
use super::common::{
|
||||
file::{Input, Output},
|
||||
film::{FilmSimulationOptions, SimulationSelector},
|
||||
film::FilmSimulationOptions,
|
||||
};
|
||||
use clap::Subcommand;
|
||||
|
||||
@@ -14,14 +14,14 @@ pub enum SimulationCmd {
|
||||
#[command(alias = "g")]
|
||||
Get {
|
||||
/// Simulation number or name
|
||||
simulation: SimulationSelector,
|
||||
simulation: u8,
|
||||
},
|
||||
|
||||
/// Set simulation parameters
|
||||
#[command(alias = "s")]
|
||||
Set {
|
||||
/// Simulation number or name
|
||||
simulation: SimulationSelector,
|
||||
simulation: u8,
|
||||
|
||||
#[command(flatten)]
|
||||
film_simulation_options: FilmSimulationOptions,
|
||||
@@ -31,7 +31,7 @@ pub enum SimulationCmd {
|
||||
#[command(alias = "e")]
|
||||
Export {
|
||||
/// Simulation number or name
|
||||
simulation: SimulationSelector,
|
||||
simulation: u8,
|
||||
|
||||
/// Output file (use '-' to write to stdout)
|
||||
output_file: Output,
|
||||
|
@@ -1,384 +0,0 @@
|
||||
use std::{
|
||||
fmt,
|
||||
ops::{Deref, DerefMut},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use anyhow::bail;
|
||||
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 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");
|
13
src/log.rs
13
src/log.rs
@@ -6,13 +6,12 @@ use log4rs::{
|
||||
encode::pattern::PatternEncoder,
|
||||
};
|
||||
|
||||
pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> {
|
||||
let level = if quiet {
|
||||
LevelFilter::Warn
|
||||
} else if verbose {
|
||||
LevelFilter::Debug
|
||||
} else {
|
||||
LevelFilter::Info
|
||||
pub fn init(verbose: u8) -> anyhow::Result<()> {
|
||||
let level = match verbose {
|
||||
0 => LevelFilter::Warn,
|
||||
1 => LevelFilter::Info,
|
||||
2 => LevelFilter::Debug,
|
||||
_ => LevelFilter::Trace,
|
||||
};
|
||||
|
||||
let encoder = Box::new(PatternEncoder::new("{d} {h({l})} {M}::{L} - {m}{n}"));
|
||||
|
@@ -4,15 +4,15 @@
|
||||
use clap::Parser;
|
||||
use cli::Commands;
|
||||
|
||||
mod camera;
|
||||
mod cli;
|
||||
mod hardware;
|
||||
mod log;
|
||||
mod usb;
|
||||
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cli = cli::Cli::parse();
|
||||
|
||||
log::init(cli.quiet, cli.verbose)?;
|
||||
log::init(cli.verbose)?;
|
||||
|
||||
let device_id = cli.device.as_deref();
|
||||
|
||||
|
@@ -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.")),
|
||||
|
Reference in New Issue
Block a user