diff --git a/Cargo.lock b/Cargo.lock index 39d96c5..05b3be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -241,9 +241,15 @@ dependencies = [ "clap", "log", "log4rs", + "num_enum", + "ptp_cursor", + "ptp_macro", "rusb", "serde", "serde_json", + "strsim", + "strum", + "strum_macros", ] [[package]] @@ -430,6 +436,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" @@ -489,6 +517,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.101" @@ -498,6 +535,24 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptp_cursor" +version = "0.1.0" +dependencies = [ + "byteorder", +] + +[[package]] +name = "ptp_macro" +version = "0.1.0" +dependencies = [ + "byteorder", + "proc-macro2", + "ptp_cursor", + "quote", + "syn", +] + [[package]] name = "quote" version = "1.0.41" @@ -676,6 +731,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.106" @@ -727,6 +803,36 @@ 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 = "typemap-ors" version = "1.0.0" @@ -1095,6 +1201,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" diff --git a/Cargo.toml b/Cargo.toml index 58ae572..772e201 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,12 @@ byteorder = "1.5.0" clap = { version = "4.5.48", features = ["derive", "wrap_help"] } log = "0.4.28" log4rs = "1.4.0" +num_enum = "0.7.4" rusb = "0.9.4" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" +strsim = "0.11.1" +ptp_macro = { path = "crates/ptp/macro" } +ptp_cursor = { path = "crates/ptp/cursor" } +strum = { version = "0.27.2", features = ["strum_macros"] } +strum_macros = "0.27.2" diff --git a/crates/ptp/cursor/Cargo.lock b/crates/ptp/cursor/Cargo.lock new file mode 100644 index 0000000..20e088d --- /dev/null +++ b/crates/ptp/cursor/Cargo.lock @@ -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", +] diff --git a/crates/ptp/cursor/Cargo.toml b/crates/ptp/cursor/Cargo.toml new file mode 100644 index 0000000..3cca5bf --- /dev/null +++ b/crates/ptp/cursor/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "ptp_cursor" +version = "0.1.0" +edition = "2024" + +[dependencies] +byteorder = { version = "1.5.0" } diff --git a/crates/ptp/cursor/src/lib.rs b/crates/ptp/cursor/src/lib.rs new file mode 100644 index 0000000..f122f7f --- /dev/null +++ b/crates/ptp/cursor/src/lib.rs @@ -0,0 +1,264 @@ +#![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 { + self.read_u8() + } + + fn read_ptp_i8(&mut self) -> io::Result { + self.read_i8() + } + + fn read_ptp_u16(&mut self) -> io::Result { + self.read_u16::() + } + + fn read_ptp_i16(&mut self) -> io::Result { + self.read_i16::() + } + + fn read_ptp_u32(&mut self) -> io::Result { + self.read_u32::() + } + + fn read_ptp_i32(&mut self) -> io::Result { + self.read_i32::() + } + + fn read_ptp_u64(&mut self) -> io::Result { + self.read_u64::() + } + + fn read_ptp_i64(&mut self) -> io::Result { + self.read_i64::() + } + + fn read_ptp_vec(&mut self, func: F) -> io::Result> + where + F: Fn(&mut Self) -> io::Result, + { + let len = self.read_u32::()? as usize; + (0..len).map(|_| func(self)).collect() + } + + fn read_ptp_u8_vec(&mut self) -> io::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u8()) + } + + fn read_ptp_i8_vec(&mut self) -> io::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i8()) + } + + fn read_ptp_u16_vec(&mut self) -> io::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u16()) + } + + fn read_ptp_i16_vec(&mut self) -> io::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i16()) + } + + fn read_ptp_u32_vec(&mut self) -> io::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u32()) + } + + fn read_ptp_i32_vec(&mut self) -> io::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i32()) + } + + fn read_ptp_u64_vec(&mut self) -> io::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_u64()) + } + + fn read_ptp_i64_vec(&mut self) -> io::Result> { + self.read_ptp_vec(|cur| cur.read_ptp_i64()) + } + + fn read_ptp_str(&mut self) -> io::Result { + let len = self.read_u8()?; + if len == 0 { + return Ok(String::new()); + } + + let data: Vec = (0..(len - 1)) + .map(|_| self.read_u16::()) + .collect::>()?; + self.read_u16::()?; + String::from_utf16(&data) + .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "Invalid UTF-16")) + } + + fn expect_end(&mut self) -> io::Result<()>; +} + +impl> Read for Cursor { + 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::(*v) + } + + fn write_ptp_i16(&mut self, v: &i16) -> io::Result<()> { + self.write_i16::(*v) + } + + fn write_ptp_u32(&mut self, v: &u32) -> io::Result<()> { + self.write_u32::(*v) + } + + fn write_ptp_i32(&mut self, v: &i32) -> io::Result<()> { + self.write_i32::(*v) + } + + fn write_ptp_u64(&mut self, v: &u64) -> io::Result<()> { + self.write_u64::(*v) + } + + fn write_ptp_i64(&mut self, v: &i64) -> io::Result<()> { + self.write_i64::(*v) + } + + fn write_ptp_vec(&mut self, vec: &[T], func: F) -> io::Result<()> + where + F: Fn(&mut Self, &T) -> io::Result<()>, + { + self.write_u32::(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 = s.encode_utf16().collect(); + self.write_u8((utf16.len() + 1) as u8)?; + for c in utf16 { + self.write_u16::(c)?; + } + self.write_u16::(0)?; + Ok(()) + } +} + +impl Write for Vec {} + +pub trait PtpSerialize: Sized { + fn try_into_ptp(&self) -> io::Result>; + + fn try_write_ptp(&self, buf: &mut Vec) -> io::Result<()>; +} + +pub trait PtpDeserialize: Sized { + fn try_from_ptp(buf: &[u8]) -> io::Result; + + fn try_read_ptp(cur: &mut R) -> io::Result; +} + +macro_rules! impl_ptp { + ($ty:ty, $read_fn:ident, $write_fn:ident) => { + impl PtpSerialize for $ty { + fn try_into_ptp(&self) -> io::Result> { + let mut buf = Vec::new(); + self.try_write_ptp(&mut buf)?; + Ok(buf) + } + + fn try_write_ptp(&self, buf: &mut Vec) -> io::Result<()> { + buf.$write_fn(self) + } + } + + impl PtpDeserialize for $ty { + fn try_from_ptp(buf: &[u8]) -> io::Result { + let mut cur = Cursor::new(buf); + let val = Self::try_read_ptp(&mut cur)?; + cur.expect_end()?; + Ok(val) + } + + fn try_read_ptp(cur: &mut R) -> io::Result { + cur.$read_fn() + } + } + }; +} + +impl_ptp!(u8, read_ptp_u8, write_ptp_u8); +impl_ptp!(i8, read_ptp_i8, write_ptp_i8); +impl_ptp!(u16, read_ptp_u16, write_ptp_u16); +impl_ptp!(i16, read_ptp_i16, write_ptp_i16); +impl_ptp!(u32, read_ptp_u32, write_ptp_u32); +impl_ptp!(i32, read_ptp_i32, write_ptp_i32); +impl_ptp!(u64, read_ptp_u64, write_ptp_u64); +impl_ptp!(i64, read_ptp_i64, write_ptp_i64); +impl_ptp!(String, read_ptp_str, write_ptp_str); +impl_ptp!(Vec, read_ptp_u8_vec, write_ptp_u8_vec); +impl_ptp!(Vec, read_ptp_i8_vec, write_ptp_i8_vec); +impl_ptp!(Vec, read_ptp_u16_vec, write_ptp_u16_vec); +impl_ptp!(Vec, read_ptp_i16_vec, write_ptp_i16_vec); +impl_ptp!(Vec, read_ptp_u32_vec, write_ptp_u32_vec); +impl_ptp!(Vec, read_ptp_i32_vec, write_ptp_i32_vec); +impl_ptp!(Vec, read_ptp_u64_vec, write_ptp_u64_vec); +impl_ptp!(Vec, read_ptp_i64_vec, write_ptp_i64_vec); diff --git a/crates/ptp/macro/Cargo.lock b/crates/ptp/macro/Cargo.lock new file mode 100644 index 0000000..b5fc3d5 --- /dev/null +++ b/crates/ptp/macro/Cargo.lock @@ -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" diff --git a/crates/ptp/macro/Cargo.toml b/crates/ptp/macro/Cargo.toml new file mode 100644 index 0000000..c5a069e --- /dev/null +++ b/crates/ptp/macro/Cargo.toml @@ -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" } diff --git a/crates/ptp/macro/src/lib.rs b/crates/ptp/macro/src/lib.rs new file mode 100644 index 0000000..3303f8d --- /dev/null +++ b/crates/ptp/macro/src/lib.rs @@ -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> { + let mut buf = Vec::new(); + self.try_write_ptp(&mut buf)?; + Ok(buf) + } + + fn try_write_ptp(&self, buf: &mut Vec) -> 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> { + let mut buf = Vec::new(); + self.try_write_ptp(&mut buf)?; + Ok(buf) + } + + fn try_write_ptp(&self, buf: &mut Vec) -> std::io::Result<()> { + #(#write_fields)* + Ok(()) + } + } + } + } + Fields::Unit => { + quote! { + impl ptp_cursor::PtpSerialize for #name { + fn try_into_ptp(&self) -> std::io::Result> { + Ok(Vec::new()) + } + + fn try_write_ptp(&self, _buf: &mut Vec) -> 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::::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> { + let mut buf = Vec::new(); + self.try_write_ptp(&mut buf)?; + Ok(buf) + } + + fn try_write_ptp(&self, buf: &mut Vec) -> 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 { + 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(cur: &mut R) -> std::io::Result { + 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 { + 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(cur: &mut R) -> std::io::Result { + Ok(Self(#(#read_fields),*)) + } + } + } + } + Fields::Unit => { + quote! { + impl ptp_cursor::PtpDeserialize for #name { + fn try_from_ptp(buf: &[u8]) -> std::io::Result { + use ptp_cursor::Read; + + let mut cur = std::io::Cursor::new(buf); + cur.expect_end()?; + Ok(Self) + } + + fn try_read_ptp(_cur: &mut R) -> std::io::Result { + Ok(Self) + } + } + } + } + }, + Data::Enum(_) => { + let repr_ty = input + .attrs + .iter() + .find_map(|attr| { + if attr.path().is_ident("repr") { + attr.parse_args_with( + Punctuated::::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 { + 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(cur: &mut R) -> std::io::Result { + 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() +} diff --git a/flake.nix b/flake.nix index d2703eb..0965da8 100755 --- a/flake.nix +++ b/flake.nix @@ -59,6 +59,7 @@ clippy cargo-udeps cargo-outdated + cargo-expand ]; shellHook = '' diff --git a/src/camera/mod.rs b/src/camera/mod.rs index b430680..990c712 100644 --- a/src/camera/mod.rs +++ b/src/camera/mod.rs @@ -2,17 +2,23 @@ pub mod devices; pub mod error; pub mod ptp; -use std::{io::Cursor, time::Duration}; +use std::time::Duration; use anyhow::{anyhow, bail}; -use byteorder::{LittleEndian, WriteBytesExt}; use devices::SupportedCamera; use log::{debug, error}; use ptp::{ Ptp, - enums::{CommandCode, PropCode, UsbMode}, + hex::{ + CommandCode, DevicePropCode, FujiClarity, FujiColor, FujiColorChromeEffect, + FujiColorChromeFXBlue, FujiCustomSetting, FujiDynamicRange, FujiFilmSimulation, + FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, FujiImageSize, + FujiShadowTone, FujiSharpness, FujiStillDynamicRangePriority, FujiWhiteBalance, + FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, UsbMode, + }, structs::DeviceInfo, }; +use ptp_cursor::{PtpDeserialize, PtpSerialize}; use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE}; const SESSION: u32 = 1; @@ -108,17 +114,6 @@ impl Camera { format!("{}.{}", self.ptp.bus, self.ptp.address) } - fn prop_value_as_scalar(data: &[u8]) -> anyhow::Result { - let data = match data.len() { - 1 => anyhow::Ok(u32::from(data[0])), - 2 => anyhow::Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))), - 4 => anyhow::Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])), - n => bail!("Cannot parse {n} bytes as scalar"), - }?; - - Ok(data) - } - pub fn get_info(&mut self) -> anyhow::Result { let info = self.r#impl.get_info(&mut self.ptp)?; Ok(info) @@ -127,30 +122,22 @@ impl Camera { pub fn get_usb_mode(&mut self) -> anyhow::Result { let data = self .r#impl - .get_prop_value(&mut self.ptp, PropCode::FujiUsbMode); - - let result = Self::prop_value_as_scalar(&data?)?.into(); + .get_prop_value(&mut self.ptp, DevicePropCode::FujiUsbMode)?; + let result = UsbMode::try_from_ptp(&data)?; Ok(result) } pub fn get_battery_info(&mut self) -> anyhow::Result { let data = self .r#impl - .get_prop_value(&mut self.ptp, PropCode::FujiBatteryInfo2); + .get_prop_value(&mut self.ptp, DevicePropCode::FujiBatteryInfo2)?; - let data = data?; debug!("Raw battery data: {data:?}"); - let utf16: Vec = data[1..] - .chunks(2) - .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) - .take_while(|&c| c != 0) - .collect(); + let raw_string = String::try_from_ptp(&data)?; + debug!("Decoded raw string: {raw_string}"); - let utf8_string = String::from_utf16(&utf16)?; - debug!("Decoded UTF-16 string: {utf8_string}"); - - let percentage: u32 = utf8_string + let percentage: u32 = raw_string .split(',') .next() .ok_or_else(|| anyhow!("Failed to parse battery percentage"))? @@ -166,6 +153,86 @@ impl Camera { pub fn import_backup(&mut self, backup: &[u8]) -> anyhow::Result<()> { self.r#impl.import_backup(&mut self.ptp, backup) } + + pub fn set_active_custom_setting(&mut self, slot: FujiCustomSetting) -> anyhow::Result<()> { + self.r#impl.set_custom_setting_slot(&mut self.ptp, slot) + } + + pub fn get_custom_setting_name(&mut self) -> anyhow::Result { + self.r#impl.get_custom_setting_name(&mut self.ptp) + } + + pub fn get_image_size(&mut self) -> anyhow::Result { + self.r#impl.get_image_size(&mut self.ptp) + } + + pub fn get_image_quality(&mut self) -> anyhow::Result { + self.r#impl.get_image_quality(&mut self.ptp) + } + + pub fn get_dynamic_range(&mut self) -> anyhow::Result { + self.r#impl.get_dynamic_range(&mut self.ptp) + } + + pub fn get_dynamic_range_priority(&mut self) -> anyhow::Result { + self.r#impl.get_dynamic_range_priority(&mut self.ptp) + } + + pub fn get_film_simulation(&mut self) -> anyhow::Result { + self.r#impl.get_film_simulation(&mut self.ptp) + } + + pub fn get_grain_effect(&mut self) -> anyhow::Result { + self.r#impl.get_grain_effect(&mut self.ptp) + } + + pub fn get_white_balance(&mut self) -> anyhow::Result { + self.r#impl.get_white_balance(&mut self.ptp) + } + + pub fn get_high_iso_nr(&mut self) -> anyhow::Result { + self.r#impl.get_high_iso_nr(&mut self.ptp) + } + + pub fn get_highlight_tone(&mut self) -> anyhow::Result { + self.r#impl.get_highlight_tone(&mut self.ptp) + } + + pub fn get_shadow_tone(&mut self) -> anyhow::Result { + self.r#impl.get_shadow_tone(&mut self.ptp) + } + + pub fn get_color(&mut self) -> anyhow::Result { + self.r#impl.get_color(&mut self.ptp) + } + + pub fn get_sharpness(&mut self) -> anyhow::Result { + self.r#impl.get_sharpness(&mut self.ptp) + } + + pub fn get_clarity(&mut self) -> anyhow::Result { + self.r#impl.get_clarity(&mut self.ptp) + } + + pub fn get_wb_shift_red(&mut self) -> anyhow::Result { + self.r#impl.get_wb_shift_red(&mut self.ptp) + } + + pub fn get_wb_shift_blue(&mut self) -> anyhow::Result { + self.r#impl.get_wb_shift_blue(&mut self.ptp) + } + + pub fn get_wb_temperature(&mut self) -> anyhow::Result { + self.r#impl.get_wb_temperature(&mut self.ptp) + } + + pub fn get_color_chrome_effect(&mut self) -> anyhow::Result { + self.r#impl.get_color_chrome_effect(&mut self.ptp) + } + + pub fn get_color_chrome_fx_blue(&mut self) -> anyhow::Result { + self.r#impl.get_color_chrome_fx_blue(&mut self.ptp) + } } impl Drop for Camera { @@ -193,9 +260,8 @@ pub trait CameraImpl { debug!("Sending OpenSession command"); _ = ptp.send( CommandCode::OpenSession, - Some(&[session_id]), + &[session_id], None, - true, self.timeout(), )?; Ok(()) @@ -203,25 +269,41 @@ pub trait CameraImpl { fn close_session(&self, ptp: &mut Ptp, _: u32) -> anyhow::Result<()> { debug!("Sending CloseSession command"); - _ = ptp.send(CommandCode::CloseSession, None, None, true, self.timeout())?; + _ = ptp.send(CommandCode::CloseSession, &[], None, self.timeout())?; Ok(()) } fn get_info(&self, ptp: &mut Ptp) -> anyhow::Result { debug!("Sending GetDeviceInfo command"); - let response = ptp.send(CommandCode::GetDeviceInfo, None, None, true, self.timeout())?; + let response = ptp.send(CommandCode::GetDeviceInfo, &[], None, self.timeout())?; debug!("Received response with {} bytes", response.len()); - let info = DeviceInfo::try_from(response.as_slice())?; + let info = DeviceInfo::try_from_ptp(&response)?; Ok(info) } - fn get_prop_value(&self, ptp: &mut Ptp, prop: PropCode) -> anyhow::Result> { + fn get_prop_value(&self, ptp: &mut Ptp, prop: DevicePropCode) -> anyhow::Result> { debug!("Sending GetDevicePropValue command for property {prop:?}"); let response = ptp.send( CommandCode::GetDevicePropValue, - Some(&[prop as u32]), + &[prop.into()], None, - true, + self.timeout(), + )?; + debug!("Received response with {} bytes", response.len()); + Ok(response) + } + + fn set_prop_value( + &self, + ptp: &mut Ptp, + prop: DevicePropCode, + value: &[u8], + ) -> anyhow::Result> { + debug!("Sending GetDevicePropValue command for property {prop:?}"); + let response = ptp.send( + CommandCode::SetDevicePropValue, + &[prop.into()], + Some(value), self.timeout(), )?; debug!("Received response with {} bytes", response.len()); @@ -232,23 +314,11 @@ pub trait CameraImpl { const HANDLE: u32 = 0x0; debug!("Sending GetObjectInfo command for backup"); - let response = ptp.send( - CommandCode::GetObjectInfo, - Some(&[HANDLE]), - None, - true, - self.timeout(), - )?; + 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, - Some(&[HANDLE]), - None, - true, - self.timeout(), - )?; + let response = ptp.send(CommandCode::GetObject, &[HANDLE], None, self.timeout())?; debug!("Received response with {} bytes", response.len()); Ok(response) @@ -257,21 +327,18 @@ pub trait CameraImpl { fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> { debug!("Preparing ObjectInfo header for backup"); - let mut header1 = vec![0u8; 1012]; - let mut cursor = Cursor::new(&mut header1[..]); - cursor.write_u32::(0x0)?; - cursor.write_u16::(0x5000)?; - cursor.write_u16::(0x0)?; - cursor.write_u32::(u32::try_from(buffer.len())?)?; - - let header2 = vec![0u8; 64]; + let mut header = vec![0u8; 1076]; + 0x0u32.try_write_ptp(&mut header)?; + 0x5000u16.try_write_ptp(&mut header)?; + 0x0u16.try_write_ptp(&mut header)?; + 0x0u32.try_write_ptp(&mut header)?; + u32::try_from(buffer.len())?.try_write_ptp(&mut header)?; debug!("Sending SendObjectInfo command for backup"); - let response = ptp.send_many( + let response = ptp.send( CommandCode::SendObjectInfo, - Some(&[0x0, 0x0]), - Some(&[&header1, &header2]), - true, + &[0x0, 0x0], + Some(&header), self.timeout(), )?; debug!("Received response with {} bytes", response.len()); @@ -279,13 +346,154 @@ pub trait CameraImpl { debug!("Sending SendObject command for backup"); let response = ptp.send( CommandCode::SendObject, - Some(&[0x0]), + &[0x0], Some(buffer), - true, self.timeout(), )?; debug!("Received response with {} bytes", response.len()); Ok(()) } + + fn set_custom_setting_slot( + &self, + ptp: &mut Ptp, + slot: FujiCustomSetting, + ) -> anyhow::Result<()> { + self.set_prop_value( + ptp, + DevicePropCode::FujiStillCustomSetting, + &slot.try_into_ptp()?, + )?; + Ok(()) + } + + fn get_custom_setting_name(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingName)?; + let name = String::try_from_ptp(&bytes)?; + Ok(name) + } + + fn get_image_size(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingImageSize)?; + let result = FujiImageSize::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_image_quality(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingImageQuality)?; + let result = FujiImageQuality::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_dynamic_range(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingDynamicRange)?; + let result = FujiDynamicRange::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_dynamic_range_priority( + &self, + ptp: &mut Ptp, + ) -> anyhow::Result { + let bytes = self.get_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingDynamicRangePriority, + )?; + let result = FujiStillDynamicRangePriority::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_film_simulation(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = + self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingFilmSimulation)?; + let result = FujiFilmSimulation::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_grain_effect(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingGrainEffect)?; + let result = FujiGrainEffect::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_white_balance(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalance)?; + let result = FujiWhiteBalance::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_high_iso_nr(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingHighISONR)?; + let result = FujiHighISONR::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_highlight_tone(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = + self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingHighlightTone)?; + let result = FujiHighlightTone::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_shadow_tone(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingShadowTone)?; + let result = FujiShadowTone::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_color(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColor)?; + let result = FujiColor::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_sharpness(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingSharpness)?; + let result = FujiSharpness::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_clarity(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingClarity)?; + let result = FujiClarity::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_wb_shift_red(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = + self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalanceRed)?; + let result = FujiWhiteBalanceShift::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_wb_shift_blue(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = + self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingWhiteBalanceBlue)?; + let result = FujiWhiteBalanceShift::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_wb_temperature(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = self.get_prop_value( + ptp, + DevicePropCode::FujiStillCustomSettingWhiteBalanceTemperature, + )?; + let result = FujiWhiteBalanceTemperature::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_color_chrome_effect(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = + self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColorChromeEffect)?; + let result = FujiColorChromeEffect::try_from_ptp(&bytes)?; + Ok(result) + } + + fn get_color_chrome_fx_blue(&self, ptp: &mut Ptp) -> anyhow::Result { + let bytes = + self.get_prop_value(ptp, DevicePropCode::FujiStillCustomSettingColorChromeFXBlue)?; + let result = FujiColorChromeFXBlue::try_from_ptp(&bytes)?; + Ok(result) + } } diff --git a/src/camera/ptp/enums.rs b/src/camera/ptp/enums.rs deleted file mode 100644 index 2174952..0000000 --- a/src/camera/ptp/enums.rs +++ /dev/null @@ -1,205 +0,0 @@ -use std::fmt; - -use anyhow::bail; -use serde::Serialize; - -#[repr(u16)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum CommandCode { - GetDeviceInfo = 0x1001, - OpenSession = 0x1002, - CloseSession = 0x1003, - GetObjectInfo = 0x1008, - GetObject = 0x1009, - SendObjectInfo = 0x100C, - SendObject = 0x100D, - GetDevicePropValue = 0x1015, - SetDevicePropValue = 0x1016, -} - -impl TryFrom for CommandCode { - type Error = anyhow::Error; - - fn try_from(value: u16) -> Result { - match value { - 0x1001 => Ok(Self::GetDeviceInfo), - 0x1002 => Ok(Self::OpenSession), - 0x1003 => Ok(Self::CloseSession), - 0x1008 => Ok(Self::GetObjectInfo), - 0x1009 => Ok(Self::GetObject), - 0x100C => Ok(Self::SendObjectInfo), - 0x100D => Ok(Self::SendObject), - 0x1015 => Ok(Self::GetDevicePropValue), - 0x1016 => Ok(Self::SetDevicePropValue), - v => bail!("Unknown command code '{v:x?}'"), - } - } -} - -#[repr(u16)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ResponseCode { - Undefined = 0x2000, - Ok = 0x2001, - GeneralError = 0x2002, - SessionNotOpen = 0x2003, - InvalidTransactionId = 0x2004, - OperationNotSupported = 0x2005, - ParameterNotSupported = 0x2006, - IncompleteTransfer = 0x2007, - InvalidStorageId = 0x2008, - InvalidObjectHandle = 0x2009, - DevicePropNotSupported = 0x200A, - InvalidObjectFormatCode = 0x200B, - StoreFull = 0x200C, - ObjectWriteProtected = 0x200D, - StoreReadOnly = 0x200E, - AccessDenied = 0x200F, - NoThumbnailPresent = 0x2010, - SelfTestFailed = 0x2011, - PartialDeletion = 0x2012, - StoreNotAvailable = 0x2013, - SpecificationByFormatUnsupported = 0x2014, - NoValidObjectInfo = 0x2015, - InvalidCodeFormat = 0x2016, - UnknownVendorCode = 0x2017, - CaptureAlreadyTerminated = 0x2018, - DeviceBusy = 0x2019, - InvalidParentObject = 0x201A, - InvalidDevicePropFormat = 0x201B, - InvalidDevicePropValue = 0x201C, - InvalidParameter = 0x201D, - SessionAlreadyOpen = 0x201E, - TransactionCancelled = 0x201F, - SpecificationOfDestinationUnsupported = 0x2020, -} - -impl std::convert::TryFrom for ResponseCode { - type Error = anyhow::Error; - - fn try_from(value: u16) -> Result { - match value { - 0x2000 => Ok(Self::Undefined), - 0x2001 => Ok(Self::Ok), - 0x2002 => Ok(Self::GeneralError), - 0x2003 => Ok(Self::SessionNotOpen), - 0x2004 => Ok(Self::InvalidTransactionId), - 0x2005 => Ok(Self::OperationNotSupported), - 0x2006 => Ok(Self::ParameterNotSupported), - 0x2007 => Ok(Self::IncompleteTransfer), - 0x2008 => Ok(Self::InvalidStorageId), - 0x2009 => Ok(Self::InvalidObjectHandle), - 0x200A => Ok(Self::DevicePropNotSupported), - 0x200B => Ok(Self::InvalidObjectFormatCode), - 0x200C => Ok(Self::StoreFull), - 0x200D => Ok(Self::ObjectWriteProtected), - 0x200E => Ok(Self::StoreReadOnly), - 0x200F => Ok(Self::AccessDenied), - 0x2010 => Ok(Self::NoThumbnailPresent), - 0x2011 => Ok(Self::SelfTestFailed), - 0x2012 => Ok(Self::PartialDeletion), - 0x2013 => Ok(Self::StoreNotAvailable), - 0x2014 => Ok(Self::SpecificationByFormatUnsupported), - 0x2015 => Ok(Self::NoValidObjectInfo), - 0x2016 => Ok(Self::InvalidCodeFormat), - 0x2017 => Ok(Self::UnknownVendorCode), - 0x2018 => Ok(Self::CaptureAlreadyTerminated), - 0x2019 => Ok(Self::DeviceBusy), - 0x201A => Ok(Self::InvalidParentObject), - 0x201B => Ok(Self::InvalidDevicePropFormat), - 0x201C => Ok(Self::InvalidDevicePropValue), - 0x201D => Ok(Self::InvalidParameter), - 0x201E => Ok(Self::SessionAlreadyOpen), - 0x201F => Ok(Self::TransactionCancelled), - 0x2020 => Ok(Self::SpecificationOfDestinationUnsupported), - v => bail!("Unknown response code '{v:x?}'"), - } - } -} - -#[repr(u16)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ContainerCode { - Command(CommandCode), - Response(ResponseCode), -} - -impl From for u16 { - fn from(code: ContainerCode) -> Self { - match code { - ContainerCode::Command(cmd) => cmd as Self, - ContainerCode::Response(resp) => resp as Self, - } - } -} - -impl TryFrom for ContainerCode { - type Error = anyhow::Error; - - fn try_from(value: u16) -> Result { - if let Ok(cmd) = CommandCode::try_from(value) { - return Ok(Self::Command(cmd)); - } - - if let Ok(resp) = ResponseCode::try_from(value) { - return Ok(Self::Response(resp)); - } - - bail!("Unknown container code '{value:x?}'"); - } -} - -#[repr(u32)] -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum PropCode { - FujiUsbMode = 0xd16e, - FujiBatteryInfo2 = 0xD36B, -} - -#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] -pub enum UsbMode { - RawConversion, - Unsupported, -} - -impl From for UsbMode { - fn from(val: u32) -> Self { - match val { - 6 => Self::RawConversion, - _ => Self::Unsupported, - } - } -} - -impl fmt::Display for UsbMode { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Self::RawConversion => "USB RAW CONV./BACKUP RESTORE", - Self::Unsupported => "Unsupported USB Mode", - }; - write!(f, "{s}") - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u16)] -pub enum ContainerType { - Command = 1, - Data = 2, - Response = 3, - Event = 4, -} - -impl TryFrom for ContainerType { - type Error = anyhow::Error; - - fn try_from(value: u16) -> Result { - match value { - 1 => Ok(Self::Command), - 2 => Ok(Self::Data), - 3 => Ok(Self::Response), - 4 => Ok(Self::Event), - v => bail!("Invalid message type '{v}'"), - } - } -} diff --git a/src/camera/ptp/error.rs b/src/camera/ptp/error.rs index 363d396..e86cbd4 100644 --- a/src/camera/ptp/error.rs +++ b/src/camera/ptp/error.rs @@ -1,6 +1,6 @@ use std::{fmt, io}; -use crate::camera::ptp::enums::ResponseCode; +use crate::camera::ptp::hex::ResponseCode; #[derive(Debug)] pub enum Error { diff --git a/src/camera/ptp/hex.rs b/src/camera/ptp/hex.rs new file mode 100644 index 0000000..de96a1e --- /dev/null +++ b/src/camera/ptp/hex.rs @@ -0,0 +1,1120 @@ +use std::{ + fmt, + io::{self, Cursor}, + str::FromStr, +}; + +use anyhow::{Context, bail}; +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use ptp_cursor::{PtpDeserialize, PtpSerialize, Read}; +use ptp_macro::{PtpDeserialize, PtpSerialize}; +use serde::{Serialize, Serializer}; +use strum::IntoEnumIterator; +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, +} + +#[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 for u16 { + fn from(code: ContainerCode) -> Self { + match code { + ContainerCode::Command(cmd) => cmd.into(), + ContainerCode::Response(resp) => resp.into(), + } + } +} + +impl TryFrom for ContainerCode { + type Error = anyhow::Error; + + fn try_from(value: u16) -> Result { + 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> { + let value: u16 = (*self).into(); + value.try_into_ptp() + } + + fn try_write_ptp(&self, buf: &mut Vec) -> io::Result<()> { + let value: u16 = (*self).into(); + value.try_write_ptp(buf) + } +} + +impl PtpDeserialize for ContainerCode { + fn try_from_ptp(buf: &[u8]) -> io::Result { + 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(cur: &mut R) -> io::Result { + let value = ::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, + FujiStillCustomSetting = 0xD18C, + FujiStillCustomSettingName = 0xD18D, + FujiStillCustomSettingImageSize = 0xD18E, + FujiStillCustomSettingImageQuality = 0xD18F, + FujiStillCustomSettingDynamicRange = 0xD190, + FujiStillCustomSettingDynamicRangePriority = 0xD191, + FujiStillCustomSettingFilmSimulation = 0xD192, + // TODO: 0xD193 All 0s + // TODO: 0xD194 All 0s + FujiStillCustomSettingGrainEffect = 0xD195, + FujiStillCustomSettingColorChromeEffect = 0xD196, + FujiStillCustomSettingColorChromeFXBlue = 0xD197, + // TODO: 0xD198 All 1s + FujiStillCustomSettingWhiteBalance = 0xD199, + FujiStillCustomSettingWhiteBalanceRed = 0xD19A, + FujiStillCustomSettingWhiteBalanceBlue = 0xD19B, + FujiStillCustomSettingWhiteBalanceTemperature = 0xD19C, + FujiStillCustomSettingHighlightTone = 0xD19D, + FujiStillCustomSettingShadowTone = 0xD19E, + FujiStillCustomSettingColor = 0xD19F, + FujiStillCustomSettingSharpness = 0xD1A0, + FujiStillCustomSettingHighISONR = 0xD1A1, + FujiStillCustomSettingClarity = 0xD1A2, + // TODO: 0xD1A3 All 1s + // TODO: 0xD1A4 All 1s + // TODO: 0xD1A5 All 7s + FujiBatteryInfo2 = 0xD36B, +} + +const SIMILARITY_THRESHOLD: usize = 8; + +fn suggest_closest<'a, I, S>(input: &str, choices: I) -> Option<&'a str> +where + I: IntoIterator, + S: AsRef + '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 = strsim::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 + } +} + +#[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, +} + +impl fmt::Display for FujiCustomSetting { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::C1 => write!(f, "C1"), + Self::C2 => write!(f, "C2"), + Self::C3 => write!(f, "C3"), + Self::C4 => write!(f, "C4"), + Self::C5 => write!(f, "C5"), + Self::C6 => write!(f, "C6"), + Self::C7 => write!(f, "C7"), + } + } +} + +impl Serialize for FujiCustomSetting { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_u16(*self as u16) + } +} + +impl FromStr for FujiCustomSetting { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + 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) + } +} + +#[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, +} + +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 Serialize for FujiImageSize { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +impl FromStr for FujiImageSize { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s.trim().to_lowercase(); + + match input.as_str() { + "max" | "maximum" | "full" | "largest" => return Ok(Self::R7728x5152), + _ => {} + } + + let resolution_input = s.replace(' ', "x").replace("by", "x"); + if let Some((w_str, h_str)) = resolution_input.split_once('x') + && let (Ok(w), Ok(h)) = (w_str.trim().parse::(), h_str.trim().parse::()) + { + 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 = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_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'."); + } +} + +#[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, +} + +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 { + 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 = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_closest(s, &choices) { + bail!("Unknown image quality '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown image quality '{s}'"); + } +} + +#[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, +} + +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 { + 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 = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_closest(s, &choices) { + bail!("Unknown dynamic range '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown dynamic range '{s}'"); + } +} + +#[repr(u16)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + IntoPrimitive, + TryFromPrimitive, + PtpSerialize, + PtpDeserialize, + EnumIter, +)] +pub enum FujiStillDynamicRangePriority { + Auto = 0x8000, + Strong = 0x2, + Weak = 0x1, + Off = 0x0, +} + +impl fmt::Display for FujiStillDynamicRangePriority { + 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 FujiStillDynamicRangePriority { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + 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 = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_closest(s, &choices) { + bail!("Unknown dynamic range priority '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown dynamic range priority '{s}'"); + } +} + +#[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, +} + +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 { + 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 = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_closest(s, &choices) { + bail!("Unknown value '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown value '{input}'"); + } +} + +#[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, +} + +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 { + let input = s + .trim() + .to_lowercase() + .replace(['+', '-', ',', ' '].as_ref(), ""); + + match input.as_str() { + "stronglarge" => return Ok(Self::StrongLarge), + "weaklarge" => return Ok(Self::WeakLarge), + "strongsmall" => return Ok(Self::StrongSmall), + "weaksmall" => return Ok(Self::WeakSmall), + "off" => return Ok(Self::Off), + _ => {} + } + + let choices: Vec = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_closest(&input, &choices) { + bail!("Unknown grain effect '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown grain effect '{s}'"); + } +} + +#[repr(u16)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + IntoPrimitive, + TryFromPrimitive, + PtpSerialize, + PtpDeserialize, + EnumIter, +)] +pub enum FujiColorChromeEffect { + Strong = 0x3, + Weak = 0x2, + Off = 0x1, +} + +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 { + 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 = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_closest(s, &choices) { + bail!("Unknown color chrome effect '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown color chrome effect '{s}'"); + } +} + +#[repr(u16)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + IntoPrimitive, + TryFromPrimitive, + PtpSerialize, + PtpDeserialize, + EnumIter, +)] +pub enum FujiColorChromeFXBlue { + Strong = 0x3, + Weak = 0x2, + Off = 0x1, +} + +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 { + 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 = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_closest(s, &choices) { + bail!("Unknown color chrome fx blue '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown color chrome fx blue '{s}'"); + } +} + +#[repr(u16)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + Serialize, + IntoPrimitive, + TryFromPrimitive, + PtpSerialize, + PtpDeserialize, + EnumIter, +)] +pub enum FujiWhiteBalance { + 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, +} + +impl fmt::Display for FujiWhiteBalance { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + 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 { + let input = s.trim().to_lowercase().replace(['-', ' '].as_ref(), ""); + + match input.as_str() { + "whitepriority" | "white" => return Ok(Self::WhitePriority), + "auto" => 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 = Self::iter().map(|v| v.to_string()).collect(); + if let Some(best) = suggest_closest(s, &choices) { + bail!("Unknown white balance '{s}'. Did you mean '{best}'?"); + } + + bail!("Unknown white balance '{s}'"); + } +} + +#[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, +} + +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 Serialize for FujiHighISONR { + fn serialize(&self, serializer: S) -> Result + 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 FromStr for FujiHighISONR { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + let input = s + .trim() + .parse::() + .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",), + } + } +} + +macro_rules! define_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: i16 = $min; + pub const MAX: i16 = $max; + pub const STEP: i16 = $step; + pub const SCALE: f32 = $scale; + } + + 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 for $name { + type Error = anyhow::Error; + + fn try_from(value: i16) -> anyhow::Result { + if !(Self::MIN..=Self::MAX).contains(&value) { + anyhow::bail!("Value {} is out of range", value); + } + #[allow(clippy::modulo_one)] + if (value - Self::MIN) % Self::STEP != 0 { + anyhow::bail!("Value {} is not aligned to step {}", value, Self::STEP); + } + Ok(Self(value)) + } + } + + impl std::fmt::Display for $name { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if (Self::SCALE - 1.0).abs() < f32::EPSILON { + write!(f, "{}", self.0) + } else { + let val = f32::from(self.0) * Self::SCALE; + if val.fract().abs() < f32::EPSILON { + #[allow(clippy::cast_possible_truncation)] + let val = val as i32; + write!(f, "{}", val as i32) + } else { + write!(f, "{:.1}", val) + } + } + } + } + + impl std::str::FromStr for $name { + type Err = anyhow::Error; + + fn from_str(s: &str) -> anyhow::Result { + use anyhow::Context; + + let input = s + .trim() + .parse::() + .with_context(|| format!("Invalid numeric value '{s}'"))?; + + #[allow(clippy::cast_possible_truncation)] + let raw = (input / Self::SCALE).round() as i16; + Self::try_from(raw) + } + } + + impl serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let val = f32::from(self.0) * Self::SCALE; + if (val.fract().abs() < f32::EPSILON) { + serializer.serialize_i32(val as i32) + } else { + serializer.serialize_f32(val) + } + } + } + }; +} + +define_fuji_i16!(FujiWhiteBalanceShift, -9, 9, 1, 1.0); +define_fuji_i16!(FujiWhiteBalanceTemperature, 2500, 10000, 10, 1.0); +define_fuji_i16!(FujiHighlightTone, -40, 20, 5, 0.1); +define_fuji_i16!(FujiShadowTone, -20, 40, 5, 0.1); +define_fuji_i16!(FujiColor, -40, 40, 10, 0.1); +define_fuji_i16!(FujiSharpness, -40, 40, 10, 0.1); +define_fuji_i16!(FujiClarity, -50, 50, 10, 0.1); + +#[repr(u16)] +#[derive( + Debug, Clone, Copy, Serialize, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpDeserialize, +)] +pub enum UsbMode { + RawConversion = 0x6, +} + +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}") + } +} + +#[derive( + Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive, PtpSerialize, PtpDeserialize, +)] +#[repr(u16)] +pub enum ContainerType { + Command = 1, + Data = 2, + Response = 3, + Event = 4, +} diff --git a/src/camera/ptp/mod.rs b/src/camera/ptp/mod.rs index c98769b..d8629bb 100644 --- a/src/camera/ptp/mod.rs +++ b/src/camera/ptp/mod.rs @@ -1,14 +1,13 @@ -pub mod enums; pub mod error; -pub mod read; +pub mod hex; pub mod structs; -use std::{cmp::min, time::Duration}; +use std::{cmp::min, io::Cursor, time::Duration}; use anyhow::bail; -use byteorder::{LittleEndian, WriteBytesExt}; -use enums::{CommandCode, ContainerCode, ContainerType, ResponseCode}; -use log::{debug, error, trace}; +use hex::{CommandCode, ContainerCode, ContainerType, ResponseCode}; +use log::{debug, error, trace, warn}; +use ptp_cursor::{PtpDeserialize, PtpSerialize}; use rusb::GlobalContext; use structs::ContainerInfo; @@ -27,61 +26,30 @@ impl Ptp { pub fn send( &mut self, code: CommandCode, - params: Option<&[u32]>, + params: &[u32], data: Option<&[u8]>, - transaction: bool, timeout: Duration, ) -> anyhow::Result> { - let (params, transaction_id) = self.prepare_send(params, transaction); + 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)?; } - self.receive_response(timeout) - } - - pub fn send_many( - &mut self, - code: CommandCode, - params: Option<&[u32]>, - data: Option<&[&[u8]]>, - transaction: bool, - timeout: Duration, - ) -> anyhow::Result> { - let (params, transaction_id) = self.prepare_send(params, transaction); - self.send_header(code, params, transaction_id, timeout)?; - if let Some(data) = data { - self.write_many(ContainerType::Data, code, data, transaction_id, timeout)?; - } - self.receive_response(timeout) - } - - fn prepare_send<'a>( - &mut self, - params: Option<&'a [u32]>, - transaction: bool, - ) -> (&'a [u32], Option) { - let params = params.unwrap_or_default(); - let transaction_id = if transaction { - let transaction_id = Some(self.transaction_id); - self.transaction_id += 1; - transaction_id - } else { - None - }; - (params, transaction_id) + let response = self.receive_response(timeout); + self.transaction_id += 1; + response } fn send_header( &self, code: CommandCode, params: &[u32], - transaction_id: Option, + transaction_id: u32, timeout: Duration, ) -> anyhow::Result<()> { let mut payload = Vec::with_capacity(params.len() * 4); for p in params { - payload.write_u32::(*p).ok(); + p.try_write_ptp(&mut payload)?; } trace!( @@ -113,10 +81,18 @@ impl Ptp { } 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 as u16)); + bail!(error::Error::Response(code.into())); } } @@ -127,7 +103,7 @@ impl Ptp { return Ok(response); } _ => { - debug!("Ignoring unexpected container type: {:?}", container.kind); + warn!("Unexpected container type: {:?}", container.kind); } } } @@ -138,13 +114,13 @@ impl Ptp { kind: ContainerType, code: CommandCode, payload: &[u8], - transaction_id: Option, + transaction_id: u32, timeout: Duration, ) -> anyhow::Result<()> { let container_info = ContainerInfo::new(kind, code, transaction_id, payload.len())?; - let mut buffer: Vec = container_info.try_into()?; + let mut buffer: Vec = container_info.try_into_ptp()?; - let first_chunk_len = min(payload.len(), self.chunk_size - container_info.len()); + let first_chunk_len = min(payload.len(), self.chunk_size - ContainerInfo::SIZE); buffer.extend_from_slice(&payload[..first_chunk_len]); trace!( @@ -165,62 +141,6 @@ impl Ptp { Ok(()) } - fn write_many( - &self, - kind: ContainerType, - code: CommandCode, - parts: &[&[u8]], - transaction_id: Option, - timeout: Duration, - ) -> anyhow::Result<()> { - if parts.is_empty() { - return self.write(kind, code, &[], transaction_id, timeout); - } - - if parts.len() == 1 { - return self.write(kind, code, parts[0], transaction_id, timeout); - } - - let total_len: usize = parts.iter().map(|c| c.len()).sum(); - let container_info = ContainerInfo::new(kind, code, transaction_id, total_len)?; - let mut buffer: Vec = container_info.try_into()?; - - let first = parts[0]; - let first_part_chunk_len = min(first.len(), self.chunk_size - container_info.len()); - buffer.extend_from_slice(&first[..first_part_chunk_len]); - - trace!( - "Writing PTP {kind:?} container, code: {code:?}, transaction: {transaction_id:?}, first payload part chunk ({first_part_chunk_len} bytes)", - ); - self.handle.write_bulk(self.bulk_out, &buffer, timeout)?; - - for chunk in first[first_part_chunk_len..].chunks(self.chunk_size) { - trace!( - "Writing additional payload part chunk ({} bytes)", - chunk.len(), - ); - self.handle.write_bulk(self.bulk_out, chunk, timeout)?; - } - - for part in &parts[1..] { - trace!("Writing additional payload part"); - for chunk in part.chunks(self.chunk_size) { - trace!( - "Writing additional payload part chunk ({} bytes)", - chunk.len(), - ); - self.handle.write_bulk(self.bulk_out, chunk, timeout)?; - } - trace!( - "Write completed for part, total payload of {} bytes", - part.len() - ); - } - - trace!("Write completed for code {code:?}, total payload of {total_len} bytes"); - Ok(()) - } - fn read(&self, timeout: Duration) -> anyhow::Result<(ContainerInfo, Vec)> { let mut stack_buf = [0u8; 8 * 1024]; @@ -230,7 +150,8 @@ impl Ptp { let buf = &stack_buf[..n]; trace!("Read chunk ({n} bytes)"); - let container_info = ContainerInfo::try_from(buf)?; + 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 { diff --git a/src/camera/ptp/read.rs b/src/camera/ptp/read.rs deleted file mode 100644 index 91fb729..0000000 --- a/src/camera/ptp/read.rs +++ /dev/null @@ -1,127 +0,0 @@ -#![allow(dead_code)] -#![allow(clippy::redundant_closure_for_method_calls)] - -use std::io::Cursor; - -use anyhow::bail; -use byteorder::{LittleEndian, ReadBytesExt}; - -pub trait Read: ReadBytesExt { - fn read_ptp_u8(&mut self) -> anyhow::Result { - Ok(self.read_u8()?) - } - - fn read_ptp_i8(&mut self) -> anyhow::Result { - Ok(self.read_i8()?) - } - - fn read_ptp_u16(&mut self) -> anyhow::Result { - Ok(self.read_u16::()?) - } - - fn read_ptp_i16(&mut self) -> anyhow::Result { - Ok(self.read_i16::()?) - } - - fn read_ptp_u32(&mut self) -> anyhow::Result { - Ok(self.read_u32::()?) - } - - fn read_ptp_i32(&mut self) -> anyhow::Result { - Ok(self.read_i32::()?) - } - - fn read_ptp_u64(&mut self) -> anyhow::Result { - Ok(self.read_u64::()?) - } - - fn read_ptp_i64(&mut self) -> anyhow::Result { - Ok(self.read_i64::()?) - } - - fn read_ptp_u128(&mut self) -> anyhow::Result { - Ok(self.read_u128::()?) - } - - fn read_ptp_i128(&mut self) -> anyhow::Result { - Ok(self.read_i128::()?) - } - - fn read_ptp_vec anyhow::Result>( - &mut self, - func: U, - ) -> anyhow::Result> { - let len = self.read_u32::()? as usize; - (0..len).map(|_| func(self)).collect() - } - - fn read_ptp_u8_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_u8()) - } - - fn read_ptp_i8_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_i8()) - } - - fn read_ptp_u16_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_u16()) - } - - fn read_ptp_i16_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_i16()) - } - - fn read_ptp_u32_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_u32()) - } - - fn read_ptp_i32_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_i32()) - } - - fn read_ptp_u64_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_u64()) - } - - fn read_ptp_i64_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_i64()) - } - - fn read_ptp_u128_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_u128()) - } - - fn read_ptp_i128_vec(&mut self) -> anyhow::Result> { - self.read_ptp_vec(|cur| cur.read_ptp_i128()) - } - - fn read_ptp_str(&mut self) -> anyhow::Result { - let len = self.read_u8()?; - if len > 0 { - let data: Vec = (0..(len - 1)) - .map(|_| self.read_u16::()) - .collect::>()?; - self.read_u16::()?; - Ok(String::from_utf16(&data)?) - } else { - Ok(String::new()) - } - } - - fn expect_end(&mut self) -> anyhow::Result<()>; -} - -impl> Read for Cursor { - fn expect_end(&mut self) -> anyhow::Result<()> { - let len = self.get_ref().as_ref().len(); - if len as u64 != self.position() { - bail!(super::error::Error::Malformed(format!( - "Response {} bytes, expected {} bytes", - len, - self.position() - ))) - } - - Ok(()) - } -} diff --git a/src/camera/ptp/structs.rs b/src/camera/ptp/structs.rs index 20eeaa7..9c5a1b1 100644 --- a/src/camera/ptp/structs.rs +++ b/src/camera/ptp/structs.rs @@ -1,14 +1,9 @@ -use std::io::Cursor; +use ptp_macro::{PtpDeserialize, PtpSerialize}; -use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt}; - -use super::{ - enums::{CommandCode, ContainerCode, ContainerType}, - read::Read, -}; +use super::hex::{CommandCode, ContainerCode, ContainerType}; #[allow(dead_code)] -#[derive(Debug)] +#[derive(Debug, PtpSerialize, PtpDeserialize)] pub struct DeviceInfo { pub version: u16, pub vendor_ex_id: u32, @@ -26,87 +21,26 @@ pub struct DeviceInfo { pub serial_number: String, } -impl TryFrom<&[u8]> for DeviceInfo { - type Error = anyhow::Error; - - fn try_from(buf: &[u8]) -> Result { - let mut cur = Cursor::new(buf); - - Ok(Self { - version: cur.read_ptp_u16()?, - vendor_ex_id: cur.read_ptp_u32()?, - vendor_ex_version: cur.read_ptp_u16()?, - vendor_extension_desc: cur.read_ptp_str()?, - functional_mode: cur.read_ptp_u16()?, - operations_supported: cur.read_ptp_u16_vec()?, - events_supported: cur.read_ptp_u16_vec()?, - device_properties_supported: cur.read_ptp_u16_vec()?, - capture_formats: cur.read_ptp_u16_vec()?, - image_formats: cur.read_ptp_u16_vec()?, - manufacturer: cur.read_ptp_str()?, - model: cur.read_ptp_str()?, - device_version: cur.read_ptp_str()?, - serial_number: cur.read_ptp_str()?, - }) - } -} - -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PtpSerialize, PtpDeserialize)] pub struct ContainerInfo { pub total_len: u32, pub kind: ContainerType, pub code: ContainerCode, - pub transaction_id: Option, + pub transaction_id: u32, } impl ContainerInfo { - const BASE_SIZE: usize = size_of::() + size_of::() + size_of::(); - pub const SIZE: usize = Self::BASE_SIZE + size_of::(); + pub const SIZE: usize = + size_of::() + size_of::() + size_of::() + size_of::(); pub fn new( kind: ContainerType, code: CommandCode, - transaction_id: Option, + transaction_id: u32, payload_len: usize, ) -> anyhow::Result { - let mut total_len = if transaction_id.is_some() { - Self::SIZE - } else { - Self::BASE_SIZE - }; - total_len += payload_len; - - Ok(Self { - total_len: u32::try_from(total_len)?, - kind, - code: ContainerCode::Command(code), - transaction_id, - }) - } - - pub const fn len(&self) -> usize { - if self.transaction_id.is_some() { - Self::SIZE - } else { - Self::BASE_SIZE - } - } - - pub const fn payload_len(&self) -> usize { - self.total_len as usize - self.len() - } -} - -impl TryFrom<&[u8]> for ContainerInfo { - type Error = anyhow::Error; - - fn try_from(bytes: &[u8]) -> Result { - let mut r = Cursor::new(bytes); - - let total_len = r.read_u32::()?; - let kind = ContainerType::try_from(r.read_u16::()?)?; - let code = ContainerCode::try_from(r.read_u16::()?)?; - let transaction_id = Some(r.read_u32::()?); + let total_len = u32::try_from(Self::SIZE + payload_len)?; + let code = ContainerCode::Command(code); Ok(Self { total_len, @@ -115,19 +49,8 @@ impl TryFrom<&[u8]> for ContainerInfo { transaction_id, }) } -} -impl TryFrom for Vec { - type Error = anyhow::Error; - - fn try_from(val: ContainerInfo) -> Result { - let mut buf = Self::with_capacity(val.len()); - buf.write_u32::(val.total_len)?; - buf.write_u16::(val.kind as u16)?; - buf.write_u16::(val.code.into())?; - if let Some(transaction_id) = val.transaction_id { - buf.write_u32::(transaction_id)?; - } - Ok(buf) + pub const fn payload_len(&self) -> usize { + self.total_len as usize - Self::SIZE } } diff --git a/src/cli/common/film.rs b/src/cli/common/film.rs index 2562576..e7f48e7 100644 --- a/src/cli/common/film.rs +++ b/src/cli/common/film.rs @@ -1,4 +1,87 @@ use clap::Args; +use crate::camera::ptp::hex::{ + FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiDynamicRange, + FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, FujiImageQuality, + FujiImageSize, FujiShadowTone, FujiSharpness, FujiStillDynamicRangePriority, FujiWhiteBalance, + FujiWhiteBalanceShift, FujiWhiteBalanceTemperature, +}; + #[derive(Args, Debug)] -pub struct FilmSimulationOptions {} +pub struct FilmSimulationOptions { + /// The name of the slot + #[clap(long)] + pub name: Option, + + /// The Fujifilm film simulation to use + #[clap(long)] + pub simulation: Option, + + /// The output image resolution + #[clap(long, alias = "size")] + pub resolution: Option, + + /// The output image quality (JPEG compression level) + #[clap(long, value_parser)] + pub quality: Option, + + /// Highlight Tone + #[clap(long, value_parser)] + pub highlights: Option, + + /// Shadow Tone + #[clap(long, value_parser)] + pub shadows: Option, + + /// Color + #[clap(long, value_parser)] + pub color: Option, + + /// Sharpness + #[clap(long, value_parser)] + pub sharpness: Option, + + /// Clarity + #[clap(long, value_parser)] + pub clarity: Option, + + /// White Balance + #[clap(long, value_parser)] + pub white_balance: Option, + + /// White Balance Shift Red + #[clap(long, value_parser)] + pub wb_shift_red: Option, + + /// White Balance Shift Blue + #[clap(long, value_parser)] + pub wb_shift_blue: Option, + + /// White Balance Temperature (Only used if WB is set to 'Temperature') + #[clap(long, value_parser)] + pub wb_temperature: Option, + + /// Dynamic Range + #[clap(long, value_parser)] + pub dynamic_range: Option, + + /// Dynamic Range Priority + #[clap(long, value_parser)] + pub dr_priority: Option, + + /// High ISO Noise Reduction + #[clap(long, value_parser)] + pub noise_reduction: Option, + + /// Grain Effect + #[clap(long, value_parser)] + pub grain: Option, + + /// Color Chrome Effect + #[clap(long, value_parser)] + pub color_chrome_effect: Option, + + /// Color Chrome FX Blue + #[clap(long, value_parser)] + pub color_chrome_fx_blue: Option, +} diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs index f4d952e..e278fd9 100644 --- a/src/cli/device/mod.rs +++ b/src/cli/device/mod.rs @@ -4,7 +4,7 @@ use clap::Subcommand; use serde::Serialize; use crate::{ - camera::{Camera, ptp::enums::UsbMode}, + camera::{Camera, ptp::hex::UsbMode}, usb, }; @@ -20,6 +20,7 @@ pub enum DeviceCmd { } #[derive(Serialize)] +#[serde(rename_all = "camelCase")] pub struct CameraItemRepr { pub name: &'static str, pub usb_id: String, @@ -60,11 +61,11 @@ fn handle_list(json: bool) -> anyhow::Result<()> { } if cameras.is_empty() { - println!("No supported cameras connected."); + println!("No supported cameras connected"); return Ok(()); } - println!("Connected cameras:"); + println!("Connected Cameras:"); for d in cameras { println!("- {d}"); } @@ -73,6 +74,7 @@ fn handle_list(json: bool) -> anyhow::Result<()> { } #[derive(Serialize)] +#[serde(rename_all = "camelCase")] pub struct CameraRepr { #[serde(flatten)] pub device: CameraItemRepr, diff --git a/src/cli/simulation/mod.rs b/src/cli/simulation/mod.rs index edf0741..825d01b 100644 --- a/src/cli/simulation/mod.rs +++ b/src/cli/simulation/mod.rs @@ -1,8 +1,23 @@ +use std::fmt; + +use crate::{ + camera::ptp::hex::{ + FujiClarity, FujiColor, FujiColorChromeEffect, FujiColorChromeFXBlue, FujiCustomSetting, + FujiDynamicRange, FujiFilmSimulation, FujiGrainEffect, FujiHighISONR, FujiHighlightTone, + FujiImageQuality, FujiImageSize, FujiShadowTone, FujiSharpness, + FujiStillDynamicRangePriority, FujiWhiteBalance, FujiWhiteBalanceShift, + FujiWhiteBalanceTemperature, + }, + usb, +}; + use super::common::{ file::{Input, Output}, film::FilmSimulationOptions, }; use clap::Subcommand; +use serde::Serialize; +use strum::IntoEnumIterator; #[derive(Subcommand, Debug)] pub enum SimulationCmd { @@ -13,15 +28,15 @@ pub enum SimulationCmd { /// Get simulation #[command(alias = "g")] Get { - /// Simulation number or name - simulation: u8, + /// Simulation slot number + slot: FujiCustomSetting, }, /// Set simulation parameters #[command(alias = "s")] Set { - /// Simulation number or name - simulation: u8, + /// Simulation slot number + slot: FujiCustomSetting, #[command(flatten)] film_simulation_options: FilmSimulationOptions, @@ -30,8 +45,8 @@ pub enum SimulationCmd { /// Export simulation #[command(alias = "e")] Export { - /// Simulation number or name - simulation: u8, + /// Simulation slot number + slot: FujiCustomSetting, /// Output file (use '-' to write to stdout) output_file: Output, @@ -40,10 +55,166 @@ 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(Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CustomSettingRepr { + pub slot: FujiCustomSetting, + pub name: String, +} + +fn handle_list(json: bool, device_id: Option<&str>) -> anyhow::Result<()> { + let mut camera = usb::get_camera(device_id)?; + + let mut slots = Vec::new(); + + for slot in FujiCustomSetting::iter() { + camera.set_active_custom_setting(slot)?; + let name = camera.get_custom_setting_name()?; + slots.push(CustomSettingRepr { slot, name }); + } + + if json { + println!("{}", serde_json::to_string_pretty(&slots)?); + } else { + println!("Film Simulations:"); + for slot in slots { + println!("- {}: {}", slot.slot, slot.name); + } + } + + Ok(()) +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FilmSimulationRepr { + pub slot: FujiCustomSetting, + pub name: String, + pub simulation: FujiFilmSimulation, + pub resolution: FujiImageSize, + pub quality: FujiImageQuality, + pub highlights: FujiHighlightTone, + pub shadows: FujiShadowTone, + pub color: FujiColor, + pub sharpness: FujiSharpness, + pub clarity: FujiClarity, + pub white_balance: FujiWhiteBalance, + pub wb_shift_red: FujiWhiteBalanceShift, + pub wb_shift_blue: FujiWhiteBalanceShift, + pub wb_temperature: FujiWhiteBalanceTemperature, + pub dynamic_range: FujiDynamicRange, + pub dr_priority: FujiStillDynamicRangePriority, + pub noise_reduction: FujiHighISONR, + pub grain: FujiGrainEffect, + pub color_chrome_effect: FujiColorChromeEffect, + pub color_chrome_fx_blue: FujiColorChromeFXBlue, +} + +impl fmt::Display for FilmSimulationRepr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Slot: {}", self.slot)?; + writeln!(f, "Name: {}", self.name)?; + writeln!(f, "Simulation: {}", self.simulation)?; + writeln!(f, "Resolution: {}", self.resolution)?; + writeln!(f, "Quality: {}", self.quality)?; + writeln!(f, "Highlights: {}", self.highlights)?; + writeln!(f, "Shadows: {}", self.shadows)?; + writeln!(f, "Color: {}", self.color)?; + writeln!(f, "Sharpness: {}", self.sharpness)?; + writeln!(f, "Clarity: {}", self.clarity)?; + writeln!(f, "White Balance: {}", self.white_balance)?; + writeln!( + f, + "WB Shift (R/B): {} / {}", + self.wb_shift_red, self.wb_shift_blue + )?; + writeln!(f, "WB Temperature: {}K", self.wb_temperature)?; + writeln!(f, "Dynamic Range: {}", self.dynamic_range)?; + writeln!(f, "DR Priority: {}", self.dr_priority)?; + writeln!(f, "Noise Reduction: {}", self.noise_reduction)?; + writeln!(f, "Grain: {}", self.grain)?; + writeln!(f, "Color Chrome: {}", self.color_chrome_effect)?; + writeln!(f, "CC FX Blue: {}", self.color_chrome_fx_blue) + } +} + +fn handle_get(json: bool, device_id: Option<&str>, slot: FujiCustomSetting) -> anyhow::Result<()> { + let mut camera = usb::get_camera(device_id)?; + camera.set_active_custom_setting(slot)?; + + let repr = FilmSimulationRepr { + slot, + name: camera.get_custom_setting_name()?, + simulation: camera.get_film_simulation()?, + resolution: camera.get_image_size()?, + quality: camera.get_image_quality()?, + highlights: camera.get_highlight_tone()?, + shadows: camera.get_shadow_tone()?, + color: camera.get_color()?, + sharpness: camera.get_sharpness()?, + clarity: camera.get_clarity()?, + white_balance: camera.get_white_balance()?, + wb_shift_red: camera.get_wb_shift_red()?, + wb_shift_blue: camera.get_wb_shift_blue()?, + wb_temperature: camera.get_wb_temperature()?, + dynamic_range: camera.get_dynamic_range()?, + dr_priority: camera.get_dynamic_range_priority()?, + noise_reduction: camera.get_high_iso_nr()?, + grain: camera.get_grain_effect()?, + color_chrome_effect: camera.get_color_chrome_effect()?, + color_chrome_fx_blue: camera.get_color_chrome_fx_blue()?, + }; + + if json { + println!("{}", serde_json::to_string_pretty(&repr)?); + } else { + println!("{repr}"); + } + + Ok(()) +} + +fn handle_set( + _device_id: Option<&str>, + _slot: FujiCustomSetting, + _opts: &FilmSimulationOptions, +) -> anyhow::Result<()> { + todo!(); +} + +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, + film_simulation_options, + } => handle_set(device_id, slot, &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), + } +} diff --git a/src/main.rs b/src/main.rs index 94f5795..1a30421 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,7 +19,10 @@ fn main() -> anyhow::Result<()> { match cli.command { Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?, Commands::Backup(backup_cmd) => cli::backup::handle(backup_cmd, device_id)?, - _ => todo!(), + Commands::Simulation(simulation_cmd) => { + cli::simulation::handle(simulation_cmd, cli.json, device_id)?; + } + Commands::Render(_) => todo!(), } Ok(()) diff --git a/src/usb/mod.rs b/src/usb/mod.rs index 13b6b51..d6dd30b 100644 --- a/src/usb/mod.rs +++ b/src/usb/mod.rs @@ -38,6 +38,6 @@ pub fn get_camera(device_id: Option<&str>) -> anyhow::Result { None => get_connected_cameras()? .into_iter() .next() - .ok_or_else(|| anyhow!("No supported devices connected.")), + .ok_or_else(|| anyhow!("No supported devices connected")), } }