Compare commits

...

12 Commits

Author SHA1 Message Date
4532fb1775 chore: refactor for device support
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-21 15:55:00 +03:00
7e8599fa61 chore: reorganize cli traits
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-19 21:25:11 +01:00
fb4610bdaa feat: color space, lens optimizer
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-19 19:50:45 +01:00
91d5d5b16b feat: monochromatic color tint/temp
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-19 19:31:23 +01:00
76ab55acd1 chore: partially demystify restore header
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-19 16:50:12 +01:00
e3e41999a6 chore: macros
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-19 12:57:47 +01:00
8120690caa feat: smooth skin effect
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-19 11:56:55 +01:00
6b0753b072 chore: refactor camera trait, optimize setter
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-18 23:46:14 +01:00
0f5997042c feat: custom option setter
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-18 17:05:18 +01:00
a1668bb277 feat: custom option getter
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-18 02:40:09 +01:00
7c43e0f7ab feat: backup restore
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-16 15:22:22 +01:00
4825b699a6 chore: clean up containerinfo
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-16 12:15:09 +01:00
30 changed files with 3494 additions and 879 deletions

140
Cargo.lock generated
View File

@@ -210,6 +210,17 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "erased-serde"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "259d404d09818dec19332e31d94558aeb442fea04c817006456c24b5460bbd4b"
dependencies = [
"serde",
"serde_core",
"typeid",
]
[[package]]
name = "errno"
version = "0.3.14"
@@ -239,11 +250,19 @@ dependencies = [
"anyhow",
"byteorder",
"clap",
"erased-serde",
"log",
"log4rs",
"num_enum",
"paste",
"ptp_cursor",
"ptp_macro",
"rusb",
"serde",
"serde_json",
"strsim",
"strum",
"strum_macros",
]
[[package]]
@@ -430,6 +449,28 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_enum"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a"
dependencies = [
"num_enum_derive",
"rustversion",
]
[[package]]
name = "num_enum_derive"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "once_cell"
version = "1.21.3"
@@ -474,6 +515,12 @@ dependencies = [
"windows-link",
]
[[package]]
name = "paste"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pkg-config"
version = "0.3.32"
@@ -489,6 +536,15 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro-crate"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.101"
@@ -498,6 +554,24 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "ptp_cursor"
version = "0.1.0"
dependencies = [
"byteorder",
]
[[package]]
name = "ptp_macro"
version = "0.1.0"
dependencies = [
"byteorder",
"proc-macro2",
"ptp_cursor",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.41"
@@ -676,6 +750,27 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "2.0.106"
@@ -727,6 +822,42 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "toml_datetime"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.23.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
dependencies = [
"winnow",
]
[[package]]
name = "typeid"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c"
[[package]]
name = "typemap-ors"
version = "1.0.0"
@@ -1095,6 +1226,15 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.46.0"

View File

@@ -19,6 +19,14 @@ byteorder = "1.5.0"
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
log = "0.4.28"
log4rs = "1.4.0"
num_enum = "0.7.4"
rusb = "0.9.4"
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"
strsim = "0.11.1"
ptp_macro = { path = "crates/ptp/macro" }
ptp_cursor = { path = "crates/ptp/cursor" }
strum = { version = "0.27.2", features = ["strum_macros"] }
strum_macros = "0.27.2"
paste = "1.0.15"
erased-serde = "0.4.8"

16
crates/ptp/cursor/Cargo.lock generated Normal file
View File

@@ -0,0 +1,16 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "ptp_cursor"
version = "0.1.0"
dependencies = [
"byteorder",
]

View File

@@ -0,0 +1,7 @@
[package]
name = "ptp_cursor"
version = "0.1.0"
edition = "2024"
[dependencies]
byteorder = { version = "1.5.0" }

View File

@@ -0,0 +1,286 @@
#![allow(dead_code)]
#![allow(clippy::redundant_closure_for_method_calls)]
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use std::io::{self, Cursor};
pub trait Read: ReadBytesExt {
fn read_ptp_u8(&mut self) -> io::Result<u8> {
self.read_u8()
}
fn read_ptp_i8(&mut self) -> io::Result<i8> {
self.read_i8()
}
fn read_ptp_u16(&mut self) -> io::Result<u16> {
self.read_u16::<LittleEndian>()
}
fn read_ptp_i16(&mut self) -> io::Result<i16> {
self.read_i16::<LittleEndian>()
}
fn read_ptp_u32(&mut self) -> io::Result<u32> {
self.read_u32::<LittleEndian>()
}
fn read_ptp_i32(&mut self) -> io::Result<i32> {
self.read_i32::<LittleEndian>()
}
fn read_ptp_u64(&mut self) -> io::Result<u64> {
self.read_u64::<LittleEndian>()
}
fn read_ptp_i64(&mut self) -> io::Result<i64> {
self.read_i64::<LittleEndian>()
}
fn read_ptp_vec<T, F>(&mut self, func: F) -> io::Result<Vec<T>>
where
F: Fn(&mut Self) -> io::Result<T>,
{
let len = self.read_u32::<LittleEndian>()? as usize;
(0..len).map(|_| func(self)).collect()
}
fn read_ptp_u8_vec(&mut self) -> io::Result<Vec<u8>> {
self.read_ptp_vec(|cur| cur.read_ptp_u8())
}
fn read_ptp_i8_vec(&mut self) -> io::Result<Vec<i8>> {
self.read_ptp_vec(|cur| cur.read_ptp_i8())
}
fn read_ptp_u16_vec(&mut self) -> io::Result<Vec<u16>> {
self.read_ptp_vec(|cur| cur.read_ptp_u16())
}
fn read_ptp_i16_vec(&mut self) -> io::Result<Vec<i16>> {
self.read_ptp_vec(|cur| cur.read_ptp_i16())
}
fn read_ptp_u32_vec(&mut self) -> io::Result<Vec<u32>> {
self.read_ptp_vec(|cur| cur.read_ptp_u32())
}
fn read_ptp_i32_vec(&mut self) -> io::Result<Vec<i32>> {
self.read_ptp_vec(|cur| cur.read_ptp_i32())
}
fn read_ptp_u64_vec(&mut self) -> io::Result<Vec<u64>> {
self.read_ptp_vec(|cur| cur.read_ptp_u64())
}
fn read_ptp_i64_vec(&mut self) -> io::Result<Vec<i64>> {
self.read_ptp_vec(|cur| cur.read_ptp_i64())
}
fn read_ptp_str(&mut self) -> io::Result<String> {
let len = self.read_u8()?;
if len == 0 {
return Ok(String::new());
}
let data: Vec<u16> = (0..(len - 1))
.map(|_| self.read_u16::<LittleEndian>())
.collect::<io::Result<_>>()?;
self.read_u16::<LittleEndian>()?;
String::from_utf16(&data)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16"))
}
fn expect_end(&mut self) -> io::Result<()>;
}
impl<T: AsRef<[u8]>> Read for Cursor<T> {
fn expect_end(&mut self) -> io::Result<()> {
let len = self.get_ref().as_ref().len();
if len as u64 != self.position() {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
format!(
"Buffer contained {} bytes, expected {} bytes",
len,
self.position()
),
));
}
Ok(())
}
}
pub trait Write: WriteBytesExt {
fn write_ptp_u8(&mut self, v: &u8) -> io::Result<()> {
self.write_u8(*v)
}
fn write_ptp_i8(&mut self, v: &i8) -> io::Result<()> {
self.write_i8(*v)
}
fn write_ptp_u16(&mut self, v: &u16) -> io::Result<()> {
self.write_u16::<LittleEndian>(*v)
}
fn write_ptp_i16(&mut self, v: &i16) -> io::Result<()> {
self.write_i16::<LittleEndian>(*v)
}
fn write_ptp_u32(&mut self, v: &u32) -> io::Result<()> {
self.write_u32::<LittleEndian>(*v)
}
fn write_ptp_i32(&mut self, v: &i32) -> io::Result<()> {
self.write_i32::<LittleEndian>(*v)
}
fn write_ptp_u64(&mut self, v: &u64) -> io::Result<()> {
self.write_u64::<LittleEndian>(*v)
}
fn write_ptp_i64(&mut self, v: &i64) -> io::Result<()> {
self.write_i64::<LittleEndian>(*v)
}
fn write_ptp_vec<T, F>(&mut self, vec: &[T], func: F) -> io::Result<()>
where
F: Fn(&mut Self, &T) -> io::Result<()>,
{
self.write_u32::<LittleEndian>(vec.len() as u32)?;
for v in vec {
func(self, v)?;
}
Ok(())
}
fn write_ptp_u8_vec(&mut self, vec: &[u8]) -> io::Result<()> {
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_u8(v))
}
fn write_ptp_i8_vec(&mut self, vec: &[i8]) -> io::Result<()> {
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_i8(v))
}
fn write_ptp_u16_vec(&mut self, vec: &[u16]) -> io::Result<()> {
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_u16(v))
}
fn write_ptp_i16_vec(&mut self, vec: &[i16]) -> io::Result<()> {
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_i16(v))
}
fn write_ptp_u32_vec(&mut self, vec: &[u32]) -> io::Result<()> {
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_u32(v))
}
fn write_ptp_i32_vec(&mut self, vec: &[i32]) -> io::Result<()> {
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_i32(v))
}
fn write_ptp_u64_vec(&mut self, vec: &[u64]) -> io::Result<()> {
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_u64(v))
}
fn write_ptp_i64_vec(&mut self, vec: &[i64]) -> io::Result<()> {
self.write_ptp_vec(vec, |cur, v| cur.write_ptp_i64(v))
}
fn write_ptp_str(&mut self, s: &str) -> io::Result<()> {
if s.is_empty() {
return self.write_u8(0);
}
let utf16: Vec<u16> = s.encode_utf16().collect();
self.write_u8((utf16.len() + 1) as u8)?;
for c in utf16 {
self.write_u16::<LittleEndian>(c)?;
}
self.write_u16::<LittleEndian>(0)?;
Ok(())
}
}
impl Write for Vec<u8> {}
pub trait PtpSerialize: Sized {
fn try_into_ptp(&self) -> io::Result<Vec<u8>>;
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()>;
}
pub trait PtpDeserialize: Sized {
fn try_from_ptp(buf: &[u8]) -> io::Result<Self>;
fn try_read_ptp<R: Read>(cur: &mut R) -> io::Result<Self>;
}
macro_rules! ptp_ser {
($ty:ty, $read_fn:ident, $write_fn:ident) => {
impl PtpSerialize for $ty {
fn try_into_ptp(&self) -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.try_write_ptp(&mut buf)?;
Ok(buf)
}
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()> {
buf.$write_fn(self)
}
}
};
}
macro_rules! ptp_de {
($ty:ty, $read_fn:ident, $write_fn:ident) => {
impl PtpDeserialize for $ty {
fn try_from_ptp(buf: &[u8]) -> io::Result<Self> {
let mut cur = Cursor::new(buf);
let val = Self::try_read_ptp(&mut cur)?;
cur.expect_end()?;
Ok(val)
}
fn try_read_ptp<R: Read>(cur: &mut R) -> io::Result<Self> {
cur.$read_fn()
}
}
};
}
ptp_ser!(u8, read_ptp_u8, write_ptp_u8);
ptp_de!(u8, read_ptp_u8, write_ptp_u8);
ptp_ser!(i8, read_ptp_i8, write_ptp_i8);
ptp_de!(i8, read_ptp_i8, write_ptp_i8);
ptp_ser!(u16, read_ptp_u16, write_ptp_u16);
ptp_de!(u16, read_ptp_u16, write_ptp_u16);
ptp_ser!(i16, read_ptp_i16, write_ptp_i16);
ptp_de!(i16, read_ptp_i16, write_ptp_i16);
ptp_ser!(u32, read_ptp_u32, write_ptp_u32);
ptp_de!(u32, read_ptp_u32, write_ptp_u32);
ptp_ser!(i32, read_ptp_i32, write_ptp_i32);
ptp_de!(i32, read_ptp_i32, write_ptp_i32);
ptp_ser!(u64, read_ptp_u64, write_ptp_u64);
ptp_de!(u64, read_ptp_u64, write_ptp_u64);
ptp_ser!(i64, read_ptp_i64, write_ptp_i64);
ptp_de!(i64, read_ptp_i64, write_ptp_i64);
ptp_ser!(&str, read_ptp_str, write_ptp_str);
ptp_ser!(String, read_ptp_str, write_ptp_str);
ptp_de!(String, read_ptp_str, write_ptp_str);
ptp_ser!(Vec<u8>, read_ptp_u8_vec, write_ptp_u8_vec);
ptp_de!(Vec<u8>, read_ptp_u8_vec, write_ptp_u8_vec);
ptp_ser!(Vec<i8>, read_ptp_i8_vec, write_ptp_i8_vec);
ptp_de!(Vec<i8>, read_ptp_i8_vec, write_ptp_i8_vec);
ptp_ser!(Vec<u16>, read_ptp_u16_vec, write_ptp_u16_vec);
ptp_de!(Vec<u16>, read_ptp_u16_vec, write_ptp_u16_vec);
ptp_ser!(Vec<i16>, read_ptp_i16_vec, write_ptp_i16_vec);
ptp_de!(Vec<i16>, read_ptp_i16_vec, write_ptp_i16_vec);
ptp_ser!(Vec<u32>, read_ptp_u32_vec, write_ptp_u32_vec);
ptp_de!(Vec<u32>, read_ptp_u32_vec, write_ptp_u32_vec);
ptp_ser!(Vec<i32>, read_ptp_i32_vec, write_ptp_i32_vec);
ptp_de!(Vec<i32>, read_ptp_i32_vec, write_ptp_i32_vec);
ptp_ser!(Vec<u64>, read_ptp_u64_vec, write_ptp_u64_vec);
ptp_de!(Vec<u64>, read_ptp_u64_vec, write_ptp_u64_vec);
ptp_ser!(Vec<i64>, read_ptp_i64_vec, write_ptp_i64_vec);
ptp_de!(Vec<i64>, read_ptp_i64_vec, write_ptp_i64_vec);

