feat: custom PTP implementation

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2025-10-15 23:35:35 +01:00
parent 943f22c074
commit 1f26a91dcd
17 changed files with 743 additions and 173 deletions

12
Cargo.lock generated
View File

@@ -239,7 +239,6 @@ dependencies = [
"anyhow",
"byteorder",
"clap",
"libptp",
"log",
"log4rs",
"rusb",
@@ -339,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"

View File

@@ -17,7 +17,6 @@ codegen-units = 1
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"

13
src/camera/error.rs Normal file
View 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 {}

View File

@@ -1,33 +1,38 @@
pub mod devices;
pub mod error;
pub mod ptp;
use std::{error::Error, fmt, io::Cursor, time::Duration};
use std::{cmp::min, io::Cursor, time::Duration};
use anyhow::{anyhow, bail};
use byteorder::{LittleEndian, WriteBytesExt};
use devices::SupportedCamera;
use libptp::{DeviceInfo, StandardCommandCode};
use log::{debug, error};
use rusb::GlobalContext;
use serde::Serialize;
#[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 {}
use log::{debug, error, trace};
use ptp::{
enums::{CommandCode, ContainerType, PropCode, ResponseCode, UsbMode},
structs::{ContainerInfo, DeviceInfo},
};
use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE};
const SESSION: u32 = 1;
pub struct Camera {
pub struct Usb {
bus: u8,
address: u8,
ptp: libptp::Camera<GlobalContext>,
r#impl: Box<dyn CameraImpl<GlobalContext>>,
interface: u8,
}
pub struct Ptp {
bulk_in: u8,
bulk_out: u8,
handle: rusb::DeviceHandle<GlobalContext>,
transaction_id: u32,
}
pub struct Camera {
pub r#impl: Box<dyn CameraImpl<GlobalContext>>,
usb: Usb,
pub ptp: Ptp,
}
impl Camera {
@@ -36,24 +41,69 @@ impl Camera {
if let Ok(r#impl) = supported_camera.new_camera(device) {
let bus = device.bus_number();
let address = device.address();
let mut ptp = libptp::Camera::new(device)?;
let config_desc = device.active_config_descriptor()?;
let interface_descriptor = config_desc
.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 usb = Usb {
bus,
address,
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 mut ptp = Ptp {
bulk_in,
bulk_out,
handle,
transaction_id,
};
debug!("Opening session");
let () = r#impl.open_session(&mut ptp, SESSION)?;
debug!("Session opened");
return Ok(Self {
bus,
address,
ptp,
r#impl,
});
return Ok(Self { r#impl, usb, 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
}
@@ -67,7 +117,7 @@ impl Camera {
}
pub fn connected_usb_id(&self) -> String {
format!("{}.{}", self.bus, self.address)
format!("{}.{}", self.usb.bus, self.usb.address)
}
fn prop_value_as_scalar(data: &[u8]) -> anyhow::Result<u32> {
@@ -89,7 +139,7 @@ impl Camera {
pub fn get_usb_mode(&mut self) -> anyhow::Result<UsbMode> {
let data = self
.r#impl
.get_prop_value(&mut self.ptp, DevicePropCode::FujiUsbMode);
.get_prop_value(&mut self.ptp, PropCode::FujiUsbMode);
let result = Self::prop_value_as_scalar(&data?)?.into();
Ok(result)
@@ -98,7 +148,7 @@ impl Camera {
pub fn get_battery_info(&mut self) -> anyhow::Result<u32> {
let data = self
.r#impl
.get_prop_value(&mut self.ptp, DevicePropCode::FujiBatteryInfo2);
.get_prop_value(&mut self.ptp, PropCode::FujiBatteryInfo2);
let data = data?;
debug!("Raw battery data: {data:?}");
@@ -134,41 +184,15 @@ 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}")
error!("Error closing session: {e}");
}
debug!("Session closed");
}
}
#[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,
debug!("Releasing interface");
if let Err(e) = self.ptp.handle.release_interface(self.usb.interface) {
error!("Error releasing interface: {e}");
}
}
}
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}")
debug!("Interface released");
}
}
@@ -179,81 +203,219 @@ pub trait CameraImpl<P: rusb::UsbContext> {
None
}
fn open_session(&self, ptp: &mut libptp::Camera<P>, session_id: u32) -> anyhow::Result<()> {
fn chunk_size(&self) -> usize {
1024 * 1024
}
fn send(
&self,
ptp: &mut Ptp,
code: CommandCode,
params: Option<&[u32]>,
data: Option<&[u8]>,
transaction: bool,
) -> anyhow::Result<Vec<u8>> {
let transaction_id = if transaction {
Some(ptp.transaction_id)
} else {
None
};
let params = params.unwrap_or_default();
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(ptp, ContainerType::Command, code, &payload, transaction_id)?;
if let Some(data) = data {
trace!("Sending PTP data: {} bytes", data.len());
self.write(ptp, ContainerType::Data, code, data, transaction_id)?;
}
let mut data_payload = Vec::new();
loop {
let (container, payload) = self.read(ptp)?;
match container.kind {
ContainerType::Data => {
trace!("Data received: {} bytes", payload.len());
data_payload = payload;
}
ContainerType::Response => {
trace!("Response received: code {:?}", container.code);
let code = ResponseCode::try_from(container.code)?;
if code != ResponseCode::Ok {
bail!(ptp::error::Error::Response(container.code));
}
trace!(
"Command {:?} completed successfully with data payload of {} bytes",
code,
data_payload.len(),
);
return Ok(data_payload);
}
_ => {
debug!("Ignoring unexpected container type: {:?}", container.kind);
}
}
}
}
fn write(
&self,
ptp: &mut Ptp,
kind: ContainerType,
code: CommandCode,
payload: &[u8],
// Fuji, for the love of God don't ever write code again.
transaction_id: Option<u32>,
) -> anyhow::Result<()> {
// Look at what you made me do. Fuck.
let header_len = ContainerInfo::SIZE
- if transaction_id.is_none() {
size_of::<u32>()
} else {
0
};
let first_chunk_len = min(payload.len(), self.chunk_size() - header_len);
let total_len = u32::try_from(payload.len() + header_len)?;
let mut buffer = Vec::with_capacity(first_chunk_len + header_len);
buffer.write_u32::<LittleEndian>(total_len)?;
buffer.write_u16::<LittleEndian>(kind as u16)?;
buffer.write_u16::<LittleEndian>(code as u16)?;
if let Some(transaction_id) = transaction_id {
buffer.write_u32::<LittleEndian>(transaction_id)?;
}
buffer.extend_from_slice(&payload[..first_chunk_len]);
trace!(
"Writing PTP {kind:?} container, code: {code:?}, transaction: {transaction_id:?}, first_chunk: {first_chunk_len} bytes",
);
let timeout = self.timeout().unwrap_or_default();
ptp.handle.write_bulk(ptp.bulk_out, &buffer, timeout)?;
for chunk in payload[first_chunk_len..].chunks(self.chunk_size()) {
trace!("Writing additional chunk ({} bytes)", chunk.len(),);
ptp.handle.write_bulk(ptp.bulk_out, chunk, timeout)?;
}
trace!(
"Write completed for code {:?}, total payload of {} bytes",
code,
payload.len()
);
Ok(())
}
fn read(&self, ptp: &mut Ptp) -> anyhow::Result<(ContainerInfo, Vec<u8>)> {
let timeout = self.timeout().unwrap_or_default();
let mut stack_buf = [0u8; 8 * 1024];
let n = ptp.handle.read_bulk(ptp.bulk_in, &mut stack_buf, timeout)?;
let buf = &stack_buf[..n];
trace!("Read {n} bytes from bulk_in");
let container_info = ContainerInfo::parse(buf)?;
if container_info.payload_len == 0 {
trace!("No payload in container");
return Ok((container_info, Vec::new()));
}
let payload_len = container_info.payload_len as usize;
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 = ptp.handle.read_bulk(ptp.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))
}
fn open_session(&self, ptp: &mut Ptp, session_id: u32) -> anyhow::Result<()> {
debug!("Sending OpenSession command");
_ = ptp.command(
StandardCommandCode::OpenSession,
&[session_id],
_ = self.send(
ptp,
CommandCode::OpenSession,
Some(&[session_id]),
None,
self.timeout(),
true,
)?;
Ok(())
}
fn close_session(&self, ptp: &mut libptp::Camera<P>, _: u32) -> anyhow::Result<()> {
fn close_session(&self, ptp: &mut Ptp, _: u32) -> anyhow::Result<()> {
debug!("Sending CloseSession command");
let _ = ptp.command(StandardCommandCode::CloseSession, &[], None, self.timeout())?;
_ = self.send(ptp, CommandCode::CloseSession, None, None, true)?;
Ok(())
}
fn get_info(&self, ptp: &mut libptp::Camera<P>) -> anyhow::Result<DeviceInfo> {
fn get_info(&self, ptp: &mut Ptp) -> anyhow::Result<DeviceInfo> {
debug!("Sending GetDeviceInfo command");
let response = ptp.command(
StandardCommandCode::GetDeviceInfo,
&[],
None,
self.timeout(),
)?;
let response = self.send(ptp, CommandCode::GetDeviceInfo, None, None, true)?;
debug!("Received response with {} bytes", response.len());
let info = DeviceInfo::decode(&response)?;
let info = DeviceInfo::try_from(response.as_slice())?;
Ok(info)
}
fn get_prop_value(
&self,
ptp: &mut libptp::Camera<P>,
prop: DevicePropCode,
) -> anyhow::Result<Vec<u8>> {
fn get_prop_value(&self, ptp: &mut Ptp, prop: PropCode) -> anyhow::Result<Vec<u8>> {
debug!("Sending GetDevicePropValue command for property {prop:?}");
let response = ptp.command(
StandardCommandCode::GetDevicePropValue,
&[prop as u32],
let response = self.send(
ptp,
CommandCode::GetDevicePropValue,
Some(&[prop as u32]),
None,
self.timeout(),
true,
)?;
debug!("Received response with {} bytes", response.len());
Ok(response)
}
fn export_backup(&self, ptp: &mut libptp::Camera<P>) -> anyhow::Result<Vec<u8>> {
fn export_backup(&self, ptp: &mut Ptp) -> anyhow::Result<Vec<u8>> {
const HANDLE: u32 = 0x0;
debug!("Sending GetObjectInfo command for backup");
let response = ptp.command(
StandardCommandCode::GetObjectInfo,
&[HANDLE],
None,
self.timeout(),
)?;
let response = self.send(ptp, CommandCode::GetObjectInfo, Some(&[HANDLE]), None, true)?;
debug!("Received response with {} bytes", response.len());
debug!("Sending GetObject command for backup");
let response = ptp.command(
StandardCommandCode::GetObject,
&[HANDLE],
None,
self.timeout(),
)?;
let response = self.send(ptp, CommandCode::GetObject, Some(&[HANDLE]), None, true)?;
debug!("Received response with {} bytes", response.len());
Ok(response)
}
fn import_backup(&self, ptp: &mut libptp::Camera<P>, buffer: &[u8]) -> anyhow::Result<()> {
fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> {
debug!("Preparing ObjectInfo header for backup");
let mut obj_info = vec![0u8; 1012];
let mut obj_info = vec![0u8; 1088];
let mut cursor = Cursor::new(&mut obj_info[..]);
cursor.write_u32::<LittleEndian>(0x0)?;
cursor.write_u16::<LittleEndian>(0x5000)?;
@@ -261,20 +423,22 @@ pub trait CameraImpl<P: rusb::UsbContext> {
cursor.write_u32::<LittleEndian>(u32::try_from(buffer.len())?)?;
debug!("Sending SendObjectInfo command for backup");
let response = ptp.command(
libptp::StandardCommandCode::SendObjectInfo,
&[0x0, 0x0],
let response = self.send(
ptp,
CommandCode::SendObjectInfo,
Some(&[0x0, 0x0]),
Some(&obj_info),
self.timeout(),
true,
)?;
debug!("Received response with {} bytes", response.len());
debug!("Sending SendObject command for backup");
let response = ptp.command(
libptp::StandardCommandCode::SendObject,
&[0x0],
let response = self.send(
ptp,
CommandCode::SendObject,
Some(&[0x0]),
Some(buffer),
self.timeout(),
false,
)?;
debug!("Received response with {} bytes", response.len());

173
src/camera/ptp/enums.rs Normal file
View File

@@ -0,0 +1,173 @@
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}'"),
}
}
}
#[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}'"),
}
}
}
#[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
View 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),
}
}
}

4
src/camera/ptp/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod enums;
pub mod error;
pub mod read;
pub mod structs;

127
src/camera/ptp/read.rs Normal file
View 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(())
}
}

