Compare commits

15 Commits
sdk ... main

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
1f26a91dcd feat: custom PTP implementation
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-15 23:35:35 +01:00
943f22c074 chore: refactor impl
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-15 20:04:16 +01:00
2e9fb61762 chore: clean up restore payload generation
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-15 08:41:37 +01:00
34 changed files with 3783 additions and 821 deletions

157
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"
@@ -237,15 +248,21 @@ name = "fujicli"
version = "0.1.0"
dependencies = [
"anyhow",
"bitflags",
"byteorder",
"clap",
"libc",
"libptp",
"erased-serde",
"log",
"log4rs",
"once_cell",
"num_enum",
"paste",
"ptp_cursor",
"ptp_macro",
"rusb",
"serde",
"serde_json",
"strsim",
"strum",
"strum_macros",
]
[[package]]
@@ -340,17 +357,6 @@ version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libptp"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e6b84822d9579c3adb36bcea61c396dc2596a95ca03a0ffd69636fc85ccc4e2"
dependencies = [
"byteorder",
"log",
"rusb",
]
[[package]]
name = "libusb1-sys"
version = "0.7.0"
@@ -443,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"
@@ -487,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"
@@ -502,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"
@@ -511,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"
@@ -689,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"
@@ -740,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"
@@ -1108,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

@@ -6,7 +6,6 @@ description = "A CLI to manage Fujifilm devices, simulations, backups, and rende
authors = [
"Nikolaos Karaolidis <nick@karaolidis.com>",
]
build = "build.rs"
[profile.release]
panic = 'abort'
@@ -16,12 +15,18 @@ codegen-units = 1
[dependencies]
anyhow = "1.0.100"
bitflags = "2.9.4"
byteorder = "1.5.0"
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
libc = "0.2.177"
libptp = "0.6.5"
log = "0.4.28"
log4rs = "1.4.0"
once_cell = "1.21.3"
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"

View File

@@ -1,12 +0,0 @@
use std::env;
use std::path::Path;
fn main() {
let sdk_path = env::var("XSDK_PATH").expect("XSDK_PATH environment variable must be set.");
let lib_path = Path::new(&sdk_path);
println!("cargo:rustc-link-search=native={}", lib_path.display());
println!("cargo:rustc-link-lib=dylib=stdc++");
println!("cargo:rerun-if-env-changed=XSDK_PATH");
}

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