62
crates/ptp/macro/Cargo.lock generated Normal file
View File

@@ -0,0 +1,62 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "proc-macro2"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "ptp_cursor"
version = "0.1.0"
dependencies = [
"byteorder",
]
[[package]]
name = "ptp_macro"
version = "0.1.0"
dependencies = [
"byteorder",
"proc-macro2",
"ptp_cursor",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "syn"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"

View File

@@ -0,0 +1,14 @@
[package]
name = "ptp_macro"
version = "0.1.0"
edition = "2024"
[lib]
proc-macro = true
[dependencies]
byteorder = "1.5.0"
proc-macro2 = "1.0.101"
quote = "1.0.41"
syn = { version = "2.0.106", features = ["full"] }
ptp_cursor = { path = "../cursor" }

253
crates/ptp/macro/src/lib.rs Normal file
View File

@@ -0,0 +1,253 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Expr, Fields, parse_macro_input, punctuated::Punctuated};
#[proc_macro_derive(PtpSerialize)]
pub fn derive_ptp_serialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(named) => {
let fields = &named.named;
let write_fields = fields.iter().map(|f| {
let name = &f.ident;
quote! {
self.#name.try_write_ptp(buf)?;
}
});
quote! {
impl ptp_cursor::PtpSerialize for #name {
fn try_into_ptp(&self) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.try_write_ptp(&mut buf)?;
Ok(buf)
}
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> std::io::Result<()> {
#(#write_fields)*
Ok(())
}
}
}
}
Fields::Unnamed(unnamed) => {
let fields = &unnamed.unnamed;
let write_fields = (0..fields.len()).map(|i| {
let idx = syn::Index::from(i);
quote! { self.#idx.try_write_ptp(buf)?; }
});
quote! {
impl ptp_cursor::PtpSerialize for #name {
fn try_into_ptp(&self) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.try_write_ptp(&mut buf)?;
Ok(buf)
}
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> std::io::Result<()> {
#(#write_fields)*
Ok(())
}
}
}
}
Fields::Unit => {
quote! {
impl ptp_cursor::PtpSerialize for #name {
fn try_into_ptp(&self) -> std::io::Result<Vec<u8>> {
Ok(Vec::new())
}
fn try_write_ptp(&self, _buf: &mut Vec<u8>) -> std::io::Result<()> {
Ok(())
}
}
}
}
},
Data::Enum(_) => {
let repr_ty = input
.attrs
.iter()
.find_map(|attr| {
if attr.path().is_ident("repr") {
attr.parse_args_with(
Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty,
)
.ok()
.and_then(|args| args.into_iter().next())
.and_then(|expr| match expr {
Expr::Path(path) => path.path.get_ident().cloned(),
_ => None,
})
} else {
None
}
})
.expect("Enums must have a #[repr(T)] attribute for PtpSerialize");
quote! {
impl ptp_cursor::PtpSerialize for #name
where
#name: Clone + Copy + TryFrom<#repr_ty> + Into<#repr_ty>
{
fn try_into_ptp(&self) -> std::io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.try_write_ptp(&mut buf)?;
Ok(buf)
}
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> std::io::Result<()> {
let discriminant: #repr_ty = (*self).into();
discriminant.try_write_ptp(buf)?;
Ok(())
}
}
}
}
_ => {
unimplemented!("PtpSerialize cannot be automatically derived for unions")
}
};
expanded.into()
}
#[proc_macro_derive(PtpDeserialize)]
pub fn derive_ptp_deserialize(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let expanded = match &input.data {
Data::Struct(s) => match &s.fields {
Fields::Named(named) => {
let fields = &named.named;
let read_fields = fields.iter().map(|f| {
let name = &f.ident;
let ty = &f.ty;
quote! {
#name: <#ty>::try_read_ptp(cur)?
}
});
quote! {
impl ptp_cursor::PtpDeserialize for #name {
fn try_from_ptp(buf: &[u8]) -> std::io::Result<Self> {
use ptp_cursor::Read;
let mut cur = std::io::Cursor::new(buf);
let val = Self::try_read_ptp(&mut cur)?;
cur.expect_end()?;
Ok(val)
}
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> std::io::Result<Self> {
Ok(Self { #(#read_fields),* })
}
}
}
}
Fields::Unnamed(unnamed) => {
let fields = &unnamed.unnamed;
let read_fields = fields.iter().map(|f| {
let ty = &f.ty;
quote! { <#ty>::try_read_ptp(cur)? }
});
quote! {
impl ptp_cursor::PtpDeserialize for #name {
fn try_from_ptp(buf: &[u8]) -> std::io::Result<Self> {
use ptp_cursor::Read;
let mut cur = std::io::Cursor::new(buf);
let val = Self::try_read_ptp(&mut cur)?;
cur.expect_end()?;
Ok(val)
}
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> std::io::Result<Self> {
Ok(Self(#(#read_fields),*))
}
}
}
}
Fields::Unit => {
quote! {
impl ptp_cursor::PtpDeserialize for #name {
fn try_from_ptp(buf: &[u8]) -> std::io::Result<Self> {
use ptp_cursor::Read;
let mut cur = std::io::Cursor::new(buf);
cur.expect_end()?;
Ok(Self)
}
fn try_read_ptp<R: ptp_cursor::Read>(_cur: &mut R) -> std::io::Result<Self> {
Ok(Self)
}
}
}
}
},
Data::Enum(_) => {
let repr_ty = input
.attrs
.iter()
.find_map(|attr| {
if attr.path().is_ident("repr") {
attr.parse_args_with(
Punctuated::<Expr, syn::Token![,]>::parse_separated_nonempty,
)
.ok()
.and_then(|args| args.into_iter().next())
.and_then(|expr| match expr {
Expr::Path(path) => path.path.get_ident().cloned(),
_ => None,
})
} else {
None
}
})
.expect("Enums must have a #[repr(T)] attribute for PtpDeserialize");
quote! {
impl ptp_cursor::PtpDeserialize for #name
where
#name: Clone + Copy + TryFrom<#repr_ty> + Into<#repr_ty>
{
fn try_from_ptp(buf: &[u8]) -> std::io::Result<Self> {
use ptp_cursor::Read;
let mut cur = std::io::Cursor::new(buf);
let val = Self::try_read_ptp(&mut cur)?;
cur.expect_end()?;
Ok(val)
}
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> std::io::Result<Self> {
let discriminant = <#repr_ty>::try_read_ptp(cur)?;
discriminant.try_into().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("Invalid discriminant for {}: {:?}", stringify!(#name), discriminant)
)
})
}
}
}
}
_ => {
unimplemented!("PtpDeserialize cannot be automatically derived for unions")
}
};
expanded.into()
}

View File

@@ -59,6 +59,7 @@
clippy
cargo-udeps
cargo-outdated
cargo-expand
];
shellHook = ''

View File

@@ -1,7 +1,11 @@
use anyhow::bail;
use rusb::GlobalContext;
mod x_trans_v;
use super::CameraImpl;
use std::fmt;
use rusb::GlobalContext;
use serde::Serialize;
use super::{Camera, CameraImpl};
type ImplFactory<P> = fn() -> Box<dyn CameraImpl<P>>;
@@ -13,51 +17,34 @@ pub struct SupportedCamera<P: rusb::UsbContext> {
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()?;
pub const SUPPORTED: &[SupportedCamera<GlobalContext>] = &[x_trans_v::x_t5::FUJIFILM_XT5];
let matches =
descriptor.vendor_id() == self.vendor && descriptor.product_id() == self.product;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CameraInfoListItem {
pub name: &'static str,
pub usb_id: String,
pub vendor_id: String,
pub product_id: String,
}
if !matches {
bail!(
"Device with vendor {:04x} and product {:04x} does not match {}",
descriptor.vendor_id(),
descriptor.product_id(),
self.name
);
impl From<&Camera> for CameraInfoListItem {
fn from(camera: &Camera) -> Self {
Self {
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()),
}
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
}
}
};
impl fmt::Display for CameraInfoListItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} ({}:{}) (USB ID: {})",
self.name, self.vendor_id, self.product_id, self.usb_id
)
}
}
default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5");
pub const SUPPORTED: &[SupportedCamera<GlobalContext>] = &[FUJIFILM_XT5];

View File