77
src/camera/ptp/structs.rs Normal file
View File

@@ -0,0 +1,77 @@
use std::io::Cursor;
use byteorder::{LittleEndian, ReadBytesExt};
use super::{enums::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 kind: ContainerType,
pub payload_len: u32,
pub code: u16,
}
impl ContainerInfo {
pub const SIZE: usize =
size_of::<ContainerType>() + size_of::<u32>() + size_of::<u16>() + size_of::<u32>();
}
impl ContainerInfo {
pub fn parse<R: ReadBytesExt>(mut r: R) -> anyhow::Result<Self> {
let payload_len = r.read_u32::<LittleEndian>()? - Self::SIZE as u32;
let kind = r.read_u16::<LittleEndian>()?;
let kind = ContainerType::try_from(kind)?;
let code = r.read_u16::<LittleEndian>()?;
let _transaction_id = r.read_u32::<LittleEndian>()?;
Ok(Self {
kind,
payload_len,
code,
})
}
}

View File

@@ -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 {}

View File

@@ -4,7 +4,7 @@ use clap::Subcommand;
use serde::Serialize;
use crate::{
camera::{Camera, UsbMode},
camera::{Camera, ptp::enums::UsbMode},
usb,
};
@@ -112,10 +112,10 @@ fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
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,
};

View File

@@ -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)]

View File

@@ -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")]

View File

@@ -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,

View File

@@ -6,13 +6,12 @@ use log4rs::{
encode::pattern::PatternEncoder,
};
pub fn init(quiet: bool, verbose: bool) -> anyhow::Result<()> {
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}"));

View File

@@ -4,15 +4,15 @@
use clap::Parser;
use cli::Commands;
mod cli;
mod camera;
mod cli;
mod log;
mod usb;
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();