@@ -32,50 +32,6 @@
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = with pkgs; [
makeWrapper
fujifilm-sdk-bin
];
XSDK_PATH = "${pkgs.fujifilm-sdk-bin}/lib";
postInstall = ''
wrapProgram $out/bin/fujicli \
--prefix LD_LIBRARY_PATH : ${pkgs.fujifilm-sdk-bin}/lib \
--prefix LD_LIBRARY_PATH : ${pkgs.stdenv.cc.cc.lib}/lib
'';
};
fujifilm-sdk-bin = pkgs.stdenv.mkDerivation {
pname = "fujifilm-sdk-bin";
version = "1.33.0";
src = pkgs.fetchzip {
url = "https://dl.fujifilm-x.com/global/special/camera-control-sdk/download/SDK13300.zip";
sha256 = "sha256-sKWzRt174WoIRs1ZEO+F2kUfLCRx6cT4XxfxCOAP/R4=";
};
nativeBuildInputs = with pkgs; [
coreutils
gnutar
];
installPhase = ''
mkdir -p extract
tar -xzf $src/REDISTRIBUTABLES/Linux/Linux64PC.tar.gz -C extract
mkdir -p $out/lib
cp extract/Linux64PC/* $out/lib/
soname=$(readelf -d "$out/lib/XAPI.so" | grep SONAME | awk -F'[][]' '{print $2}')
ln -sf $out/lib/XAPI.so $out/lib/libXAPI.so
ln -sf $out/lib/XAPI.so $out/lib/$soname
'';
meta = with pkgs.lib; {
license = licenses.unfree;
};
};
};
}
@@ -103,21 +59,18 @@
clippy
cargo-udeps
cargo-outdated
fujifilm-sdk-bin
stdenv.cc.cc.lib
cargo-expand
];
shellHook = ''
TOP="$(git rev-parse --show-toplevel)"
export CARGO_HOME="$TOP/.cargo"
export XSDK_PATH="${pkgs.fujifilm-sdk-bin}/lib"
export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.fujifilm-sdk-bin}/lib";
'';
};
packages.${system} = with pkgs; {
default = fujicli;
inherit fujicli fujicli-debug fujifilm-sdk-bin;
inherit fujicli;
};
formatter.${system} = treefmt.config.build.wrapper;

50
src/camera/devices/mod.rs Normal file
View File

@@ -0,0 +1,50 @@
mod x_trans_v;
use std::fmt;
use rusb::GlobalContext;
use serde::Serialize;
use super::{Camera, CameraImpl};
type ImplFactory<P> = fn() -> Box<dyn CameraImpl<P>>;
#[derive(Debug, Clone, Copy)]
pub struct SupportedCamera<P: rusb::UsbContext> {
pub name: &'static str,
pub vendor: u16,
pub product: u16,
pub impl_factory: ImplFactory<P>,
}
pub const SUPPORTED: &[SupportedCamera<GlobalContext>] = &[x_trans_v::x_t5::FUJIFILM_XT5];
#[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,
}
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()),
}
}
}
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
)
}
}

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(())
}
}

13
src/camera/error.rs Normal file
View File

@@ -0,0 +1,13 @@
use std::{error::Error, fmt};
#[allow(dead_code)]
#[derive(Debug)]
pub struct UnsupportedFeatureError;
impl fmt::Display for UnsupportedFeatureError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "feature is not supported for this device")
}
}
impl Error for UnsupportedFeatureError {}

235
src/camera/mod.rs Normal file
View File

@@ -0,0 +1,235 @@
pub mod devices;
pub mod error;
pub mod ptp;
use std::{fmt, time::Duration};
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 Camera {
pub r#impl: Box<dyn CameraImpl<GlobalContext>>,
pub ptp: 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),*)
}
};
}
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
}
pub fn vendor_id(&self) -> u16 {
self.r#impl.supported_camera().vendor
}
pub fn product_id(&self) -> u16 {
self.r#impl.supported_camera().product
}
pub fn connected_usb_id(&self) -> String {
format!("{}.{}", self.ptp.bus, self.ptp.address)
}
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.ptp.close_session(SESSION, self.r#impl.timeout()) {
error!("Error closing session: {e}");
}
debug!("Session closed");
}
}
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 });
}
bail!("Device not supported");
}
}
pub trait CameraImpl<P: rusb::UsbContext> {
fn supported_camera(&self) -> &'static SupportedCamera<P>;
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 info_get(&self, _ptp: &mut Ptp) -> anyhow::Result<Box<dyn CameraResult>> {
bail!("This device does not support getting detailed info")
}
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,
_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<()> {
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;

53
src/camera/ptp/error.rs Normal file
View File

@@ -0,0 +1,53 @@
use std::{fmt, io};
use crate::camera::ptp::hex::ResponseCode;
#[derive(Debug)]
pub enum Error {
Response(u16),
Malformed(String),
Usb(rusb::Error),
Io(io::Error),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::Response(r) => {
let name = ResponseCode::try_from(r)
.map_or_else(|_| "Unknown".to_string(), |c| format!("{c:?}"));
write!(f, "{name} (0x{r:04x})")
}
Self::Usb(ref e) => write!(f, "USB error: {e}"),
Self::Io(ref e) => write!(f, "IO error: {e}"),
Self::Malformed(ref e) => write!(f, "{e}"),
}
}
}
impl ::std::error::Error for Error {
fn cause(&self) -> Option<&dyn (::std::error::Error)> {
match *self {
Self::Usb(ref e) => Some(e),
Self::Io(ref e) => Some(e),
_ => None,
}
}
}
impl From<rusb::Error> for Error {
fn from(e: rusb::Error) -> Self {
Self::Usb(e)
}
}
impl From<io::Error> for Error {
fn from(e: io::Error) -> Self {
match e.kind() {
io::ErrorKind::UnexpectedEof => {
Self::Malformed("Unexpected end of message".to_string())
}
_ => Self::Io(e),
}
}
}

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

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

@@ -0,0 +1,248 @@
pub mod error;
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");
}
}

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

@@ -0,0 +1,78 @@
use ptp_macro::{PtpDeserialize, PtpSerialize};
use super::hex::{CommandCode, ContainerCode, ContainerType, ObjectFormat};
#[derive(Debug, PtpSerialize, PtpDeserialize)]
pub struct DeviceInfo {
pub version: u16,
pub vendor_ex_id: u32,
pub vendor_ex_version: u16,
pub vendor_extension_desc: String,
pub functional_mode: u16,
pub operations_supported: Vec<u16>,
pub events_supported: Vec<u16>,
pub device_properties_supported: Vec<u16>,
pub capture_formats: Vec<u16>,
pub image_formats: Vec<u16>,
pub manufacturer: String,
pub model: String,
pub device_version: String,
pub serial_number: String,
}
#[derive(Debug, Clone, Copy, PtpSerialize, PtpDeserialize)]
pub struct ContainerInfo {
pub total_len: u32,
pub kind: ContainerType,
pub code: ContainerCode,
pub transaction_id: u32,
}
impl ContainerInfo {
pub const SIZE: usize =
size_of::<u32>() + size_of::<u16>() + size_of::<u16>() + size_of::<u32>();
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,
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

@@ -1,3 +1,5 @@
use crate::usb;
use super::common::file::{Input, Output};
use clap::Subcommand;
@@ -7,28 +9,41 @@ pub enum BackupCmd {
#[command(alias = "e")]
Export {
/// Output file (use '-' to write to stdout)
output: Output,
output_file: Output,
},
/// Import backup
#[command(alias = "i")]
Import {
/// Input file (use '-' to read from stdin)
input: Input,
input_file: Input,
},
}
fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
todo!()
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.backup_export()?;
writer.write_all(&backup)?;
Ok(())
}
fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> {
todo!()
fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
let mut reader = input.get_reader()?;
let mut backup = Vec::new();
reader.read_to_end(&mut backup)?;
camera.backup_import(&backup)?;
Ok(())
}
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> {
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> anyhow::Result<()> {
match cmd {
BackupCmd::Export { output } => handle_export(device_id, &output),
BackupCmd::Import { input } => handle_import(device_id, &input),
BackupCmd::Export { output_file } => handle_export(device_id, &output_file),
BackupCmd::Import { input_file } => handle_import(device_id, &input_file),
}
}

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)
@@ -18,7 +19,7 @@ impl FromStr for Input {
}
impl Input {
pub fn get_reader(&self) -> Result<Box<dyn io::Read>, anyhow::Error> {
pub fn get_reader(&self) -> anyhow::Result<Box<dyn io::Read>> {
match self {
Self::Stdin => Ok(Box::new(io::stdin())),
Self::Path(path) => Ok(Box::new(File::open(path)?)),
@@ -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)
@@ -44,7 +46,7 @@ impl FromStr for Output {
}
impl Output {
pub fn get_writer(&self) -> Result<Box<dyn io::Write>, anyhow::Error> {
pub fn get_writer(&self) -> anyhow::Result<Box<dyn io::Write>> {
match self {
Self::Stdout => Ok(Box::new(io::stdout())),
Self::Path(path) => Ok(Box::new(File::create(path)?)),

View File

@@ -1,27 +1,831 @@
use anyhow::bail;
use clap::Args;
use std::{fmt, ops::Deref, str::FromStr};
#[derive(Debug, Clone)]
pub enum SimulationSelector {
Slot(u8),
Name(String),
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 {
/// 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 std::str::FromStr for SimulationSelector {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(slot) = s.parse::<u8>() {
return Ok(Self::Slot(slot));
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"),
}
if s.is_empty() {
bail!("Simulation name cannot be empty")
}
Ok(Self::Name(s.to_string()))
}
}
#[derive(Args, Debug)]
pub struct FilmSimulationOptions {}
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,6 +1,6 @@
use clap::Subcommand;
use crate::sdk::{XSDK, XSdkInterface};
use crate::{camera::devices::CameraInfoListItem, usb};
#[derive(Subcommand, Debug, Clone, Copy)]
pub enum DeviceCmd {
@@ -13,35 +13,46 @@ pub enum DeviceCmd {
Info,
}
fn handle_list(json: bool) -> Result<(), anyhow::Error> {
let sdk = XSDK.lock().unwrap();
let cameras = sdk.get_cameras(XSdkInterface::Usb, None)?;
let valid_cameras: Vec<_> = cameras
.into_iter()
.enumerate()
.filter(|(_, cam)| cam.valid)
fn handle_list(json: bool) -> anyhow::Result<()> {
let cameras: Vec<CameraInfoListItem> = usb::get_connected_cameras()?
.iter()
.map(std::convert::Into::into)
.collect();
if json {
println!("{}", serde_json::to_string_pretty(&valid_cameras)?);
println!("{}", serde_json::to_string_pretty(&cameras)?);
return Ok(());
}
for (id, cam) in valid_cameras {
println!("[{}] {}", id, cam);
if cameras.is_empty() {
println!("No supported cameras connected");
return Ok(());
}
for d in cameras {
println!("- {d}");
}
Ok(())
}
fn handle_info(json: bool, device: Option<u32>) -> Result<(), anyhow::Error> {
todo!()
fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
let mut camera = usb::get_camera(device_id)?;
let repr = camera.info_get()?;
if json {
println!("{}", serde_json::to_string_pretty(&repr)?);
return Ok(());
}
println!("{repr}");
Ok(())
}
pub fn handle(cmd: DeviceCmd, json: bool, device: Option<u32>) -> Result<(), anyhow::Error> {
pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
match cmd {
DeviceCmd::List => handle_list(json),
DeviceCmd::Info => handle_info(json, device),
DeviceCmd::Info => handle_info(json, device_id),
}
}

View File

@@ -1,11 +1,10 @@
mod common;
pub mod backup;
pub mod common;
pub mod device;
pub mod render;
pub mod simulation;
use clap::{Parser, Subcommand};
use clap::{ArgAction, Parser, Subcommand};
use backup::BackupCmd;
use device::DeviceCmd;
@@ -23,17 +22,13 @@ pub struct Cli {
#[arg(long, short = 'j', global = true)]
pub json: bool,
/// Only log warnings and errors
#[arg(long, short = 'q', global = true, conflicts_with = "verbose")]
pub quiet: bool,
/// Log extra debugging information
#[arg(long, short = 'v', global = true, conflicts_with = "quiet")]
pub verbose: bool,
/// Log extra debugging information (multiple instances increase verbosity)
#[arg(long, short = 'v', action = ArgAction::Count, global = true)]
pub verbose: u8,
/// Manually specify target device
#[arg(long, short = 'd', global = true)]
pub device: Option<u32>,
pub device: Option<String>,
}
#[derive(Subcommand, Debug)]

View File

@@ -2,7 +2,7 @@ use std::path::PathBuf;
use super::common::{
file::{Input, Output},
film::{FilmSimulationOptions, SimulationSelector},
film::FilmSimulationOptions,
};
use clap::Args;
@@ -10,7 +10,7 @@ use clap::Args;
pub struct RenderCmd {
/// Simulation number or name
#[arg(long, conflicts_with = "simulation_file")]
simulation: Option<SimulationSelector>,
simulation: Option<u8>,
/// Path to exported simulation
#[arg(long, conflicts_with = "simulation")]

View File

@@ -1,8 +1,13 @@
use crate::{
camera::ptp::hex::{FujiCustomSetting, FujiCustomSettingName},
usb,
};
use super::common::{
file::{Input, Output},
film::{FilmSimulationOptions, SimulationSelector},
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: SimulationSelector,
/// Simulation slot number
slot: FujiCustomSetting,
},
/// Set simulation parameters
#[command(alias = "s")]
Set {
/// Simulation number or name
simulation: SimulationSelector,
/// 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: SimulationSelector,
/// 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

@@ -6,16 +6,21 @@ use log4rs::{
encode::pattern::PatternEncoder,
};
pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> {
let level = if quiet {
LevelFilter::Warn
} else if verbose {
LevelFilter::Debug
} else {
LevelFilter::Info
pub fn init(verbose: u8) -> anyhow::Result<()> {
let level = match verbose {
0 => LevelFilter::Warn,
1 => LevelFilter::Info,
2 => LevelFilter::Debug,
_ => LevelFilter::Trace,
};
let encoder = Box::new(PatternEncoder::new("{d} {h({l})} {M}::{L} - {m}{n}"));
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

@@ -4,20 +4,25 @@
use clap::Parser;
use cli::Commands;
mod camera;
mod cli;
mod log;
mod sdk;
mod usb;
fn main() -> Result<(), anyhow::Error> {
fn main() -> anyhow::Result<()> {
let cli = cli::Cli::parse();
log::init(cli.quiet, cli.verbose)?;
log::init(cli.verbose)?;
let device_id = cli.device;
let device_id = cli.device.as_deref();
match cli.command {
Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?,
_ => todo!(),
Commands::Backup(backup_cmd) => cli::backup::handle(backup_cmd, device_id)?,
Commands::Simulation(simulation_cmd) => {
cli::simulation::handle(simulation_cmd, cli.json, device_id)?;
}
Commands::Render(_) => todo!(),
}
Ok(())

View File

@@ -1,143 +0,0 @@
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XSdkErrorSeverity {
Info,
Fatal,
}
impl fmt::Display for XSdkErrorSeverity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
XSdkErrorSeverity::Info => "INFO",
XSdkErrorSeverity::Fatal => "FATAL",
};
write!(f, "{}", text)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum XSdkErrorCode {
NoErr,
Sequence,
Param,
InvalidCamera,
LoadLib,
Unsupported,
Busy,
ForceModeBusy,
AfTimeout,
ShootError,
FrameFull,
Standby,
NoDriver,
NoModelModule,
ApiNotFound,
ApiMismatch,
InvalidUsbMode,
Communication,
Timeout,
Combination,
WriteError,
CardFull,
Hardware,
Internal,
MemFull,
Unknown,
RunningOtherFunction(XSdkErrorDetail),
}
impl XSdkErrorCode {
pub fn severity(&self) -> XSdkErrorSeverity {
match self {
XSdkErrorCode::NoErr => XSdkErrorSeverity::Info,
XSdkErrorCode::InvalidCamera
| XSdkErrorCode::Busy
| XSdkErrorCode::ForceModeBusy
| XSdkErrorCode::AfTimeout
| XSdkErrorCode::FrameFull
| XSdkErrorCode::Standby
| XSdkErrorCode::WriteError
| XSdkErrorCode::CardFull
| XSdkErrorCode::RunningOtherFunction(_) => XSdkErrorSeverity::Info,
_ => XSdkErrorSeverity::Fatal,
}
}
}
impl fmt::Display for XSdkErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
XSdkErrorCode::NoErr => "No error",
XSdkErrorCode::Sequence => "Function call sequence error",
XSdkErrorCode::Param => "Function parameter error",
XSdkErrorCode::InvalidCamera => "Invalid camera",
XSdkErrorCode::LoadLib => "Lower-layer libraries cannot be loaded",
XSdkErrorCode::Unsupported => "Unsupported function call",
XSdkErrorCode::Busy => "Camera is busy",
XSdkErrorCode::ForceModeBusy => {
"Camera is busy. XSDK_SetForceMode can be used to recover"
}
XSdkErrorCode::AfTimeout => "Unable to focus using autofocus",
XSdkErrorCode::ShootError => "Error occurred during shooting",
XSdkErrorCode::FrameFull => "Frame buffer full; release canceled",
XSdkErrorCode::Standby => "System standby",
XSdkErrorCode::NoDriver => "No camera found",
XSdkErrorCode::NoModelModule => "No library; model-dependent function cannot be called",
XSdkErrorCode::ApiNotFound => "Unknown model-dependent function call",
XSdkErrorCode::ApiMismatch => "Parameter mismatch for model-dependent function call",
XSdkErrorCode::InvalidUsbMode => "Invalid USB mode",
XSdkErrorCode::Communication => "Communication error",
XSdkErrorCode::Timeout => "Operation timeout for unknown reasons",
XSdkErrorCode::Combination => "Function call combination error",
XSdkErrorCode::WriteError => "Memory card write error. Memory card must be replaced",
XSdkErrorCode::CardFull => {
"Memory card full. Memory card must be replaced or formatted"
}
XSdkErrorCode::Hardware => "Camera hardware error",
XSdkErrorCode::Internal => "Unexpected internal error",
XSdkErrorCode::MemFull => "Unexpected memory error",
XSdkErrorCode::Unknown => "Other unexpected error",
XSdkErrorCode::RunningOtherFunction(details) => {
&format!("Camera is busy due to another function: {details}")
}
};
write!(f, "{text} ({})", self.severity())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum XSdkErrorDetail {
S1,
AEL,
AFL,
InstantAF,
AFON,
Shooting,
ShootingCountdown,
Recording,
LiveView,
UntransferredImage,
}
impl fmt::Display for XSdkErrorDetail {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text = match self {
XSdkErrorDetail::S1 => "S1 error (generic placeholder)",
XSdkErrorDetail::AEL => "AE is locked",
XSdkErrorDetail::AFL => "AF is locked",
XSdkErrorDetail::InstantAF => "INSTANT AF in operation",
XSdkErrorDetail::AFON => "AF for AF ON in operation",
XSdkErrorDetail::Shooting => "Shooting in progress",
XSdkErrorDetail::ShootingCountdown => "SELF-TIMER in operation",
XSdkErrorDetail::Recording => "Movie is in recording",
XSdkErrorDetail::LiveView => "Liveview is in progress",
XSdkErrorDetail::UntransferredImage => {
"Pictures remain in the in-camera volatile memory"
}
};
write!(f, "{}", text)
}
}

View File

@@ -1,404 +0,0 @@
mod error;
mod private;
use std::{
ffi::{CStr, CString},
fmt,
marker::PhantomPinned,
mem::MaybeUninit,
net::Ipv4Addr,
ptr,
sync::Mutex,
};
use error::{XSdkErrorCode, XSdkErrorDetail};
use anyhow::bail;
use libc::{c_char, c_long};
use log::{debug, error};
use once_cell::sync::Lazy;
use serde::Serialize;
#[derive(Debug)]
pub enum XSdkInterface {
Usb,
WifiLocal,
WifiIp(String),
}
#[derive(Debug, Serialize)]
pub enum CameraFramework {
Usb,
Ethernet,
Wifi,
}
impl fmt::Display for CameraFramework {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
CameraFramework::Usb => "USB",
CameraFramework::Ethernet => "Ethernet",
CameraFramework::Wifi => "WiFi",
};
write!(f, "{}", s)
}
}
#[derive(Debug, Serialize)]
pub struct CameraInfo {
pub id: usize,
pub product: String,
pub serial_number: Option<String>,
pub ip_address: Option<Ipv4Addr>,
pub framework: CameraFramework,
pub valid: bool,
}
impl fmt::Display for CameraInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(f, "Model: {}", self.product)?;
if let Some(sn) = &self.serial_number {
writeln!(f, "Serial Number: {sn}")?;
}
writeln!(f, "Connection: {}", self.framework)?;
if let Some(ip) = self.ip_address {
writeln!(f, "IP Address: {ip}")?;
}
fmt::Result::Ok(())
}
}
impl TryFrom<(usize, private::XSdkCameraList)> for CameraInfo {
type Error = anyhow::Error;
fn try_from(c: (usize, private::XSdkCameraList)) -> Result<Self, Self::Error> {
let (id, c) = c;
fn cstr_to_string(buf: &[c_char]) -> String {
let c_str = unsafe { CStr::from_ptr(buf.as_ptr()) };
c_str.to_string_lossy().into_owned()
}
let framework_str = cstr_to_string(&c.framework);
let framework = match framework_str.as_str() {
"USB" => CameraFramework::Usb,
"ETHER" => CameraFramework::Ethernet,
"IP" => CameraFramework::Wifi,
other => bail!("Unknown camera framework '{other}'"),
};
Ok(CameraInfo {
id,
product: cstr_to_string(&c.product),
serial_number: {
let s = cstr_to_string(&c.serial_no);
if s.is_empty() { None } else { Some(s) }
},
ip_address: {
let s = cstr_to_string(&c.ip_address);
if s.is_empty() {
None
} else {
Some(s.parse::<Ipv4Addr>()?)
}
},
framework,
valid: c.valid,
})
}
}
fn option_cstring_as_mut_ptr(cstring: &Option<CString>) -> *mut c_char {
cstring
.as_ref()
.map_or(std::ptr::null_mut(), |s| s.as_ptr() as *mut c_char)
}
impl XSdkInterface {
fn to_c(self) -> Result<(private::XSdkInterface, Option<CString>), anyhow::Error> {
let (l_interface, interface_cstring) = match self {
XSdkInterface::Usb => (private::XSdkInterface::USB, None),
XSdkInterface::WifiLocal => (private::XSdkInterface::WIFI_LOCAL, None),
XSdkInterface::WifiIp(ip) => {
let c_ip = match CString::new(ip) {
Ok(c) => c,
Err(_) => {
debug!("Failed to convert IP to CString");
bail!(XSdkErrorCode::ApiMismatch)
}
};
(private::XSdkInterface::WIFI_IP, Some(c_ip))
}
};
Ok((l_interface, interface_cstring))
}
}
pub static XSDK: Lazy<Mutex<XSdk>> = Lazy::new(|| {
let mut xsdk = XSdk::new();
xsdk.init().unwrap();
xsdk.detect(XSdkInterface::Usb, None).unwrap();
Mutex::new(xsdk)
});
#[derive(Debug, PartialEq, Eq)]
pub enum XSdkState {
Loaded,
Initialized,
Detected,
Session,
}
pub struct XSdk {
state: XSdkState,
handle: *mut private::XSdkHandle,
_pinned: PhantomPinned,
}
// Scary
unsafe impl Send for XSdk {}
impl XSdk {
fn new() -> Self {
debug!("Creating new XSdk instance");
Self {
state: XSdkState::Loaded,
handle: ptr::null_mut(),
_pinned: PhantomPinned,
}
}
fn check(&self, ret: private::XSdkApiEntry) -> Option<XSdkErrorCode> {
if ret == private::XSDK_COMPLETE {
debug!("No error returned by SDK");
return None;
}
debug!("Checking SDK return code: {}", ret);
let mut api_code: c_long = 0;
let mut err_code: c_long = 0;
unsafe {
private::XSDK_GetErrorNumber(self.handle, &mut api_code, &mut err_code);
}
debug!("API code: {}, Error code: {}", api_code, err_code);
let err_code = match private::XSdkErrorCode::from_bits(err_code) {
Some(code) => code,
None => {
debug!("Failed to convert error code from bits, returning Unknown");
return Some(XSdkErrorCode::Unknown);
}
};
let err_code = match err_code as private::XSdkErrorCode {
private::XSdkErrorCode::NOERR => XSdkErrorCode::NoErr,
private::XSdkErrorCode::SEQUENCE => XSdkErrorCode::Sequence,
private::XSdkErrorCode::PARAM => XSdkErrorCode::Param,
private::XSdkErrorCode::INVALID_CAMERA => XSdkErrorCode::InvalidCamera,
private::XSdkErrorCode::LOADLIB => XSdkErrorCode::LoadLib,
private::XSdkErrorCode::UNSUPPORTED => XSdkErrorCode::Unsupported,
private::XSdkErrorCode::BUSY => XSdkErrorCode::Busy,
private::XSdkErrorCode::AF_TIMEOUT => XSdkErrorCode::AfTimeout,
private::XSdkErrorCode::SHOOT_ERROR => XSdkErrorCode::ShootError,
private::XSdkErrorCode::FRAME_FULL => XSdkErrorCode::FrameFull,
private::XSdkErrorCode::STANDBY => XSdkErrorCode::Standby,
private::XSdkErrorCode::NODRIVER => XSdkErrorCode::NoDriver,
private::XSdkErrorCode::NO_MODEL_MODULE => XSdkErrorCode::NoModelModule,
private::XSdkErrorCode::API_NOTFOUND => XSdkErrorCode::ApiNotFound,
private::XSdkErrorCode::API_MISMATCH => XSdkErrorCode::ApiMismatch,
private::XSdkErrorCode::INVALID_USBMODE => XSdkErrorCode::InvalidUsbMode,
private::XSdkErrorCode::FORCEMODE_BUSY => XSdkErrorCode::ForceModeBusy,
private::XSdkErrorCode::RUNNING_OTHER_FUNCTION => {
debug!("Error: RunningOtherFunction, fetching details");
let mut details_code: c_long = 0;
unsafe {
private::XSDK_GetErrorDetails(self.handle, &mut details_code);
}
let details_code = match private::XSdkErrorDetail::from_bits(details_code) {
Some(code) => code,
None => {
debug!("Failed to convert error detail bits, returning Unknown");
return Some(XSdkErrorCode::Unknown);
}
};
let detail = match details_code {
private::XSdkErrorDetail::S1 => XSdkErrorDetail::S1,
private::XSdkErrorDetail::AEL => XSdkErrorDetail::AEL,
private::XSdkErrorDetail::AFL => XSdkErrorDetail::AFL,
private::XSdkErrorDetail::INSTANTAF => XSdkErrorDetail::InstantAF,
private::XSdkErrorDetail::AFON => XSdkErrorDetail::AFON,
private::XSdkErrorDetail::SHOOTING => XSdkErrorDetail::Shooting,
private::XSdkErrorDetail::SHOOTINGCOUNTDOWN => {
XSdkErrorDetail::ShootingCountdown
}
private::XSdkErrorDetail::RECORDING => XSdkErrorDetail::Recording,
private::XSdkErrorDetail::LIVEVIEW => XSdkErrorDetail::LiveView,
private::XSdkErrorDetail::UNTRANSFERRED_IMAGE => {
XSdkErrorDetail::UntransferredImage
}
_ => {
debug!("Unknown error detail received");
return Some(XSdkErrorCode::Unknown);
}
};
debug!("RunningOtherFunction detail: {:?}", detail);
XSdkErrorCode::RunningOtherFunction(detail)
}
private::XSdkErrorCode::COMMUNICATION => XSdkErrorCode::Communication,
private::XSdkErrorCode::TIMEOUT => XSdkErrorCode::Timeout,
private::XSdkErrorCode::COMBINATION => XSdkErrorCode::Combination,
private::XSdkErrorCode::WRITEERROR => XSdkErrorCode::WriteError,
private::XSdkErrorCode::CARDFULL => XSdkErrorCode::CardFull,
private::XSdkErrorCode::HARDWARE => XSdkErrorCode::Hardware,
private::XSdkErrorCode::INTERNAL => XSdkErrorCode::Internal,
private::XSdkErrorCode::MEMFULL => XSdkErrorCode::MemFull,
private::XSdkErrorCode::UNKNOWN => XSdkErrorCode::Unknown,
_ => XSdkErrorCode::Unknown,
};
debug!("Converted error code: {:?}", err_code);
Some(err_code)
}
fn init(&mut self) -> Result<(), anyhow::Error> {
if self.state != XSdkState::Loaded {
bail!(XSdkErrorCode::Sequence);
}
debug!("Initializing XSdk");
let ret = unsafe { private::XSDK_Init(self.handle) };
if let Some(err) = self.check(ret) {
bail!(err);
}
self.state = XSdkState::Initialized;
debug!("XSdk initialized successfully");
Ok(())
}
fn exit(&mut self) -> Result<(), anyhow::Error> {
debug!("Exiting XSdk");
if self.state != XSdkState::Loaded && self.state != XSdkState::Detected {
bail!(XSdkErrorCode::Sequence);
}
let ret = unsafe { private::XSDK_Exit() };
if let Some(err) = self.check(ret) {
bail!(err);
}
self.state = XSdkState::Loaded;
debug!("XSdk exited successfully");
Ok(())
}
fn detect(
&mut self,
interface: XSdkInterface,
device_name: Option<&str>,
) -> Result<(), anyhow::Error> {
if self.state != XSdkState::Initialized {
bail!(XSdkErrorCode::Sequence);
}
debug!(
"Detecting cameras (interface={:?}, device_name={:?})",
interface, device_name
);
let (l_interface, interface_cstring) = interface.to_c()?;
let p_interface = option_cstring_as_mut_ptr(&interface_cstring);
let device_cstring = device_name.map(|s| CString::new(s).unwrap());
let p_device_name = option_cstring_as_mut_ptr(&device_cstring);
let mut count: c_long = 0;
debug!("Calling XSDK_Detect to detect cameras");
let ret =
unsafe { private::XSDK_Detect(l_interface, p_interface, p_device_name, &mut count) };
if let Some(err) = self.check(ret) {
bail!(err);
}
self.state = XSdkState::Detected;
debug!("Detected {} cameras", count);
Ok(())
}
pub fn get_cameras(
&self,
interface: XSdkInterface,
device_name: Option<&str>,
) -> Result<Vec<CameraInfo>, anyhow::Error> {
debug!(
"Getting cameras (interface={:?}, device_name={:?})",
interface, device_name
);
if self.state != XSdkState::Detected {
bail!(XSdkErrorCode::Sequence);
}
let (l_interface, interface_cstring) = interface.to_c()?;
let p_interface = option_cstring_as_mut_ptr(&interface_cstring);
let device_cstring = device_name.map(|s| CString::new(s).unwrap());
let p_device_name = option_cstring_as_mut_ptr(&device_cstring);
let mut count: c_long = 0;
debug!("Calling XSDK_Append to count cameras");
let ret = unsafe {
private::XSDK_Append(
l_interface,
p_interface,
p_device_name,
&mut count,
std::ptr::null_mut(),
)
};
if let Some(err) = self.check(ret) {
bail!(err);
}
let mut cameras: Vec<MaybeUninit<private::XSdkCameraList>> =
vec![MaybeUninit::uninit(); count as usize];
debug!("Calling XSDK_Append to get {} cameras", count);
let ret = unsafe {
private::XSDK_Append(
l_interface,
p_interface,
p_device_name,
&mut count,
cameras.as_mut_ptr() as *mut private::XSdkCameraList,
)
};
if let Some(err) = self.check(ret) {
bail!(err);
}
let cameras = cameras
.into_iter()
.enumerate()
.map(|(id, c)| {
let c = unsafe { c.assume_init() };
(id, c).try_into()
})
.collect::<Result<Vec<_>, _>>()?;
debug!("Got {} cameras", cameras.len());
Ok(cameras)
}
}
impl Drop for XSdk {
fn drop(&mut self) {
if let Err(e) = self.exit().into() {
error!("Failed to exit XSdk: {:?}", e);
}
}
}

View File

@@ -1,109 +0,0 @@
use std::marker::PhantomPinned;
use bitflags::bitflags;
use libc::{c_char, c_int, c_long};
#[repr(C)]
pub struct XSdkHandle {
_data: (),
_marker: core::marker::PhantomData<(*mut u8, PhantomPinned)>,
}
pub type XSdkApiEntry = c_long;
pub const XSDK_COMPLETE: c_long = 0;
pub const XSDK_ERROR: c_long = -1;
pub type LPSTR = *mut libc::c_char;
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct XSdkInterface: c_long {
const USB = 0x00000001;
const WIFI_LOCAL = 0x00000010;
const WIFI_IP = 0x00000020;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XSdkErrorCode: c_long {
const NOERR = 0x00000000;
const SEQUENCE = 0x00001001;
const PARAM = 0x00001002;
const INVALID_CAMERA = 0x00001003;
const LOADLIB = 0x00001004;
const UNSUPPORTED = 0x00001005;
const BUSY = 0x00001006;
const AF_TIMEOUT = 0x00001007;
const SHOOT_ERROR = 0x00001008;
const FRAME_FULL = 0x00001009;
const STANDBY = 0x00001010;
const NODRIVER = 0x00001011;
const NO_MODEL_MODULE = 0x00001012;
const API_NOTFOUND = 0x00001013;
const API_MISMATCH = 0x00001014;
const INVALID_USBMODE = 0x00001015;
const FORCEMODE_BUSY = 0x00001016;
const RUNNING_OTHER_FUNCTION = 0x00001017;
const COMMUNICATION = 0x00002001;
const TIMEOUT = 0x00002002;
const COMBINATION = 0x00002003;
const WRITEERROR = 0x00002004;
const CARDFULL = 0x00002005;
const HARDWARE = 0x00003001;
const INTERNAL = 0x00009001;
const MEMFULL = 0x00009002;
const UNKNOWN = 0x00009100;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct XSdkErrorDetail: c_long {
const S1 = 0x00000001;
const AEL = 0x00000002;
const AFL = 0x00000004;
const INSTANTAF = 0x00000008;
const AFON = 0x00000010;
const SHOOTING = 0x00000020;
const SHOOTINGCOUNTDOWN = 0x00000040;
const RECORDING = 0x00000080;
const LIVEVIEW = 0x00000100;
const UNTRANSFERRED_IMAGE = 0x00000200;
}
}
#[repr(C, packed)]
#[derive(Debug, Clone, Copy)]
pub struct XSdkCameraList {
pub product: [c_char; 256],
pub serial_no: [c_char; 256],
pub ip_address: [c_char; 256],
pub framework: [c_char; 256],
pub valid: bool,
}
#[link(name = "XAPI")]
unsafe extern "C" {
pub fn XSDK_Init(hLib: *mut XSdkHandle) -> XSdkApiEntry;
pub fn XSDK_Exit() -> XSdkApiEntry;
pub fn XSDK_Detect(
lInterface: XSdkInterface,
pInterface: LPSTR,
pDeviceName: LPSTR,
plCount: *mut c_long,
) -> XSdkApiEntry;
pub fn XSDK_Append(
lInterface: XSdkInterface,
pInterface: LPSTR,
pDeviceName: LPSTR,
plCount: *mut c_long,
pCameraList: *mut XSdkCameraList,
) -> XSdkApiEntry;
pub fn XSDK_GetErrorNumber(
hCamera: *mut XSdkHandle,
plAPICode: *mut c_long,
plERRCode: *mut c_long,
) -> XSdkApiEntry;
pub fn XSDK_GetErrorDetails(hCamera: *mut XSdkHandle, plERRCode: *mut c_long) -> XSdkApiEntry;
}

55
src/usb/mod.rs Normal file
View File

@@ -0,0 +1,55 @@
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::try_from(&device) {
connected_cameras.push(camera);
}
}
Ok(connected_cameras)
}
pub fn get_connected_camera_by_id(id: &str) -> anyhow::Result<Camera> {
let parts: Vec<&str> = id.split('.').collect();
if parts.len() != 2 {
bail!("Invalid device id format: {id}");
}
let bus: u8 = parts[0].parse()?;
let address: u8 = parts[1].parse()?;
for device in rusb::devices()?.iter() {
if device.bus_number() == bus && device.address() == address {
return Camera::try_from(&device);
}
}
bail!("No device found with id: {id}");
}
pub fn get_camera(device_id: Option<&str>) -> anyhow::Result<Camera> {
match device_id {
Some(id) => get_connected_camera_by_id(id),
None => get_connected_cameras()?
.into_iter()
.next()
.ok_or_else(|| anyhow!("No supported devices connected")),
}
}