@@ -0,0 +1,287 @@
use std::{
fmt,
io::{self, Write},
};
use log::error;
use ptp_cursor::PtpSerialize;
use serde::Serialize;
use crate::{
camera::ptp::{
hex::{
FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace,
FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority,
FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone,
FujiImageQuality, FujiImageSize, FujiLensModulationOptimizer,
FujiMonochromaticColorTemperature, FujiMonochromaticColorTint, FujiShadowTone,
FujiSharpness, FujiSmoothSkinEffect, FujiWhiteBalance, FujiWhiteBalanceShift,
FujiWhiteBalanceTemperature, ObjectFormat, UsbMode,
},
structs::ObjectInfo,
},
cli::common::film::FilmSimulationOptions,
};
pub mod x_t5;
// TODO: Assuming that all cameras using the same sensor also have the same feature set.
// This might not actually be the case.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CameraInfo {
pub manufacturer: String,
pub model: String,
pub device_version: String,
pub serial_number: String,
pub mode: UsbMode,
pub battery: u32,
}
impl fmt::Display for CameraInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Manufacturer: {}", self.manufacturer)?;
writeln!(f, "Model: {}", self.model)?;
writeln!(f, "Version: {}", self.device_version)?;
writeln!(f, "Serial Number: {}", self.serial_number)?;
writeln!(f, "Mode: {}", self.mode)?;
write!(f, "Battery: {}%", self.battery)
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SimulationListItem {
pub slot: FujiCustomSetting,
pub name: FujiCustomSettingName,
}
impl fmt::Display for SimulationListItem {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.slot, self.name)
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Simulation {
pub name: FujiCustomSettingName,
pub size: FujiImageSize,
pub quality: FujiImageQuality,
#[allow(clippy::struct_field_names)]
pub simulation: FujiFilmSimulation,
pub monochromatic_color_temperature: FujiMonochromaticColorTemperature,
pub monochromatic_color_tint: FujiMonochromaticColorTint,
pub highlight: FujiHighlightTone,
pub shadow: FujiShadowTone,
pub color: FujiColor,
pub sharpness: FujiSharpness,
pub clarity: FujiClarity,
pub noise_reduction: FujiHighISONR,
pub grain: FujiGrainEffect,
pub color_chrome_effect: FujiColorChromeEffect,
pub color_chrome_fx_blue: FujiColorChromeFXBlue,
pub smooth_skin_effect: FujiSmoothSkinEffect,
pub white_balance: FujiWhiteBalance,
pub white_balance_shift_red: FujiWhiteBalanceShift,
pub white_balance_shift_blue: FujiWhiteBalanceShift,
pub white_balance_temperature: FujiWhiteBalanceTemperature,
pub dynamic_range: FujiDynamicRange,
pub dynamic_range_priority: FujiDynamicRangePriority,
pub lens_modulation_optimizer: FujiLensModulationOptimizer,
pub color_space: FujiColorSpace,
}
impl fmt::Display for Simulation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Name: {}", self.name)?;
writeln!(f, "Size: {}", self.size)?;
writeln!(f, "Quality: {}", self.quality)?;
writeln!(f, "Simulation: {}", self.simulation)?;
match self.simulation {
FujiFilmSimulation::Monochrome
| FujiFilmSimulation::MonochromeYe
| FujiFilmSimulation::MonochromeR
| FujiFilmSimulation::MonochromeG
| FujiFilmSimulation::AcrosSTD
| FujiFilmSimulation::AcrosYe
| FujiFilmSimulation::AcrosR
| FujiFilmSimulation::AcrosG => {
writeln!(
f,
"Monochromatic Color Temperature: {}",
self.monochromatic_color_temperature
)?;
writeln!(
f,
"Monochromatic Color Tint: {}",
self.monochromatic_color_tint
)?;
}
_ => {}
}
if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
writeln!(f, "Highlights: {}", self.highlight)?;
writeln!(f, "Shadows: {}", self.shadow)?;
}
writeln!(f, "Color: {}", self.color)?;
writeln!(f, "Sharpness: {}", self.sharpness)?;
writeln!(f, "Clarity: {}", self.clarity)?;
writeln!(f, "Noise Reduction: {}", self.noise_reduction)?;
writeln!(f, "Grain: {}", self.grain)?;
writeln!(f, "Color Chrome Effect: {}", self.color_chrome_effect)?;
writeln!(f, "Color Chrome FX Blue: {}", self.color_chrome_fx_blue)?;
writeln!(f, "Smooth Skin Effect: {}", self.smooth_skin_effect)?;
writeln!(f, "White Balance: {}", self.white_balance)?;
writeln!(
f,
"White Balance Shift (R/B): {} / {}",
self.white_balance_shift_red, self.white_balance_shift_blue
)?;
if self.white_balance == FujiWhiteBalance::Temperature {
writeln!(
f,
"White Balance Temperature: {}K",
self.white_balance_temperature
)?;
}
if self.dynamic_range_priority == FujiDynamicRangePriority::Off {
writeln!(f, "Dynamic Range: {}", self.dynamic_range)?;
}
writeln!(f, "Dynamic Range Priority: {}", self.dynamic_range_priority)?;
writeln!(
f,
"Lens Modulation Optimizer: {}",
self.lens_modulation_optimizer
)?;
writeln!(f, "Color Space: {}", self.color_space)
}
}
pub struct FujiBackupObjectInfo {
compressed_size: u32,
}
impl FujiBackupObjectInfo {
pub fn new(buffer_len: usize) -> anyhow::Result<Self> {
Ok(Self {
compressed_size: u32::try_from(buffer_len)?,
})
}
}
impl PtpSerialize for FujiBackupObjectInfo {
fn try_into_ptp(&self) -> io::Result<Vec<u8>> {
let mut buf = Vec::new();
self.try_write_ptp(&mut buf)?;
Ok(buf)
}
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()> {
let object_info = ObjectInfo {
object_format: ObjectFormat::FujiBackup,
compressed_size: self.compressed_size,
..Default::default()
};
object_info.try_write_ptp(buf)?;
// TODO: What is this?
buf.write_all(&[0x0u8; 1020])?;
Ok(())
}
}
pub trait XTransV {
fn validate_monochromatic(
final_options: &FilmSimulationOptions,
prev_simulation: FujiFilmSimulation,
) -> Result<bool, anyhow::Error> {
let mut fail = true;
if !matches!(
prev_simulation,
FujiFilmSimulation::Monochrome
| FujiFilmSimulation::MonochromeYe
| FujiFilmSimulation::MonochromeR
| FujiFilmSimulation::MonochromeG
| FujiFilmSimulation::AcrosSTD
| FujiFilmSimulation::AcrosYe
| FujiFilmSimulation::AcrosR
| FujiFilmSimulation::AcrosG
) && (final_options.monochromatic_color_temperature.is_some()
|| final_options.monochromatic_color_tint.is_some())
{
if final_options.monochromatic_color_temperature.is_some() {
error!(
"A B&W film simulation is not selected, refusing to set monochromatic color temperature"
);
fail = false;
}
if final_options.monochromatic_color_tint.is_some() {
error!(
"A B&W film simulation is not selected, refusing to set monochromatic color tint"
);
fail = false;
}
}
Ok(fail)
}
fn validate_white_balance_temperature(
final_options: &FilmSimulationOptions,
prev_white_balance: FujiWhiteBalance,
) -> Result<bool, anyhow::Error> {
if prev_white_balance != FujiWhiteBalance::Temperature
&& final_options.white_balance_temperature.is_some()
{
error!("White Balance mode is not set to 'Temperature', refusing to set temperature");
Ok(false)
} else {
Ok(true)
}
}
fn validate_exposure(
final_options: &FilmSimulationOptions,
previous_dynamic_range_priority: FujiDynamicRangePriority,
) -> Result<bool, anyhow::Error> {
let mut fail = true;
if previous_dynamic_range_priority != FujiDynamicRangePriority::Off
&& (final_options.dynamic_range.is_some()
|| final_options.highlight.is_some()
|| final_options.shadow.is_some())
{
if final_options.dynamic_range.is_some() {
error!("Dynamic Range Priority is enabled, refusing to set dynamic range");
fail = false;
}
if final_options.highlight.is_some() {
error!("Dynamic Range Priority is enabled, refusing to set highlight tone");
fail = false;
}
if final_options.shadow.is_some() {
error!("Dynamic Range Priority is enabled, refusing to set shadow tone");
fail = false;
}
}
Ok(fail)
}
}

View File

@@ -0,0 +1,308 @@
use anyhow::{Ok, anyhow, bail};
use log::debug;
use ptp_cursor::{PtpDeserialize, PtpSerialize};
use rusb::GlobalContext;
use strum::IntoEnumIterator;
use crate::{
camera::{
CameraImpl, CameraResult, camera_prop_getter, camera_prop_setter, camera_set_prop_if_some,
devices::{SupportedCamera, x_trans_v::FujiBackupObjectInfo},
ptp::{
Ptp,
hex::{
CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect,
FujiColorChromeFXBlue, FujiColorSpace, FujiCustomSetting, FujiCustomSettingName,
FujiDynamicRange, FujiDynamicRangePriority, FujiFilmSimulation, FujiGrainEffect,
FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize,
FujiLensModulationOptimizer, FujiMonochromaticColorTemperature,
FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect,
FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode,
},
},
},
cli::{common::film::FilmSimulationOptions, simulation::SetFilmSimulationOptions},
};
use super::{CameraInfo, Simulation, SimulationListItem, XTransV};
pub const FUJIFILM_XT5: SupportedCamera<GlobalContext> = SupportedCamera {
name: "FUJIFILM XT-5",
vendor: 0x04cb,
product: 0x02fc,
impl_factory: || Box::new(FujifilmXT5 {}),
};
pub struct FujifilmXT5 {}
impl XTransV for FujifilmXT5 {}
impl FujifilmXT5 {
camera_prop_getter!(get_usb_mode: UsbMode => DevicePropCode::FujiUsbMode);
fn get_battery_info(&self, ptp: &mut Ptp) -> anyhow::Result<u32> {
let data = ptp.get_prop_value(DevicePropCode::FujiBatteryInfo2, self.timeout())?;
debug!("Raw battery data: {data:?}");
let raw_string = String::try_from_ptp(&data)?;
debug!("Decoded raw string: {raw_string}");
let percentage: u32 = raw_string
.split(',')
.next()
.ok_or_else(|| anyhow!("Failed to parse battery percentage"))?
.parse()?;
Ok(percentage)
}
camera_prop_getter!(get_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName);
camera_prop_getter!(get_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize);
camera_prop_getter!(get_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality);
camera_prop_getter!(get_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange);
camera_prop_getter!(get_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority);
camera_prop_getter!(get_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation);
camera_prop_getter!(get_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature);
camera_prop_getter!(get_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint);
camera_prop_getter!(get_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect);
camera_prop_getter!(get_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance);
camera_prop_getter!(get_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR);
camera_prop_getter!(get_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone);
camera_prop_getter!(get_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone);
camera_prop_getter!(get_color: FujiColor => DevicePropCode::FujiCustomSettingColor);
camera_prop_getter!(get_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness);
camera_prop_getter!(get_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity);
camera_prop_getter!(get_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed);
camera_prop_getter!(get_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue);
camera_prop_getter!(get_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature);
camera_prop_getter!(get_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect);
camera_prop_getter!(get_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue);
camera_prop_getter!(get_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect);
camera_prop_getter!(get_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer);
camera_prop_getter!(get_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace);
camera_prop_setter!(set_active_custom_setting: FujiCustomSetting => DevicePropCode::FujiCustomSetting);
camera_prop_setter!(set_custom_setting_name: FujiCustomSettingName => DevicePropCode::FujiCustomSettingName);
camera_prop_setter!(set_image_size: FujiImageSize => DevicePropCode::FujiCustomSettingImageSize);
camera_prop_setter!(set_image_quality: FujiImageQuality => DevicePropCode::FujiCustomSettingImageQuality);
camera_prop_setter!(set_dynamic_range: FujiDynamicRange => DevicePropCode::FujiCustomSettingDynamicRange);
camera_prop_setter!(set_dynamic_range_priority: FujiDynamicRangePriority => DevicePropCode::FujiCustomSettingDynamicRangePriority);
camera_prop_setter!(set_film_simulation: FujiFilmSimulation => DevicePropCode::FujiCustomSettingFilmSimulation);
camera_prop_setter!(set_monochromatic_color_temperature: FujiMonochromaticColorTemperature => DevicePropCode::FujiCustomSettingMonochromaticColorTemperature);
camera_prop_setter!(set_monochromatic_color_tint: FujiMonochromaticColorTint => DevicePropCode::FujiCustomSettingMonochromaticColorTint);
camera_prop_setter!(set_grain_effect: FujiGrainEffect => DevicePropCode::FujiCustomSettingGrainEffect);
camera_prop_setter!(set_white_balance: FujiWhiteBalance => DevicePropCode::FujiCustomSettingWhiteBalance);
camera_prop_setter!(set_high_iso_nr: FujiHighISONR => DevicePropCode::FujiCustomSettingHighISONR);
camera_prop_setter!(set_highlight_tone: FujiHighlightTone => DevicePropCode::FujiCustomSettingHighlightTone);
camera_prop_setter!(set_shadow_tone: FujiShadowTone => DevicePropCode::FujiCustomSettingShadowTone);
camera_prop_setter!(set_color: FujiColor => DevicePropCode::FujiCustomSettingColor);
camera_prop_setter!(set_sharpness: FujiSharpness => DevicePropCode::FujiCustomSettingSharpness);
camera_prop_setter!(set_clarity: FujiClarity => DevicePropCode::FujiCustomSettingClarity);
camera_prop_setter!(set_white_balance_shift_red: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftRed);
camera_prop_setter!(set_white_balance_shift_blue: FujiWhiteBalanceShift => DevicePropCode::FujiCustomSettingWhiteBalanceShiftBlue);
camera_prop_setter!(set_white_balance_temperature: FujiWhiteBalanceTemperature => DevicePropCode::FujiCustomSettingWhiteBalanceTemperature);
camera_prop_setter!(set_color_chrome_effect: FujiColorChromeEffect => DevicePropCode::FujiCustomSettingColorChromeEffect);
camera_prop_setter!(set_color_chrome_fx_blue: FujiColorChromeFXBlue => DevicePropCode::FujiCustomSettingColorChromeFXBlue);
camera_prop_setter!(set_smooth_skin_effect: FujiSmoothSkinEffect => DevicePropCode::FujiCustomSettingSmoothSkinEffect);
camera_prop_setter!(set_lens_modulation_optimizer: FujiLensModulationOptimizer => DevicePropCode::FujiCustomSettingLensModulationOptimizer);
camera_prop_setter!(set_color_space: FujiColorSpace => DevicePropCode::FujiCustomSettingColorSpace);
fn validate_simulation_set(
&self,
ptp: &mut Ptp,
options: &FilmSimulationOptions,
) -> Result<(), anyhow::Error> {
let prev_simulation = if let Some(simulation) = options.simulation {
simulation
} else {
self.get_film_simulation(ptp)?
};
let prev_white_balance = if let Some(white_balance) = options.white_balance {
white_balance
} else {
self.get_white_balance(ptp)?
};
let prev_dynamic_range_priority =
if let Some(dynamic_range_priority) = options.dynamic_range_priority {
dynamic_range_priority
} else {
self.get_dynamic_range_priority(ptp)?
};
if !Self::validate_monochromatic(options, prev_simulation)?
|| !Self::validate_white_balance_temperature(options, prev_white_balance)?
|| !Self::validate_exposure(options, prev_dynamic_range_priority)?
{
bail!("Incompatible options detected")
}
Ok(())
}
}
impl CameraImpl<GlobalContext> for FujifilmXT5 {
fn supported_camera(&self) -> &'static SupportedCamera<GlobalContext> {
&FUJIFILM_XT5
}
fn chunk_size(&self) -> usize {
16128 * 1024
}
fn info_get(&self, ptp: &mut Ptp) -> anyhow::Result<Box<dyn CameraResult>> {
let info = ptp.get_info(self.timeout())?;
let mode = self.get_usb_mode(ptp)?;
let battery = self.get_battery_info(ptp)?;
let repr = CameraInfo {
manufacturer: info.manufacturer.clone(),
model: info.model.clone(),
device_version: info.device_version.clone(),
serial_number: info.serial_number,
mode,
battery,
};
let repr = Box::new(repr);
Ok(repr)
}
fn simulation_list(&self, ptp: &mut Ptp) -> anyhow::Result<Vec<Box<dyn CameraResult>>> {
let mut slots = Vec::new();
for slot in FujiCustomSetting::iter() {
self.set_active_custom_setting(ptp, &slot)?;
let name = self.get_custom_setting_name(ptp)?;
let repr = SimulationListItem { slot, name };
let repr: Box<dyn CameraResult> = Box::new(repr);
slots.push(repr);
}
Ok(slots)
}
fn simulation_get(
&self,
ptp: &mut Ptp,
slot: FujiCustomSetting,
) -> anyhow::Result<Box<dyn CameraResult>> {
self.set_active_custom_setting(ptp, &slot)?;
let repr = Simulation {
name: self.get_custom_setting_name(ptp)?,
size: self.get_image_size(ptp)?,
quality: self.get_image_quality(ptp)?,
simulation: self.get_film_simulation(ptp)?,
monochromatic_color_temperature: self.get_monochromatic_color_temperature(ptp)?,
monochromatic_color_tint: self.get_monochromatic_color_tint(ptp)?,
highlight: self.get_highlight_tone(ptp)?,
shadow: self.get_shadow_tone(ptp)?,
color: self.get_color(ptp)?,
sharpness: self.get_sharpness(ptp)?,
clarity: self.get_clarity(ptp)?,
noise_reduction: self.get_high_iso_nr(ptp)?,
grain: self.get_grain_effect(ptp)?,
color_chrome_effect: self.get_color_chrome_effect(ptp)?,
color_chrome_fx_blue: self.get_color_chrome_fx_blue(ptp)?,
smooth_skin_effect: self.get_smooth_skin_effect(ptp)?,
white_balance: self.get_white_balance(ptp)?,
white_balance_shift_red: self.get_white_balance_shift_red(ptp)?,
white_balance_shift_blue: self.get_white_balance_shift_blue(ptp)?,
white_balance_temperature: self.get_white_balance_temperature(ptp)?,
dynamic_range: self.get_dynamic_range(ptp)?,
dynamic_range_priority: self.get_dynamic_range_priority(ptp)?,
lens_modulation_optimizer: self.get_lens_modulation_optimizer(ptp)?,
color_space: self.get_color_space(ptp)?,
};
let repr = Box::new(repr);
Ok(repr)
}
fn simulation_set(
&self,
ptp: &mut Ptp,
slot: FujiCustomSetting,
set_options: &SetFilmSimulationOptions,
options: &FilmSimulationOptions,
) -> anyhow::Result<()> {
self.set_active_custom_setting(ptp, &slot)?;
self.validate_simulation_set(ptp, options)?;
camera_set_prop_if_some!(self, ptp, set_options,
name => set_custom_setting_name,
);
camera_set_prop_if_some!(self, ptp, options,
size => set_image_size,
quality => set_image_quality,
simulation => set_film_simulation,
monochromatic_color_temperature => set_monochromatic_color_temperature,
monochromatic_color_tint => set_monochromatic_color_tint,
color => set_color,
sharpness => set_sharpness,
clarity => set_clarity,
noise_reduction => set_high_iso_nr,
grain => set_grain_effect,
color_chrome_effect => set_color_chrome_effect,
color_chrome_fx_blue => set_color_chrome_fx_blue,
smooth_skin_effect => set_smooth_skin_effect,
white_balance => set_white_balance,
white_balance_temperature => set_white_balance_temperature,
white_balance_shift_red => set_white_balance_shift_red,
white_balance_shift_blue => set_white_balance_shift_blue,
dynamic_range_priority => set_dynamic_range_priority,
dynamic_range => set_dynamic_range,
highlight => set_highlight_tone,
shadow => set_shadow_tone,
lens_modulation_optimizer => set_lens_modulation_optimizer,
color_space => set_color_space,
);
Ok(())
}
fn backup_export(&self, ptp: &mut Ptp) -> anyhow::Result<Vec<u8>> {
const HANDLE: u32 = 0x0;
debug!("Sending GetObjectInfo command for backup");
let response = ptp.send(CommandCode::GetObjectInfo, &[HANDLE], None, self.timeout())?;
debug!("Received response with {} bytes", response.len());
debug!("Sending GetObject command for backup");
let response = ptp.send(CommandCode::GetObject, &[HANDLE], None, self.timeout())?;
debug!("Received response with {} bytes", response.len());
Ok(response)
}
fn backup_import(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> {
debug!("Sending SendObjectInfo command for backup");
let object_info = FujiBackupObjectInfo::new(buffer.len())?;
let response = ptp.send(
CommandCode::SendObjectInfo,
&[0x0, 0x0],
Some(&object_info.try_into_ptp()?),
self.timeout(),
)?;
debug!("Received response with {} bytes", response.len());
debug!("Sending SendObject command for backup");
let response = ptp.send(
CommandCode::SendObject,
&[0x0],
Some(buffer),
self.timeout(),
)?;
debug!("Received response with {} bytes", response.len());
Ok(())
}
}

View File

@@ -2,108 +2,44 @@ pub mod devices;
pub mod error;
pub mod ptp;
use std::{cmp::min, io::Cursor, time::Duration};
use std::{fmt, time::Duration};
use anyhow::{anyhow, bail};
use byteorder::{LittleEndian, WriteBytesExt};
use devices::SupportedCamera;
use log::{debug, error, trace};
use ptp::{
enums::{CommandCode, ContainerType, PropCode, ResponseCode, UsbMode},
structs::{ContainerInfo, DeviceInfo},
use crate::{
cli::common::film::FilmSimulationOptions, cli::simulation::SetFilmSimulationOptions,
usb::find_endpoint,
};
use anyhow::bail;
use devices::SupportedCamera;
use erased_serde::serialize_trait_object;
use log::{debug, error};
use ptp::{Ptp, hex::FujiCustomSetting};
use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE};
const SESSION: u32 = 1;
pub struct Usb {
bus: u8,
address: u8,
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 {
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_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 { r#impl, usb, ptp });
}
macro_rules! camera_to_impl_with_ptp {
($name:ident -> $ret:ty) => {
pub fn $name(&mut self) -> $ret {
self.r#impl.$name(&mut self.ptp)
}
};
($name:ident($($arg:ident: $arg_ty:ty),*) -> $ret:ty) => {
pub fn $name(&mut self, $($arg: $arg_ty),*) -> $ret {
self.r#impl.$name(&mut self.ptp, $($arg),*)
}
};
}
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 trait CameraResult: fmt::Display + erased_serde::Serialize {}
impl<T: fmt::Display + serde::Serialize> CameraResult for T {}
serialize_trait_object!(CameraResult);
impl Camera {
pub fn name(&self) -> &'static str {
self.r#impl.supported_camera().name
}
@@ -117,331 +53,183 @@ impl Camera {
}
pub fn connected_usb_id(&self) -> String {
format!("{}.{}", self.usb.bus, self.usb.address)
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)
}
camera_to_impl_with_ptp!(info_get() -> anyhow::Result<Box<dyn CameraResult>>);
camera_to_impl_with_ptp!(backup_export -> anyhow::Result<Vec<u8>>);
camera_to_impl_with_ptp!(backup_import(buffer: &[u8]) -> anyhow::Result<()>);
camera_to_impl_with_ptp!(simulation_list -> anyhow::Result<Vec<Box<dyn CameraResult>>>);
camera_to_impl_with_ptp!(simulation_get(slot: FujiCustomSetting) -> anyhow::Result<Box<dyn CameraResult>>);
camera_to_impl_with_ptp!(simulation_set(slot: FujiCustomSetting, set_options: &SetFilmSimulationOptions, options: &FilmSimulationOptions) -> anyhow::Result<()>);
}
impl Drop for Camera {
fn drop(&mut self) {
debug!("Closing session");
if let Err(e) = self.r#impl.close_session(&mut self.ptp, SESSION) {
if let Err(e) = self.ptp.close_session(SESSION, self.r#impl.timeout()) {
error!("Error closing session: {e}");
}
debug!("Session closed");
}
}
debug!("Releasing interface");
if let Err(e) = self.ptp.handle.release_interface(self.usb.interface) {
error!("Error releasing interface: {e}");
impl TryFrom<&rusb::Device<GlobalContext>> for Camera {
type Error = anyhow::Error;
fn try_from(device: &rusb::Device<GlobalContext>) -> anyhow::Result<Self> {
let descriptor = device.device_descriptor()?;
let vendor = descriptor.vendor_id();
let product = descriptor.product_id();
for supported_camera in devices::SUPPORTED {
if vendor != supported_camera.vendor || product != supported_camera.product {
continue;
}
let r#impl = (supported_camera.impl_factory)();
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 = find_endpoint(
&interface_descriptor,
rusb::Direction::In,
rusb::TransferType::Bulk,
)?;
let bulk_out = 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 () = ptp.open_session(SESSION, r#impl.timeout())?;
debug!("Session opened");
return Ok(Self { r#impl, ptp });
}
debug!("Interface released");
bail!("Device not supported");
}
}
pub trait CameraImpl<P: rusb::UsbContext> {
fn supported_camera(&self) -> &'static SupportedCamera<P>;
fn timeout(&self) -> Option<Duration> {
None
fn timeout(&self) -> Duration {
Duration::default()
}
fn chunk_size(&self) -> usize {
// Conservative estimate. Could go up to 15.75 * 1024^2 on the X-T5 but only gained 200ms.
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 info_get(&self, _ptp: &mut Ptp) -> anyhow::Result<Box<dyn CameraResult>> {
bail!("This device does not support getting detailed info")
}
fn write(
fn backup_export(&self, _ptp: &mut Ptp) -> anyhow::Result<Vec<u8>> {
bail!("This device does not support exporting backups")
}
fn backup_import(&self, _ptp: &mut Ptp, _buffer: &[u8]) -> anyhow::Result<()> {
bail!("This device does not support importing backups")
}
fn simulation_list(&self, _ptp: &mut Ptp) -> anyhow::Result<Vec<Box<dyn CameraResult>>> {
bail!("This device does not support listing simulations")
}
fn simulation_get(
&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>,
_ptp: &mut Ptp,
_slot: FujiCustomSetting,
) -> anyhow::Result<Box<dyn CameraResult>> {
bail!("This device does not support getting simulation options")
}
fn simulation_set(
&self,
_ptp: &mut Ptp,
_slot: FujiCustomSetting,
_set_options: &SetFilmSimulationOptions,
_options: &FilmSimulationOptions,
) -> 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");
_ = self.send(
ptp,
CommandCode::OpenSession,
Some(&[session_id]),
None,
true,
)?;
Ok(())
}
fn close_session(&self, ptp: &mut Ptp, _: u32) -> anyhow::Result<()> {
debug!("Sending CloseSession command");
_ = self.send(ptp, CommandCode::CloseSession, None, None, true)?;
Ok(())
}
fn get_info(&self, ptp: &mut Ptp) -> anyhow::Result<DeviceInfo> {
debug!("Sending GetDeviceInfo command");
let response = self.send(ptp, CommandCode::GetDeviceInfo, None, None, true)?;
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 = self.send(
ptp,
CommandCode::GetDevicePropValue,
Some(&[prop as u32]),
None,
true,
)?;
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 = self.send(ptp, CommandCode::GetObjectInfo, Some(&[HANDLE]), None, true)?;
debug!("Received response with {} bytes", response.len());
debug!("Sending GetObject command for backup");
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 Ptp, buffer: &[u8]) -> anyhow::Result<()> {
debug!("Preparing ObjectInfo header for backup");
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)?;
cursor.write_u16::<LittleEndian>(0x0)?;
cursor.write_u32::<LittleEndian>(u32::try_from(buffer.len())?)?;
debug!("Sending SendObjectInfo command for backup");
let response = self.send(
ptp,
CommandCode::SendObjectInfo,
Some(&[0x0, 0x0]),
Some(&obj_info),
true,
)?;
debug!("Received response with {} bytes", response.len());
debug!("Sending SendObject command for backup");
let response = self.send(
ptp,
CommandCode::SendObject,
Some(&[0x0]),
Some(buffer),
false,
)?;
debug!("Received response with {} bytes", response.len());
Ok(())
bail!("This device does not support setting simulation options")
}
}
macro_rules! camera_prop_getter {
($name:ident: $type:ty => $code:expr) => {
pub fn $name(&self, ptp: &mut crate::camera::ptp::Ptp) -> anyhow::Result<$type> {
use ptp_cursor::PtpDeserialize;
let bytes = ptp.get_prop_value($code, self.timeout())?;
let result = <$type>::try_from_ptp(&bytes)?;
Ok(result)
}
};
}
macro_rules! camera_prop_setter {
($name:ident: $type:ty => $code:expr) => {
pub fn $name(
&self,
ptp: &mut crate::camera::ptp::Ptp,
value: &$type,
) -> anyhow::Result<()> {
use ptp_cursor::PtpSerialize;
let bytes = value.try_into_ptp()?;
ptp.set_prop_value($code, &bytes, self.timeout())?;
Ok(())
}
};
}
macro_rules! camera_set_prop_if_some {
($self:ident, $ptp:ident, $options:ident,
$( $field:ident => $setter:ident ),* $(,)? ) => {
$(
if let Some(val) = &$options.$field {
$self.$setter($ptp, val)?;
}
)*
};
}
pub(crate) use camera_prop_getter;
pub(crate) use camera_prop_setter;
pub(crate) use camera_set_prop_if_some;

View File

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

View File

@@ -1,6 +1,6 @@
use std::{fmt, io};
use crate::camera::ptp::enums::ResponseCode;
use crate::camera::ptp::hex::ResponseCode;
#[derive(Debug)]
pub enum Error {

622
src/camera/ptp/hex.rs Normal file
View File

@@ -0,0 +1,622 @@
use std::{
io::{self, Cursor},
ops::{Deref, DerefMut},
};
use anyhow::bail;
use num_enum::{IntoPrimitive, TryFromPrimitive};
use ptp_cursor::{PtpDeserialize, PtpSerialize, Read};
use ptp_macro::{PtpDeserialize, PtpSerialize};
use serde::Serialize;
use strum_macros::EnumIter;
#[repr(u16)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
)]
pub enum CommandCode {
GetDeviceInfo = 0x1001,
OpenSession = 0x1002,
CloseSession = 0x1003,
GetObjectInfo = 0x1008,
GetObject = 0x1009,
SendObjectInfo = 0x100C,
SendObject = 0x100D,
GetDevicePropValue = 0x1015,
SetDevicePropValue = 0x1016,
FujiSendObjectInfo = 0x900c,
FujiSendObject = 0x900d,
}
#[repr(u16)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
)]
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,
}
#[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.into(),
ContainerCode::Response(resp) => resp.into(),
}
}
}
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?}'");
}
}
impl PtpSerialize for ContainerCode {
fn try_into_ptp(&self) -> io::Result<Vec<u8>> {
let value: u16 = (*self).into();
value.try_into_ptp()
}
fn try_write_ptp(&self, buf: &mut Vec<u8>) -> io::Result<()> {
let value: u16 = (*self).into();
value.try_write_ptp(buf)
}
}
impl PtpDeserialize for ContainerCode {
fn try_from_ptp(buf: &[u8]) -> io::Result<Self> {
let mut cur = Cursor::new(buf);
let value = Self::try_read_ptp(&mut cur)?;
cur.expect_end()?;
io::Result::Ok(value)
}
fn try_read_ptp<R: ptp_cursor::Read>(cur: &mut R) -> io::Result<Self> {
let value = <u16>::try_read_ptp(cur)?;
Self::try_from(value)
.map_err(|e: anyhow::Error| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
#[repr(u32)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
)]
pub enum DevicePropCode {
FujiUsbMode = 0xd16e,
FujiRawConversionRun = 0xD183,
FujiRawConversionProfile = 0xD185,
FujiCustomSetting = 0xD18C,
FujiCustomSettingName = 0xD18D,
FujiCustomSettingImageSize = 0xD18E,
FujiCustomSettingImageQuality = 0xD18F,
FujiCustomSettingDynamicRange = 0xD190,
FujiCustomSettingDynamicRangePriority = 0xD191,
FujiCustomSettingFilmSimulation = 0xD192,
FujiCustomSettingMonochromaticColorTemperature = 0xD193,
FujiCustomSettingMonochromaticColorTint = 0xD194,
FujiCustomSettingGrainEffect = 0xD195,
FujiCustomSettingColorChromeEffect = 0xD196,
FujiCustomSettingColorChromeFXBlue = 0xD197,
FujiCustomSettingSmoothSkinEffect = 0xD198,
FujiCustomSettingWhiteBalance = 0xD199,
FujiCustomSettingWhiteBalanceShiftRed = 0xD19A,
FujiCustomSettingWhiteBalanceShiftBlue = 0xD19B,
FujiCustomSettingWhiteBalanceTemperature = 0xD19C,
FujiCustomSettingHighlightTone = 0xD19D,
FujiCustomSettingShadowTone = 0xD19E,
FujiCustomSettingColor = 0xD19F,
FujiCustomSettingSharpness = 0xD1A0,
FujiCustomSettingHighISONR = 0xD1A1,
FujiCustomSettingClarity = 0xD1A2,
FujiCustomSettingLensModulationOptimizer = 0xD1A3,
FujiCustomSettingColorSpace = 0xD1A4,
// TODO: 0xD1A5 All 7s
FujiBatteryInfo2 = 0xD36B,
}
#[repr(u16)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
)]
pub enum ObjectFormat {
None = 0x0,
FujiBackup = 0x5000,
FujiRAF = 0xf802,
}
impl Default for ObjectFormat {
fn default() -> Self {
Self::None
}
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiCustomSetting {
C1 = 0x1,
C2 = 0x2,
C3 = 0x3,
C4 = 0x4,
C5 = 0x5,
C6 = 0x6,
C7 = 0x7,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, PtpSerialize, PtpDeserialize)]
pub struct FujiCustomSettingName(String);
impl FujiCustomSettingName {
pub const MAX_LEN: usize = 25;
pub const unsafe fn new_unchecked(value: String) -> Self {
Self(value)
}
}
impl Deref for FujiCustomSettingName {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FujiCustomSettingName {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl TryFrom<String> for FujiCustomSettingName {
type Error = anyhow::Error;
fn try_from(value: String) -> anyhow::Result<Self> {
if value.len() > Self::MAX_LEN {
bail!("Value '{}' exceeds max length of {}", value, Self::MAX_LEN);
}
Ok(Self(value))
}
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiImageSize {
R7728x5152 = 0x7,
R7728x4344 = 0x8,
R5152x5152 = 0x9,
R6864x5152 = 0xe,
R6432x5152 = 0x10,
R5472x3648 = 0x4,
R5472x3080 = 0x5,
R3648x3648 = 0x6,
R4864x3648 = 0x12,
R4560x3648 = 0x14,
R3888x2592 = 0x1,
R3888x2184 = 0x2,
R2592x2592 = 0x3,
R3456x2592 = 0xa,
R3264x2592 = 0xc,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiImageQuality {
FineRaw = 0x4,
Fine = 0x2,
NormalRaw = 0x5,
Normal = 0x3,
Raw = 0x1,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiDynamicRange {
Auto = 0xffff,
HDR100 = 0x64,
HDR200 = 0xc8,
HDR400 = 0x190,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiDynamicRangePriority {
Auto = 0x8000,
Strong = 0x2,
Weak = 0x1,
Off = 0x0,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiFilmSimulation {
Provia = 0x1,
Velvia = 0x2,
Astia = 0x3,
PRONegHi = 0x4,
PRONegStd = 0x5,
Monochrome = 0x6,
MonochromeYe = 0x7,
MonochromeR = 0x8,
MonochromeG = 0x9,
Sepia = 0xa,
ClassicChrome = 0xb,
AcrosSTD = 0xc,
AcrosYe = 0xd,
AcrosR = 0xe,
AcrosG = 0xf,
Eterna = 0x10,
ClassicNegative = 0x11,
NostalgicNegative = 0x13,
EternaBleachBypass = 0x12,
RealaAce = 0x14,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiGrainEffect {
StrongLarge = 0x5,
WeakLarge = 0x4,
StrongSmall = 0x3,
WeakSmall = 0x2,
Off = 0x6,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiColorChromeEffect {
Strong = 0x3,
Weak = 0x2,
Off = 0x1,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiColorChromeFXBlue {
Strong = 0x3,
Weak = 0x2,
Off = 0x1,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiSmoothSkinEffect {
Strong = 0x3,
Weak = 0x2,
Off = 0x1,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiWhiteBalance {
AsShot = 0x1,
WhitePriority = 0x8020,
Auto = 0x2,
AmbiencePriority = 0x8021,
Custom1 = 0x8008,
Custom2 = 0x8009,
Custom3 = 0x800A,
Temperature = 0x8007,
Daylight = 0x4,
Shade = 0x8006,
Fluorescent1 = 0x8001,
Fluorescent2 = 0x8002,
Fluorescent3 = 0x8003,
Incandescent = 0x6,
Underwater = 0x8,
}
#[repr(u16)]
#[derive(
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
)]
pub enum FujiHighISONR {
Plus4 = 0x5000,
Plus3 = 0x6000,
Plus2 = 0x0,
Plus1 = 0x1000,
Zero = 0x2000,
Minus1 = 0x3000,
Minus2 = 0x4000,
Minus3 = 0x7000,
Minus4 = 0x8000,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
pub enum FujiLensModulationOptimizer {
Off = 0x2,
On = 0x1,
}
#[repr(u16)]
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Serialize,
IntoPrimitive,
TryFromPrimitive,
PtpSerialize,
PtpDeserialize,
EnumIter,
)]
#[allow(clippy::upper_case_acronyms)]
pub enum FujiColorSpace {
SRGB = 0x2,
AdobeRGB = 0x1,
}
macro_rules! fuji_i16 {
($name:ident, $min:expr, $max:expr, $step:expr, $scale:literal) => {
#[derive(Debug, Clone, Copy, PartialEq, Eq, PtpSerialize, PtpDeserialize)]
pub struct $name(i16);
impl $name {
pub const MIN: f32 = $min;
pub const MAX: f32 = $max;
pub const STEP: f32 = $step;
pub const SCALE: f32 = $scale as f32;
#[allow(clippy::cast_possible_truncation)]
pub const RAW_MIN: i16 = ($min * $scale as f32) as i16;
#[allow(clippy::cast_possible_truncation)]
pub const RAW_MAX: i16 = ($max * $scale as f32) as i16;
#[allow(clippy::cast_possible_truncation)]
pub const RAW_STEP: i16 = ($step * $scale as f32) as i16;
pub const unsafe fn new_unchecked(value: i16) -> Self {
Self(value)
}
}
impl std::ops::Deref for $name {
type Target = i16;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for $name {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::convert::TryFrom<i16> for $name {
type Error = anyhow::Error;
fn try_from(value: i16) -> anyhow::Result<Self> {
if !(Self::RAW_MIN..=Self::RAW_MAX).contains(&value) {
anyhow::bail!("Value {} is out of range", value);
}
#[allow(clippy::modulo_one)]
if (value - Self::RAW_MIN) % Self::RAW_STEP != 0 {
anyhow::bail!("Value {} is not aligned to step {}", value, Self::RAW_STEP);
}
Ok(Self(value))
}
}
};
}
fuji_i16!(FujiMonochromaticColorTemperature, -18.0, 18.0, 1.0, 10i16);
fuji_i16!(FujiMonochromaticColorTint, -18.0, 18.0, 1.0, 10i16);
fuji_i16!(FujiWhiteBalanceShift, -9.0, 9.0, 1.0, 1i16);
fuji_i16!(FujiWhiteBalanceTemperature, 2500.0, 10000.0, 10.0, 1i16);
fuji_i16!(FujiHighlightTone, -2.0, 4.0, 0.5, 10i16);
fuji_i16!(FujiShadowTone, -2.0, 4.0, 0.5, 10i16);
fuji_i16!(FujiColor, -4.0, 4.0, 1.0, 10i16);
fuji_i16!(FujiSharpness, -4.0, 4.0, 1.0, 10i16);
fuji_i16!(FujiClarity, -5.0, 5.0, 1.0, 10i16);
#[repr(u16)]
#[derive(
Debug, Clone, Copy, Serialize, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpDeserialize,
)]
pub enum UsbMode {
RawConversion = 0x6,
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize,
)]
#[repr(u16)]
pub enum ContainerType {
Command = 1,
Data = 2,
Response = 3,
Event = 4,
}

View File

@@ -1,4 +1,248 @@
pub mod enums;
pub mod error;
pub mod read;
pub mod hex;
pub mod structs;
use std::{cmp::min, io::Cursor, time::Duration};
use anyhow::bail;
use hex::{CommandCode, ContainerCode, ContainerType, DevicePropCode, ResponseCode};
use log::{debug, error, trace, warn};
use ptp_cursor::{PtpDeserialize, PtpSerialize};
use rusb::GlobalContext;
use structs::{ContainerInfo, DeviceInfo};
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: &[u32],
data: Option<&[u8]>,
timeout: Duration,
) -> anyhow::Result<Vec<u8>> {
let transaction_id = self.transaction_id;
self.send_header(code, params, transaction_id, timeout)?;
if let Some(data) = data {
self.write(ContainerType::Data, code, data, transaction_id, timeout)?;
}
let response = self.receive_response(timeout);
self.transaction_id += 1;
response
}
pub fn open_session(&mut self, session_id: u32, timeout: Duration) -> anyhow::Result<()> {
debug!("Sending OpenSession command");
self.send(CommandCode::OpenSession, &[session_id], None, timeout)?;
Ok(())
}
pub fn close_session(&mut self, _: u32, timeout: Duration) -> anyhow::Result<()> {
debug!("Sending CloseSession command");
self.send(CommandCode::CloseSession, &[], None, timeout)?;
Ok(())
}
pub fn get_info(&mut self, timeout: Duration) -> anyhow::Result<DeviceInfo> {
debug!("Sending GetDeviceInfo command");
let response = self.send(CommandCode::GetDeviceInfo, &[], None, timeout)?;
debug!("Received response with {} bytes", response.len());
let info = DeviceInfo::try_from_ptp(&response)?;
Ok(info)
}
pub fn get_prop_value(
&mut self,
prop: DevicePropCode,
timeout: Duration,
) -> anyhow::Result<Vec<u8>> {
debug!("Sending GetDevicePropValue command for property {prop:?}");
let response = self.send(
CommandCode::GetDevicePropValue,
&[prop.into()],
None,
timeout,
)?;
debug!("Received response with {} bytes", response.len());
Ok(response)
}
pub fn set_prop_value(
&mut self,
prop: DevicePropCode,
value: &[u8],
timeout: Duration,
) -> anyhow::Result<Vec<u8>> {
debug!("Sending GetDevicePropValue command for property {prop:?}");
let response = self.send(
CommandCode::SetDevicePropValue,
&[prop.into()],
Some(value),
timeout,
)?;
debug!("Received response with {} bytes", response.len());
Ok(response)
}
fn send_header(
&self,
code: CommandCode,
params: &[u32],
transaction_id: u32,
timeout: Duration,
) -> anyhow::Result<()> {
let mut payload = Vec::with_capacity(params.len() * 4);
for p in params {
p.try_write_ptp(&mut payload)?;
}
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);
if self.transaction_id != container.transaction_id {
warn!(
"Mismatched transaction_id {}, expecting {}",
container.transaction_id, self.transaction_id
);
}
match container.code {
ContainerCode::Command(_) | ContainerCode::Response(ResponseCode::Ok) => {}
ContainerCode::Response(code) => {
bail!(error::Error::Response(code.into()));
}
}
trace!(
"Command completed successfully, response payload of {} bytes",
response.len(),
);
return Ok(response);
}
_ => {
warn!("Unexpected container type: {:?}", container.kind);
}
}
}
}
fn write(
&self,
kind: ContainerType,
code: CommandCode,
payload: &[u8],
transaction_id: 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_ptp()?;
let first_chunk_len = min(payload.len(), self.chunk_size - ContainerInfo::SIZE);
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 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 mut cur = Cursor::new(buf);
let container_info = ContainerInfo::try_read_ptp(&mut cur)?;
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");
}
}

View File

@@ -1,127 +0,0 @@
#![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(())
}
}

View File

@@ -1,11 +1,8 @@
use std::io::Cursor;
use ptp_macro::{PtpDeserialize, PtpSerialize};
use byteorder::{LittleEndian, ReadBytesExt};
use super::hex::{CommandCode, ContainerCode, ContainerType, ObjectFormat};
use super::{enums::ContainerType, read::Read};
#[allow(dead_code)]
#[derive(Debug)]
#[derive(Debug, PtpSerialize, PtpDeserialize)]
pub struct DeviceInfo {
pub version: u16,
pub vendor_ex_id: u32,
@@ -23,55 +20,59 @@ pub struct DeviceInfo {
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)]
#[derive(Debug, Clone, Copy, PtpSerialize, PtpDeserialize)]
pub struct ContainerInfo {
pub total_len: u32,
pub kind: ContainerType,
pub payload_len: u32,
pub code: u16,
pub code: ContainerCode,
pub transaction_id: u32,
}
impl ContainerInfo {
pub const SIZE: usize =
size_of::<ContainerType>() + size_of::<u32>() + size_of::<u16>() + size_of::<u32>();
}
size_of::<u32>() + size_of::<u16>() + 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>()?;
pub fn new(
kind: ContainerType,
code: CommandCode,
transaction_id: u32,
payload_len: usize,
) -> anyhow::Result<Self> {
let total_len = u32::try_from(Self::SIZE + payload_len)?;
let code = ContainerCode::Command(code);
Ok(Self {
total_len,
kind,
payload_len,
code,
transaction_id,
})
}
pub const fn payload_len(&self) -> usize {
self.total_len as usize - Self::SIZE
}
}
#[derive(Debug, Clone, Default, PtpSerialize, PtpDeserialize)]
pub struct ObjectInfo {
pub storage_id: u32,
pub object_format: ObjectFormat,
pub protection_status: u16,
pub compressed_size: u32,
pub thumb_format: u16,
pub thumb_compressed_size: u32,
pub thumb_width: u32,
pub thumb_height: u32,
pub image_width: u32,
pub image_height: u32,
pub image_bit_depth: u32,
pub parent_object: u32,
pub association_type: u16,
pub association_desc: u32,
pub sequence_number: u32,
pub filename: String,
pub date_created: String,
pub date_modified: String,
pub keywords: String,
}

View File

@@ -24,7 +24,7 @@ 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()?;
let backup = camera.backup_export()?;
writer.write_all(&backup)?;
Ok(())
@@ -36,7 +36,7 @@ fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> {
let mut reader = input.get_reader()?;
let mut backup = Vec::new();
reader.read_to_end(&mut backup)?;
camera.import_backup(&backup)?;
camera.backup_import(&backup)?;
Ok(())
}

View File

@@ -8,6 +8,7 @@ pub enum Input {
impl FromStr for Input {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" {
Ok(Self::Stdin)
@@ -34,6 +35,7 @@ pub enum Output {
impl FromStr for Output {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" {
Ok(Self::Stdout)

View File

@@ -1,4 +1,831 @@
use std::{fmt, ops::Deref, str::FromStr};
use anyhow::{Context, bail};
use clap::Args;
use serde::{Serialize, Serializer};
use strum::IntoEnumIterator;
use crate::{
camera::ptp::hex::{
FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiColorSpace,
FujiCustomSetting, FujiCustomSettingName, FujiDynamicRange, FujiDynamicRangePriority,
FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality,
FujiImageSize, FujiLensModulationOptimizer, FujiMonochromaticColorTemperature,
FujiMonochromaticColorTint, FujiShadowTone, FujiSharpness, FujiSmoothSkinEffect,
FujiWhiteBalance, FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode,
},
cli::common::suggest::get_closest,
};
#[derive(Args, Debug)]
pub struct FilmSimulationOptions {}
pub struct FilmSimulationOptions {
/// Fujifilm Film Simulation
#[clap(long)]
pub simulation: Option<FujiFilmSimulation>,
/// Monochromatic Color Temperature (only applicable to B&W film simulations)
#[clap(long)]
pub monochromatic_color_temperature: Option<FujiMonochromaticColorTemperature>,
/// Monochromatic Color Tint (only applicable to B&W film simulations)
#[clap(long)]
pub monochromatic_color_tint: Option<FujiMonochromaticColorTint>,
/// The output image resolution
#[clap(long)]
pub size: Option<FujiImageSize>,
/// The output image quality (JPEG compression level)
#[clap(long)]
pub quality: Option<FujiImageQuality>,
/// Highlight Tone
#[clap(long, allow_hyphen_values(true))]
pub highlight: Option<FujiHighlightTone>,
/// Shadow Tone
#[clap(long, allow_hyphen_values(true))]
pub shadow: Option<FujiShadowTone>,
/// Color
#[clap(long, allow_hyphen_values(true))]
pub color: Option<FujiColor>,
/// Sharpness
#[clap(long, allow_hyphen_values(true))]
pub sharpness: Option<FujiSharpness>,
/// Clarity
#[clap(long, allow_hyphen_values(true))]
pub clarity: Option<FujiClarity>,
/// White Balance
#[clap(long)]
pub white_balance: Option<FujiWhiteBalance>,
/// White Balance Shift Red
#[clap(long, allow_hyphen_values(true))]
pub white_balance_shift_red: Option<FujiWhiteBalanceShift>,
/// White Balance Shift Blue
#[clap(long, allow_hyphen_values(true))]
pub white_balance_shift_blue: Option<FujiWhiteBalanceShift>,
/// White Balance Temperature (Only used if WB is set to 'Temperature')
#[clap(long)]
pub white_balance_temperature: Option<FujiWhiteBalanceTemperature>,
/// Dynamic Range
#[clap(long)]
pub dynamic_range: Option<FujiDynamicRange>,
/// Dynamic Range Priority
#[clap(long)]
pub dynamic_range_priority: Option<FujiDynamicRangePriority>,
/// High ISO Noise Reduction
#[clap(long, allow_hyphen_values(true))]
pub noise_reduction: Option<FujiHighISONR>,
/// Grain Effect
#[clap(long)]
pub grain: Option<FujiGrainEffect>,
/// Color Chrome Effect
#[clap(long)]
pub color_chrome_effect: Option<FujiColorChromeEffect>,
/// Color Chrome FX Blue
#[clap(long)]
pub color_chrome_fx_blue: Option<FujiColorChromeFXBlue>,
/// Smooth Skin Effect
#[clap(long)]
pub smooth_skin_effect: Option<FujiSmoothSkinEffect>,
/// Lens Modulation Optimizer
#[clap(long)]
pub lens_modulation_optimizer: Option<FujiLensModulationOptimizer>,
/// Color Space
#[clap(long)]
pub color_space: Option<FujiColorSpace>,
}
impl fmt::Display for FujiCustomSetting {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::C1 => write!(f, "C1"),
Self::C2 => write!(f, "C2"),
Self::C3 => write!(f, "C3"),
Self::C4 => write!(f, "C4"),
Self::C5 => write!(f, "C5"),
Self::C6 => write!(f, "C6"),
Self::C7 => write!(f, "C7"),
}
}
}
impl FromStr for FujiCustomSetting {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase();
let variant = match input.as_str() {
"c1" | "1" => Self::C1,
"c2" | "2" => Self::C2,
"c3" | "3" => Self::C3,
"c4" | "4" => Self::C4,
"c5" | "5" => Self::C5,
"c6" | "6" => Self::C6,
"c7" | "7" => Self::C7,
_ => bail!("Unknown custom setting '{s}'"),
};
Ok(variant)
}
}
impl Serialize for FujiCustomSetting {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u16((*self).into())
}
}
impl fmt::Display for FujiCustomSettingName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", &**self)
}
}
impl FromStr for FujiCustomSettingName {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
if s.len() > Self::MAX_LEN {
bail!("Value '{}' exceeds max length of {}", s, Self::MAX_LEN);
}
Ok(unsafe { Self::new_unchecked(s.to_string()) })
}
}
impl fmt::Display for FujiImageSize {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::R7728x5152 => write!(f, "7728x5152"),
Self::R7728x4344 => write!(f, "7728x4344"),
Self::R5152x5152 => write!(f, "5152x5152"),
Self::R6864x5152 => write!(f, "6864x5152"),
Self::R6432x5152 => write!(f, "6432x5152"),
Self::R5472x3648 => write!(f, "5472x3648"),
Self::R5472x3080 => write!(f, "5472x3080"),
Self::R3648x3648 => write!(f, "3648x3648"),
Self::R4864x3648 => write!(f, "4864x3648"),
Self::R4560x3648 => write!(f, "4560x3648"),
Self::R3888x2592 => write!(f, "3888x2592"),
Self::R3888x2184 => write!(f, "3888x2184"),
Self::R2592x2592 => write!(f, "2592x2592"),
Self::R3456x2592 => write!(f, "3456x2592"),
Self::R3264x2592 => write!(f, "3264x2592"),
}
}
}
impl FromStr for FujiImageSize {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase();
match input.as_str() {
"max" | "maximum" | "full" | "largest" => return Ok(Self::R7728x5152),
_ => {}
}
let input = s.replace(' ', "x").replace("by", "x");
if let Some((w_str, h_str)) = input.split_once('x')
&& let (Ok(w), Ok(h)) = (w_str.trim().parse::<u32>(), h_str.trim().parse::<u32>())
{
match (w, h) {
(7728, 5152) => return Ok(Self::R7728x5152),
(7728, 4344) => return Ok(Self::R7728x4344),
(5152, 5152) => return Ok(Self::R5152x5152),
(6864, 5152) => return Ok(Self::R6864x5152),
(6432, 5152) => return Ok(Self::R6432x5152),
(5472, 3648) => return Ok(Self::R5472x3648),
(5472, 3080) => return Ok(Self::R5472x3080),
(3648, 3648) => return Ok(Self::R3648x3648),
(4864, 3648) => return Ok(Self::R4864x3648),
(4560, 3648) => return Ok(Self::R4560x3648),
(3888, 2592) => return Ok(Self::R3888x2592),
(3888, 2184) => return Ok(Self::R3888x2184),
(2592, 2592) => return Ok(Self::R2592x2592),
(3456, 2592) => return Ok(Self::R3456x2592),
(3264, 2592) => return Ok(Self::R3264x2592),
_ => {}
}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown image size '{s}'. Did you mean '{best}'?");
}
bail!("Unknown image size '{s}'. Expected a resolution (e.g., '5472x3648') or 'maximum'.");
}
}
impl Serialize for FujiImageSize {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl fmt::Display for FujiImageQuality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FineRaw => write!(f, "Fine + RAW"),
Self::Fine => write!(f, "Fine"),
Self::NormalRaw => write!(f, "Normal + RAW"),
Self::Normal => write!(f, "Normal"),
Self::Raw => write!(f, "RAW"),
}
}
}
impl FromStr for FujiImageQuality {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase().replace(['+', ' '].as_ref(), "");
match input.as_str() {
"fineraw" => return Ok(Self::FineRaw),
"fine" => return Ok(Self::Fine),
"normalraw" => return Ok(Self::NormalRaw),
"normal" => return Ok(Self::Normal),
"raw" => return Ok(Self::Raw),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown image quality '{s}'. Did you mean '{best}'?");
}
bail!("Unknown image quality '{s}'");
}
}
impl fmt::Display for FujiDynamicRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Auto => write!(f, "Auto"),
Self::HDR100 => write!(f, "HDR100"),
Self::HDR200 => write!(f, "HDR200"),
Self::HDR400 => write!(f, "HDR400"),
}
}
}
impl FromStr for FujiDynamicRange {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), "");
match input.as_str() {
"auto" | "hdrauto" | "drauto" => return Ok(Self::Auto),
"100" | "hdr100" | "dr100" => return Ok(Self::HDR100),
"200" | "hdr200" | "dr200" => return Ok(Self::HDR200),
"400" | "hdr400" | "dr400" => return Ok(Self::HDR400),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown dynamic range '{s}'. Did you mean '{best}'?");
}
bail!("Unknown dynamic range '{s}'");
}
}
impl fmt::Display for FujiDynamicRangePriority {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Auto => write!(f, "Auto"),
Self::Strong => write!(f, "Strong"),
Self::Weak => write!(f, "Weak"),
Self::Off => write!(f, "Off"),
}
}
}
impl FromStr for FujiDynamicRangePriority {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), "");
match input.as_str() {
"auto" | "drpauto" => return Ok(Self::Auto),
"strong" | "drpstrong" => return Ok(Self::Strong),
"weak" | "drpweak" => return Ok(Self::Weak),
"off" | "drpoff" => return Ok(Self::Off),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown dynamic range priority '{s}'. Did you mean '{best}'?");
}
bail!("Unknown dynamic range priority '{s}'");
}
}
impl fmt::Display for FujiFilmSimulation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Provia => write!(f, "Provia"),
Self::Velvia => write!(f, "Velvia"),
Self::Astia => write!(f, "Astia"),
Self::PRONegHi => write!(f, "PRO Neg. Hi"),
Self::PRONegStd => write!(f, "PRO Neg. Std"),
Self::Monochrome => write!(f, "Monochrome"),
Self::MonochromeYe => write!(f, "Monochrome + Ye"),
Self::MonochromeR => write!(f, "Monochrome + R"),
Self::MonochromeG => write!(f, "Monochrome + G"),
Self::Sepia => write!(f, "Sepia"),
Self::ClassicChrome => write!(f, "Classic Chrome"),
Self::AcrosSTD => write!(f, "Acros"),
Self::AcrosYe => write!(f, "Acros + Ye"),
Self::AcrosR => write!(f, "Acros + R"),
Self::AcrosG => write!(f, "Acros + G"),
Self::Eterna => write!(f, "Eterna"),
Self::ClassicNegative => write!(f, "Classic Negative"),
Self::NostalgicNegative => write!(f, "Nostalgic Negative"),
Self::EternaBleachBypass => write!(f, "Eterna Bleach Bypass"),
Self::RealaAce => write!(f, "Reala Ace"),
}
}
}
impl FromStr for FujiFilmSimulation {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s
.trim()
.to_lowercase()
.replace([' ', '.', '+'].as_ref(), "");
match input.as_str() {
"provia" => return Ok(Self::Provia),
"velvia" => return Ok(Self::Velvia),
"astia" => return Ok(Self::Astia),
"proneghi" | "proneghigh" => {
return Ok(Self::PRONegHi);
}
"pronegstd" | "pronegstandard" => {
return Ok(Self::PRONegStd);
}
"mono" | "monochrome" => return Ok(Self::Monochrome),
"monoy" | "monoye" | "monoyellow" | "monochromey" | "monochromeye"
| "monochromeyellow" => {
return Ok(Self::MonochromeYe);
}
"monor" | "monored" | "monochromer" | "monochromered" => {
return Ok(Self::MonochromeR);
}
"monog" | "monogreen" | "monochromeg" | "monochromegreen" => {
return Ok(Self::MonochromeG);
}
"sepia" => return Ok(Self::Sepia),
"classicchrome" => return Ok(Self::ClassicChrome),
"acros" => return Ok(Self::AcrosSTD),
"acrosy" | "acrosye" | "acrosyellow" => {
return Ok(Self::AcrosYe);
}
"acrossr" | "acrossred" => {
return Ok(Self::AcrosR);
}
"acrossg" | "acrossgreen" => {
return Ok(Self::AcrosG);
}
"eterna" => return Ok(Self::Eterna),
"classicneg" | "classicnegative" => {
return Ok(Self::ClassicNegative);
}
"nostalgicneg" | "nostalgicnegative" => {
return Ok(Self::NostalgicNegative);
}
"eternabb" | "eternableach" | "eternableachbypass" => {
return Ok(Self::EternaBleachBypass);
}
"realaace" => {
return Ok(Self::RealaAce);
}
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown value '{s}'. Did you mean '{best}'?");
}
bail!("Unknown value '{input}'");
}
}
impl fmt::Display for FujiGrainEffect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::StrongLarge => write!(f, "Strong Large"),
Self::WeakLarge => write!(f, "Weak Large"),
Self::StrongSmall => write!(f, "Strong Small"),
Self::WeakSmall => write!(f, "Weak Small"),
Self::Off => write!(f, "Off"),
}
}
}
impl FromStr for FujiGrainEffect {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s
.trim()
.to_lowercase()
.replace(['+', '-', ',', ' '].as_ref(), "");
match input.as_str() {
"stronglarge" | "largestrong" => return Ok(Self::StrongLarge),
"weaklarge" | "largeweak" => return Ok(Self::WeakLarge),
"strongsmall" | "smallstrong" => return Ok(Self::StrongSmall),
"weaksmall" | "smallweak" => return Ok(Self::WeakSmall),
"off" => return Ok(Self::Off),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(&input, &choices) {
bail!("Unknown grain effect '{s}'. Did you mean '{best}'?");
}
bail!("Unknown grain effect '{s}'");
}
}
impl fmt::Display for FujiColorChromeEffect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Strong => write!(f, "Strong"),
Self::Weak => write!(f, "Weak"),
Self::Off => write!(f, "Off"),
}
}
}
impl FromStr for FujiColorChromeEffect {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase();
match input.as_str() {
"strong" => return Ok(Self::Strong),
"weak" => return Ok(Self::Weak),
"off" => return Ok(Self::Off),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown color chrome effect '{s}'. Did you mean '{best}'?");
}
bail!("Unknown color chrome effect '{s}'");
}
}
impl fmt::Display for FujiColorChromeFXBlue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Strong => write!(f, "Strong"),
Self::Weak => write!(f, "Weak"),
Self::Off => write!(f, "Off"),
}
}
}
impl FromStr for FujiColorChromeFXBlue {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase();
match input.as_str() {
"strong" => return Ok(Self::Strong),
"weak" => return Ok(Self::Weak),
"off" => return Ok(Self::Off),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown color chrome fx blue '{s}'. Did you mean '{best}'?");
}
bail!("Unknown color chrome fx blue '{s}'");
}
}
impl fmt::Display for FujiSmoothSkinEffect {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Strong => write!(f, "Strong"),
Self::Weak => write!(f, "Weak"),
Self::Off => write!(f, "Off"),
}
}
}
impl FromStr for FujiSmoothSkinEffect {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase();
match input.as_str() {
"strong" => return Ok(Self::Strong),
"weak" => return Ok(Self::Weak),
"off" => return Ok(Self::Off),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown smooth skin effect '{s}'. Did you mean '{best}'?");
}
bail!("Unknown smooth skin effect '{s}'");
}
}
impl fmt::Display for FujiWhiteBalance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::AsShot => write!(f, "As Shot"),
Self::WhitePriority => write!(f, "White Priority"),
Self::Auto => write!(f, "Auto"),
Self::AmbiencePriority => write!(f, "Ambience Priority"),
Self::Custom1 => write!(f, "Custom 1"),
Self::Custom2 => write!(f, "Custom 2"),
Self::Custom3 => write!(f, "Custom 3"),
Self::Temperature => write!(f, "Temperature"),
Self::Daylight => write!(f, "Daylight"),
Self::Shade => write!(f, "Shade"),
Self::Fluorescent1 => write!(f, "Fluorescent 1"),
Self::Fluorescent2 => write!(f, "Fluorescent 2"),
Self::Fluorescent3 => write!(f, "Fluorescent 3"),
Self::Incandescent => write!(f, "Incandescent"),
Self::Underwater => write!(f, "Underwater"),
}
}
}
impl FromStr for FujiWhiteBalance {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), "");
match input.as_str() {
"whitepriority" | "white" => return Ok(Self::WhitePriority),
// We can't set a film simulation to be "As Shot", so silently parse it to Auto
"auto" | "shot" | "asshot" | "original" => return Ok(Self::Auto),
"ambiencepriority" | "ambience" | "ambient" => {
return Ok(Self::AmbiencePriority);
}
"custom1" | "c1" => return Ok(Self::Custom1),
"custom2" | "c2" => return Ok(Self::Custom2),
"custom3" | "c3" => return Ok(Self::Custom3),
"temperature" | "k" | "kelvin" => return Ok(Self::Temperature),
"daylight" | "sunny" => return Ok(Self::Daylight),
"shade" | "cloudy" => return Ok(Self::Shade),
"fluorescent1" => {
return Ok(Self::Fluorescent1);
}
"fluorescent2" => {
return Ok(Self::Fluorescent2);
}
"fluorescent3" => {
return Ok(Self::Fluorescent3);
}
"incandescent" | "tungsten" => return Ok(Self::Incandescent),
"underwater" => return Ok(Self::Underwater),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown white balance '{s}'. Did you mean '{best}'?");
}
bail!("Unknown white balance '{s}'");
}
}
impl fmt::Display for FujiHighISONR {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Plus4 => write!(f, "+4"),
Self::Plus3 => write!(f, "+3"),
Self::Plus2 => write!(f, "+2"),
Self::Plus1 => write!(f, "+1"),
Self::Zero => write!(f, "0"),
Self::Minus1 => write!(f, "-1"),
Self::Minus2 => write!(f, "-2"),
Self::Minus3 => write!(f, "-3"),
Self::Minus4 => write!(f, "-4"),
}
}
}
impl FromStr for FujiHighISONR {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s
.trim()
.parse::<i16>()
.with_context(|| format!("Invalid numeric value '{s}'"))?;
match input {
4 => Ok(Self::Plus4),
3 => Ok(Self::Plus3),
2 => Ok(Self::Plus2),
1 => Ok(Self::Plus1),
0 => Ok(Self::Zero),
-1 => Ok(Self::Minus1),
-2 => Ok(Self::Minus2),
-3 => Ok(Self::Minus3),
-4 => Ok(Self::Minus4),
_ => bail!("Value {input} is out of range",),
}
}
}
impl Serialize for FujiHighISONR {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
Self::Plus4 => serializer.serialize_i16(4),
Self::Plus3 => serializer.serialize_i16(3),
Self::Plus2 => serializer.serialize_i16(2),
Self::Plus1 => serializer.serialize_i16(1),
Self::Zero => serializer.serialize_i16(0),
Self::Minus1 => serializer.serialize_i16(-1),
Self::Minus2 => serializer.serialize_i16(-2),
Self::Minus3 => serializer.serialize_i16(-3),
Self::Minus4 => serializer.serialize_i16(-4),
}
}
}
impl fmt::Display for FujiLensModulationOptimizer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Off => write!(f, "Off"),
Self::On => write!(f, "On"),
}
}
}
impl FromStr for FujiLensModulationOptimizer {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase();
match input.as_str() {
"off" | "false" => return Ok(Self::Off),
"on" | "true" => return Ok(Self::On),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown lens modulation optimizer '{s}'. Did you mean '{best}'?");
}
bail!("Unknown lens modulation optimizer '{s}'");
}
}
impl fmt::Display for FujiColorSpace {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::SRGB => write!(f, "sRGB"),
Self::AdobeRGB => write!(f, "Adobe RGB"),
}
}
}
impl FromStr for FujiColorSpace {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
let input = s.trim().to_lowercase();
match input.as_str() {
"s" | "srgb" => return Ok(Self::SRGB),
"adobe" | "adobergb" => return Ok(Self::AdobeRGB),
_ => {}
}
let choices: Vec<String> = Self::iter().map(|v| v.to_string()).collect();
if let Some(best) = get_closest(s, &choices) {
bail!("Unknown color space '{s}'. Did you mean '{best}'?");
}
bail!("Unknown color space '{s}'");
}
}
macro_rules! fuji_i16_cli {
($name:ident) => {
impl std::str::FromStr for $name {
type Err = anyhow::Error;
fn from_str(s: &str) -> anyhow::Result<Self> {
use anyhow::Context;
let input = s
.trim()
.parse::<f32>()
.with_context(|| format!("Invalid numeric value '{s}'"))?;
if !(Self::MIN..=Self::MAX).contains(&input) {
anyhow::bail!("Value {} is out of range", input);
}
#[allow(clippy::modulo_one)]
if (input - Self::MIN) % Self::STEP != 0.0 {
anyhow::bail!("Value {} is not aligned to step {}", input, Self::STEP);
}
#[allow(clippy::cast_possible_truncation)]
let raw = (input * Self::SCALE).round() as i16;
unsafe { Ok(Self::new_unchecked(raw)) }
}
}
impl serde::Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let val = f32::from(*self.deref()) / Self::SCALE;
serializer.serialize_f32(val)
}
}
impl std::fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let value = (f32::from(*self.deref()) / Self::SCALE);
write!(f, "{}", value)
}
}
};
}
fuji_i16_cli!(FujiMonochromaticColorTemperature);
fuji_i16_cli!(FujiMonochromaticColorTint);
fuji_i16_cli!(FujiWhiteBalanceShift);
fuji_i16_cli!(FujiWhiteBalanceTemperature);
fuji_i16_cli!(FujiHighlightTone);
fuji_i16_cli!(FujiShadowTone);
fuji_i16_cli!(FujiColor);
fuji_i16_cli!(FujiSharpness);
fuji_i16_cli!(FujiClarity);
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",
};
write!(f, "{s}")
}
}

View File

@@ -1,2 +1,3 @@
pub mod file;
pub mod film;
pub mod suggest;

29
src/cli/common/suggest.rs Normal file
View File

@@ -0,0 +1,29 @@
use strsim::damerau_levenshtein;
const SIMILARITY_THRESHOLD: usize = 8;
pub fn get_closest<'a, I, S>(input: &str, choices: I) -> Option<&'a str>
where
I: IntoIterator<Item = &'a S>,
S: AsRef<str> + 'a,
{
let mut best_score = usize::MAX;
let mut best_match: Option<&'a str> = None;
for choice in choices {
let choice_str = choice.as_ref();
let dist = damerau_levenshtein(&input.to_lowercase(), &choice_str.to_lowercase());
if dist < best_score {
best_score = dist;
best_match = Some(choice_str);
}
}
println!("{best_score}");
if best_score <= SIMILARITY_THRESHOLD {
best_match
} else {
None
}
}

View File

@@ -1,12 +1,6 @@
use std::fmt;
use clap::Subcommand;
use serde::Serialize;
use crate::{
camera::{Camera, ptp::enums::UsbMode},
usb,
};
use crate::{camera::devices::CameraInfoListItem, usb};
#[derive(Subcommand, Debug, Clone, Copy)]
pub enum DeviceCmd {
@@ -19,37 +13,8 @@ pub enum DeviceCmd {
Info,
}
#[derive(Serialize)]
pub struct CameraItemRepr {
pub name: &'static str,
pub usb_id: String,
pub vendor_id: String,
pub product_id: String,
}
impl From<&Camera> for CameraItemRepr {
fn from(camera: &Camera) -> Self {
Self {
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()),
}
}
}
impl fmt::Display for CameraItemRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{} ({}:{}) (USB ID: {})",
self.name, self.vendor_id, self.product_id, self.usb_id
)
}
}
fn handle_list(json: bool) -> anyhow::Result<()> {
let cameras: Vec<CameraItemRepr> = usb::get_connected_cameras()?
let cameras: Vec<CameraInfoListItem> = usb::get_connected_cameras()?
.iter()
.map(std::convert::Into::into)
.collect();
@@ -60,11 +25,10 @@ fn handle_list(json: bool) -> anyhow::Result<()> {
}
if cameras.is_empty() {
println!("No supported cameras connected.");
println!("No supported cameras connected");
return Ok(());
}
println!("Connected cameras:");
for d in cameras {
println!("- {d}");
}
@@ -72,53 +36,10 @@ fn handle_list(json: bool) -> anyhow::Result<()> {
Ok(())
}
#[derive(Serialize)]
pub struct CameraRepr {
#[serde(flatten)]
pub device: CameraItemRepr,
pub manufacturer: String,
pub model: String,
pub device_version: String,
pub serial_number: String,
pub mode: UsbMode,
pub battery: u32,
}
impl fmt::Display for CameraRepr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Name: {}", self.device.name)?;
writeln!(f, "USB ID: {}", self.device.usb_id)?;
writeln!(
f,
"Vendor ID: {}, Product ID: {}",
self.device.vendor_id, self.device.product_id
)?;
writeln!(f, "Manufacturer: {}", self.manufacturer)?;
writeln!(f, "Model: {}", self.model)?;
writeln!(f, "Version: {}", self.device_version)?;
writeln!(f, "Serial Number: {}", self.serial_number)?;
writeln!(f, "Mode: {}", self.mode)?;
write!(f, "Battery: {}%", self.battery)
}
}
fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
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.device_version.clone(),
serial_number: info.serial_number,
mode,
battery,
};
let repr = camera.info_get()?;
if json {
println!("{}", serde_json::to_string_pretty(&repr)?);

View File

@@ -1,6 +1,5 @@
mod common;
pub mod backup;
pub mod common;
pub mod device;
pub mod render;
pub mod simulation;

View File

@@ -1,8 +1,13 @@
use crate::{
camera::ptp::hex::{FujiCustomSetting, FujiCustomSettingName},
usb,
};
use super::common::{
file::{Input, Output},
film::FilmSimulationOptions,
};
use clap::Subcommand;
use clap::{Args, Subcommand};
#[derive(Subcommand, Debug)]
pub enum SimulationCmd {
@@ -13,15 +18,18 @@ pub enum SimulationCmd {
/// Get simulation
#[command(alias = "g")]
Get {
/// Simulation number or name
simulation: u8,
/// Simulation slot number
slot: FujiCustomSetting,
},
/// Set simulation parameters
#[command(alias = "s")]
Set {
/// Simulation number or name
simulation: u8,
/// Simulation slot number
slot: FujiCustomSetting,
#[command(flatten)]
set_film_simulation_options: SetFilmSimulationOptions,
#[command(flatten)]
film_simulation_options: FilmSimulationOptions,
@@ -30,8 +38,8 @@ pub enum SimulationCmd {
/// Export simulation
#[command(alias = "e")]
Export {
/// Simulation number or name
simulation: u8,
/// Simulation slot number
slot: FujiCustomSetting,
/// Output file (use '-' to write to stdout)
output_file: Output,
@@ -40,10 +48,93 @@ pub enum SimulationCmd {
/// Import simulation
#[command(alias = "i")]
Import {
/// Simulation number
slot: u8,
/// Simulation slot number
slot: FujiCustomSetting,
/// Input file (use '-' to read from stdin)
input_file: Input,
},
}
#[derive(Args, Debug)]
pub struct SetFilmSimulationOptions {
/// The name of the slot
#[clap(long)]
pub name: Option<FujiCustomSettingName>,
}
fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
let slots = camera.simulation_list()?;
if json {
println!("{}", serde_json::to_string_pretty(&slots)?);
} else {
for repr in slots {
println!("- {repr}");
}
}
Ok(())
}
fn handle_get(json: bool, device_id: Option<&str>, slot: FujiCustomSetting) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
let repr = camera.simulation_get(slot)?;
if json {
println!("{}", serde_json::to_string_pretty(&repr)?);
} else {
println!("{repr}");
}
Ok(())
}
#[allow(clippy::cognitive_complexity)]
#[allow(clippy::too_many_lines)]
fn handle_set(
device_id: Option<&str>,
slot: FujiCustomSetting,
set_options: &SetFilmSimulationOptions,
options: &FilmSimulationOptions,
) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
camera.simulation_set(slot, set_options, options)?;
Ok(())
}
fn handle_export(
_device_id: Option<&str>,
_slot: FujiCustomSetting,
_output: &Output,
) -> anyhow::Result<()> {
todo!();
}
fn handle_import(
_device_id: Option<&str>,
_slot: FujiCustomSetting,
_input: &Input,
) -> anyhow::Result<()> {
todo!();
}
pub fn handle(cmd: SimulationCmd, json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
match cmd {
SimulationCmd::List => handle_list(json, device_id),
SimulationCmd::Get { slot } => handle_get(json, device_id, slot),
SimulationCmd::Set {
slot,
set_film_simulation_options,
film_simulation_options,
} => handle_set(
device_id,
slot,
&set_film_simulation_options,
&film_simulation_options,
),
SimulationCmd::Export { slot, output_file } => handle_export(device_id, slot, &output_file),
SimulationCmd::Import { slot, input_file } => handle_import(device_id, slot, &input_file),
}
}

View File

@@ -14,7 +14,13 @@ pub fn init(verbose: u8) -> anyhow::Result<()> {
_ => LevelFilter::Trace,
};
let encoder = Box::new(PatternEncoder::new("{d} {h({l})} {M}::{L} - {m}{n}"));
let pattern = if verbose > 0 {
"{d} {h({l})} {M}::{L} - {m}{n}"
} else {
"{h({l})} - {m}{n}"
};
let encoder = Box::new(PatternEncoder::new(pattern));
let console = ConsoleAppender::builder()
.encoder(encoder)

View File

@@ -19,7 +19,10 @@ fn main() -> anyhow::Result<()> {
match cli.command {
Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?,
Commands::Backup(backup_cmd) => cli::backup::handle(backup_cmd, device_id)?,
_ => todo!(),
Commands::Simulation(simulation_cmd) => {
cli::simulation::handle(simulation_cmd, cli.json, device_id)?;
}
Commands::Render(_) => todo!(),
}
Ok(())

View File

@@ -2,11 +2,23 @@ use anyhow::{anyhow, bail};
use crate::camera::Camera;
pub 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 get_connected_cameras() -> anyhow::Result<Vec<Camera>> {
let mut connected_cameras = Vec::new();
for device in rusb::devices()?.iter() {
if let Ok(camera) = Camera::from_device(&device) {
if let Ok(camera) = Camera::try_from(&device) {
connected_cameras.push(camera);
}
}
@@ -25,7 +37,7 @@ pub fn get_connected_camera_by_id(id: &str) -> anyhow::Result<Camera> {
for device in rusb::devices()?.iter() {
if device.bus_number() == bus && device.address() == address {
return Camera::from_device(&device);
return Camera::try_from(&device);
}
}
@@ -38,6 +50,6 @@ pub fn get_camera(device_id: Option<&str>) -> anyhow::Result<Camera> {
None => get_connected_cameras()?
.into_iter()
.next()
.ok_or_else(|| anyhow!("No supported devices connected.")),
.ok_or_else(|| anyhow!("No supported devices connected")),
}
}