Compare commits
3 Commits
sdk
...
1f26a91dcd
Author | SHA1 | Date | |
---|---|---|---|
1f26a91dcd
|
|||
943f22c074
|
|||
2e9fb61762
|
17
Cargo.lock
generated
17
Cargo.lock
generated
@@ -237,13 +237,11 @@ name = "fujicli"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"bitflags",
|
"byteorder",
|
||||||
"clap",
|
"clap",
|
||||||
"libc",
|
|
||||||
"libptp",
|
|
||||||
"log",
|
"log",
|
||||||
"log4rs",
|
"log4rs",
|
||||||
"once_cell",
|
"rusb",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
@@ -340,17 +338,6 @@ version = "0.2.177"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
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]]
|
[[package]]
|
||||||
name = "libusb1-sys"
|
name = "libusb1-sys"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@@ -6,7 +6,6 @@ description = "A CLI to manage Fujifilm devices, simulations, backups, and rende
|
|||||||
authors = [
|
authors = [
|
||||||
"Nikolaos Karaolidis <nick@karaolidis.com>",
|
"Nikolaos Karaolidis <nick@karaolidis.com>",
|
||||||
]
|
]
|
||||||
build = "build.rs"
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
panic = 'abort'
|
panic = 'abort'
|
||||||
@@ -16,12 +15,10 @@ codegen-units = 1
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
bitflags = "2.9.4"
|
byteorder = "1.5.0"
|
||||||
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
|
clap = { version = "4.5.48", features = ["derive", "wrap_help"] }
|
||||||
libc = "0.2.177"
|
|
||||||
libptp = "0.6.5"
|
|
||||||
log = "0.4.28"
|
log = "0.4.28"
|
||||||
log4rs = "1.4.0"
|
log4rs = "1.4.0"
|
||||||
once_cell = "1.21.3"
|
rusb = "0.9.4"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
|
12
build.rs
12
build.rs
@@ -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");
|
|
||||||
}
|
|
50
flake.nix
50
flake.nix
@@ -32,50 +32,6 @@
|
|||||||
|
|
||||||
src = ./.;
|
src = ./.;
|
||||||
cargoLock.lockFile = ./Cargo.lock;
|
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,17 @@
|
|||||||
clippy
|
clippy
|
||||||
cargo-udeps
|
cargo-udeps
|
||||||
cargo-outdated
|
cargo-outdated
|
||||||
fujifilm-sdk-bin
|
|
||||||
stdenv.cc.cc.lib
|
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
TOP="$(git rev-parse --show-toplevel)"
|
TOP="$(git rev-parse --show-toplevel)"
|
||||||
export CARGO_HOME="$TOP/.cargo"
|
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; {
|
packages.${system} = with pkgs; {
|
||||||
default = fujicli;
|
default = fujicli;
|
||||||
inherit fujicli fujicli-debug fujifilm-sdk-bin;
|
inherit fujicli;
|
||||||
};
|
};
|
||||||
|
|
||||||
formatter.${system} = treefmt.config.build.wrapper;
|
formatter.${system} = treefmt.config.build.wrapper;
|
||||||
|
63
src/camera/devices/mod.rs
Normal file
63
src/camera/devices/mod.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
use anyhow::bail;
|
||||||
|
use rusb::GlobalContext;
|
||||||
|
|
||||||
|
use super::CameraImpl;
|
||||||
|
|
||||||
|
type ImplFactory<P> = fn() -> Box<dyn CameraImpl<P>>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct SupportedCamera<P: rusb::UsbContext> {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub vendor: u16,
|
||||||
|
pub product: u16,
|
||||||
|
pub impl_factory: ImplFactory<P>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<P: rusb::UsbContext> SupportedCamera<P> {
|
||||||
|
pub fn new_camera(&self, device: &rusb::Device<P>) -> anyhow::Result<Box<dyn CameraImpl<P>>> {
|
||||||
|
let descriptor = device.device_descriptor()?;
|
||||||
|
|
||||||
|
let matches =
|
||||||
|
descriptor.vendor_id() == self.vendor && descriptor.product_id() == self.product;
|
||||||
|
|
||||||
|
if !matches {
|
||||||
|
bail!(
|
||||||
|
"Device with vendor {:04x} and product {:04x} does not match {}",
|
||||||
|
descriptor.vendor_id(),
|
||||||
|
descriptor.product_id(),
|
||||||
|
self.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((self.impl_factory)())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! default_camera_impl {
|
||||||
|
(
|
||||||
|
$const_name:ident,
|
||||||
|
$struct_name:ident,
|
||||||
|
$vendor:expr,
|
||||||
|
$product:expr,
|
||||||
|
$display_name:expr
|
||||||
|
) => {
|
||||||
|
pub const $const_name: SupportedCamera<GlobalContext> = SupportedCamera {
|
||||||
|
name: $display_name,
|
||||||
|
vendor: $vendor,
|
||||||
|
product: $product,
|
||||||
|
impl_factory: || Box::new($struct_name {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct $struct_name {}
|
||||||
|
|
||||||
|
impl crate::camera::CameraImpl<GlobalContext> for $struct_name {
|
||||||
|
fn supported_camera(&self) -> &'static SupportedCamera<GlobalContext> {
|
||||||
|
&$const_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5");
|
||||||
|
|
||||||
|
pub const SUPPORTED: &[SupportedCamera<GlobalContext>] = &[FUJIFILM_XT5];
|
13
src/camera/error.rs
Normal file
13
src/camera/error.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct UnsupportedFeatureError;
|
||||||
|
|
||||||
|
impl fmt::Display for UnsupportedFeatureError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "feature is not supported for this device")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for UnsupportedFeatureError {}
|
447
src/camera/mod.rs
Normal file
447
src/camera/mod.rs
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
pub mod devices;
|
||||||
|
pub mod error;
|
||||||
|
pub mod ptp;
|
||||||
|
|
||||||
|
use std::{cmp::min, io::Cursor, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
use byteorder::{LittleEndian, WriteBytesExt};
|
||||||
|
use devices::SupportedCamera;
|
||||||
|
use log::{debug, error, trace};
|
||||||
|
use ptp::{
|
||||||
|
enums::{CommandCode, ContainerType, PropCode, ResponseCode, UsbMode},
|
||||||
|
structs::{ContainerInfo, DeviceInfo},
|
||||||
|
};
|
||||||
|
use rusb::{GlobalContext, constants::LIBUSB_CLASS_IMAGE};
|
||||||
|
|
||||||
|
const SESSION: u32 = 1;
|
||||||
|
|
||||||
|
pub struct Usb {
|
||||||
|
bus: u8,
|
||||||
|
address: u8,
|
||||||
|
interface: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Ptp {
|
||||||
|
bulk_in: u8,
|
||||||
|
bulk_out: u8,
|
||||||
|
handle: rusb::DeviceHandle<GlobalContext>,
|
||||||
|
transaction_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Camera {
|
||||||
|
pub r#impl: Box<dyn CameraImpl<GlobalContext>>,
|
||||||
|
usb: Usb,
|
||||||
|
pub ptp: Ptp,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Camera {
|
||||||
|
pub fn from_device(device: &rusb::Device<GlobalContext>) -> anyhow::Result<Self> {
|
||||||
|
for supported_camera in devices::SUPPORTED {
|
||||||
|
if let Ok(r#impl) = supported_camera.new_camera(device) {
|
||||||
|
let bus = device.bus_number();
|
||||||
|
let address = device.address();
|
||||||
|
|
||||||
|
let config_desc = device.active_config_descriptor()?;
|
||||||
|
|
||||||
|
let interface_descriptor = config_desc
|
||||||
|
.interfaces()
|
||||||
|
.flat_map(|i| i.descriptors())
|
||||||
|
.find(|x| x.class_code() == LIBUSB_CLASS_IMAGE)
|
||||||
|
.ok_or(rusb::Error::NotFound)?;
|
||||||
|
|
||||||
|
let interface = interface_descriptor.interface_number();
|
||||||
|
debug!("Found interface {interface}");
|
||||||
|
|
||||||
|
let usb = Usb {
|
||||||
|
bus,
|
||||||
|
address,
|
||||||
|
interface,
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = device.open()?;
|
||||||
|
handle.claim_interface(interface)?;
|
||||||
|
|
||||||
|
let bulk_in = Self::find_endpoint(
|
||||||
|
&interface_descriptor,
|
||||||
|
rusb::Direction::In,
|
||||||
|
rusb::TransferType::Bulk,
|
||||||
|
)?;
|
||||||
|
let bulk_out = Self::find_endpoint(
|
||||||
|
&interface_descriptor,
|
||||||
|
rusb::Direction::Out,
|
||||||
|
rusb::TransferType::Bulk,
|
||||||
|
)?;
|
||||||
|
let transaction_id = 0;
|
||||||
|
|
||||||
|
let mut ptp = Ptp {
|
||||||
|
bulk_in,
|
||||||
|
bulk_out,
|
||||||
|
handle,
|
||||||
|
transaction_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Opening session");
|
||||||
|
let () = r#impl.open_session(&mut ptp, SESSION)?;
|
||||||
|
debug!("Session opened");
|
||||||
|
|
||||||
|
return Ok(Self { r#impl, usb, ptp });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bail!("Device not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_endpoint(
|
||||||
|
interface_descriptor: &rusb::InterfaceDescriptor<'_>,
|
||||||
|
direction: rusb::Direction,
|
||||||
|
transfer_type: rusb::TransferType,
|
||||||
|
) -> Result<u8, rusb::Error> {
|
||||||
|
interface_descriptor
|
||||||
|
.endpoint_descriptors()
|
||||||
|
.find(|ep| ep.direction() == direction && ep.transfer_type() == transfer_type)
|
||||||
|
.map(|x| x.address())
|
||||||
|
.ok_or(rusb::Error::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &'static str {
|
||||||
|
self.r#impl.supported_camera().name
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn vendor_id(&self) -> u16 {
|
||||||
|
self.r#impl.supported_camera().vendor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn product_id(&self) -> u16 {
|
||||||
|
self.r#impl.supported_camera().product
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connected_usb_id(&self) -> String {
|
||||||
|
format!("{}.{}", self.usb.bus, self.usb.address)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prop_value_as_scalar(data: &[u8]) -> anyhow::Result<u32> {
|
||||||
|
let data = match data.len() {
|
||||||
|
1 => anyhow::Ok(u32::from(data[0])),
|
||||||
|
2 => anyhow::Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))),
|
||||||
|
4 => anyhow::Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])),
|
||||||
|
n => bail!("Cannot parse {n} bytes as scalar"),
|
||||||
|
}?;
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_info(&mut self) -> anyhow::Result<DeviceInfo> {
|
||||||
|
let info = self.r#impl.get_info(&mut self.ptp)?;
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_usb_mode(&mut self) -> anyhow::Result<UsbMode> {
|
||||||
|
let data = self
|
||||||
|
.r#impl
|
||||||
|
.get_prop_value(&mut self.ptp, PropCode::FujiUsbMode);
|
||||||
|
|
||||||
|
let result = Self::prop_value_as_scalar(&data?)?.into();
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_battery_info(&mut self) -> anyhow::Result<u32> {
|
||||||
|
let data = self
|
||||||
|
.r#impl
|
||||||
|
.get_prop_value(&mut self.ptp, PropCode::FujiBatteryInfo2);
|
||||||
|
|
||||||
|
let data = data?;
|
||||||
|
debug!("Raw battery data: {data:?}");
|
||||||
|
|
||||||
|
let utf16: Vec<u16> = data[1..]
|
||||||
|
.chunks(2)
|
||||||
|
.map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]]))
|
||||||
|
.take_while(|&c| c != 0)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let utf8_string = String::from_utf16(&utf16)?;
|
||||||
|
debug!("Decoded UTF-16 string: {utf8_string}");
|
||||||
|
|
||||||
|
let percentage: u32 = utf8_string
|
||||||
|
.split(',')
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to parse battery percentage"))?
|
||||||
|
.parse()?;
|
||||||
|
|
||||||
|
Ok(percentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn export_backup(&mut self) -> anyhow::Result<Vec<u8>> {
|
||||||
|
self.r#impl.export_backup(&mut self.ptp)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import_backup(&mut self, backup: &[u8]) -> anyhow::Result<()> {
|
||||||
|
self.r#impl.import_backup(&mut self.ptp, backup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for Camera {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
debug!("Closing session");
|
||||||
|
if let Err(e) = self.r#impl.close_session(&mut self.ptp, SESSION) {
|
||||||
|
error!("Error closing session: {e}");
|
||||||
|
}
|
||||||
|
debug!("Session closed");
|
||||||
|
|
||||||
|
debug!("Releasing interface");
|
||||||
|
if let Err(e) = self.ptp.handle.release_interface(self.usb.interface) {
|
||||||
|
error!("Error releasing interface: {e}");
|
||||||
|
}
|
||||||
|
debug!("Interface released");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait CameraImpl<P: rusb::UsbContext> {
|
||||||
|
fn supported_camera(&self) -> &'static SupportedCamera<P>;
|
||||||
|
|
||||||
|
fn timeout(&self) -> Option<Duration> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn chunk_size(&self) -> usize {
|
||||||
|
1024 * 1024
|
||||||
|
}
|
||||||
|
|
||||||
|
fn send(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
code: CommandCode,
|
||||||
|
params: Option<&[u32]>,
|
||||||
|
data: Option<&[u8]>,
|
||||||
|
transaction: bool,
|
||||||
|
) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let transaction_id = if transaction {
|
||||||
|
Some(ptp.transaction_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let params = params.unwrap_or_default();
|
||||||
|
|
||||||
|
let mut payload = Vec::with_capacity(params.len() * 4);
|
||||||
|
for p in params {
|
||||||
|
payload.write_u32::<LittleEndian>(*p).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Sending PTP command: {:?}, transaction: {:?}, parameters ({} bytes): {:x?}",
|
||||||
|
code,
|
||||||
|
transaction_id,
|
||||||
|
payload.len(),
|
||||||
|
payload,
|
||||||
|
);
|
||||||
|
self.write(ptp, ContainerType::Command, code, &payload, transaction_id)?;
|
||||||
|
|
||||||
|
if let Some(data) = data {
|
||||||
|
trace!("Sending PTP data: {} bytes", data.len());
|
||||||
|
self.write(ptp, ContainerType::Data, code, data, transaction_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut data_payload = Vec::new();
|
||||||
|
loop {
|
||||||
|
let (container, payload) = self.read(ptp)?;
|
||||||
|
match container.kind {
|
||||||
|
ContainerType::Data => {
|
||||||
|
trace!("Data received: {} bytes", payload.len());
|
||||||
|
data_payload = payload;
|
||||||
|
}
|
||||||
|
ContainerType::Response => {
|
||||||
|
trace!("Response received: code {:?}", container.code);
|
||||||
|
let code = ResponseCode::try_from(container.code)?;
|
||||||
|
if code != ResponseCode::Ok {
|
||||||
|
bail!(ptp::error::Error::Response(container.code));
|
||||||
|
}
|
||||||
|
trace!(
|
||||||
|
"Command {:?} completed successfully with data payload of {} bytes",
|
||||||
|
code,
|
||||||
|
data_payload.len(),
|
||||||
|
);
|
||||||
|
return Ok(data_payload);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
debug!("Ignoring unexpected container type: {:?}", container.kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(
|
||||||
|
&self,
|
||||||
|
ptp: &mut Ptp,
|
||||||
|
kind: ContainerType,
|
||||||
|
code: CommandCode,
|
||||||
|
payload: &[u8],
|
||||||
|
// Fuji, for the love of God don't ever write code again.
|
||||||
|
transaction_id: Option<u32>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
// Look at what you made me do. Fuck.
|
||||||
|
let header_len = ContainerInfo::SIZE
|
||||||
|
- if transaction_id.is_none() {
|
||||||
|
size_of::<u32>()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let first_chunk_len = min(payload.len(), self.chunk_size() - header_len);
|
||||||
|
let total_len = u32::try_from(payload.len() + header_len)?;
|
||||||
|
|
||||||
|
let mut buffer = Vec::with_capacity(first_chunk_len + header_len);
|
||||||
|
buffer.write_u32::<LittleEndian>(total_len)?;
|
||||||
|
buffer.write_u16::<LittleEndian>(kind as u16)?;
|
||||||
|
buffer.write_u16::<LittleEndian>(code as u16)?;
|
||||||
|
if let Some(transaction_id) = transaction_id {
|
||||||
|
buffer.write_u32::<LittleEndian>(transaction_id)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.extend_from_slice(&payload[..first_chunk_len]);
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Writing PTP {kind:?} container, code: {code:?}, transaction: {transaction_id:?}, first_chunk: {first_chunk_len} bytes",
|
||||||
|
);
|
||||||
|
|
||||||
|
let timeout = self.timeout().unwrap_or_default();
|
||||||
|
ptp.handle.write_bulk(ptp.bulk_out, &buffer, timeout)?;
|
||||||
|
|
||||||
|
for chunk in payload[first_chunk_len..].chunks(self.chunk_size()) {
|
||||||
|
trace!("Writing additional chunk ({} bytes)", chunk.len(),);
|
||||||
|
ptp.handle.write_bulk(ptp.bulk_out, chunk, timeout)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Write completed for code {:?}, total payload of {} bytes",
|
||||||
|
code,
|
||||||
|
payload.len()
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read(&self, ptp: &mut Ptp) -> anyhow::Result<(ContainerInfo, Vec<u8>)> {
|
||||||
|
let timeout = self.timeout().unwrap_or_default();
|
||||||
|
|
||||||
|
let mut stack_buf = [0u8; 8 * 1024];
|
||||||
|
let n = ptp.handle.read_bulk(ptp.bulk_in, &mut stack_buf, timeout)?;
|
||||||
|
let buf = &stack_buf[..n];
|
||||||
|
|
||||||
|
trace!("Read {n} bytes from bulk_in");
|
||||||
|
|
||||||
|
let container_info = ContainerInfo::parse(buf)?;
|
||||||
|
if container_info.payload_len == 0 {
|
||||||
|
trace!("No payload in container");
|
||||||
|
return Ok((container_info, Vec::new()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload_len = container_info.payload_len as usize;
|
||||||
|
let mut payload = Vec::with_capacity(payload_len);
|
||||||
|
if buf.len() > ContainerInfo::SIZE {
|
||||||
|
payload.extend_from_slice(&buf[ContainerInfo::SIZE..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
while payload.len() < payload_len {
|
||||||
|
let remaining = payload_len - payload.len();
|
||||||
|
let mut chunk = vec![0u8; min(remaining, self.chunk_size())];
|
||||||
|
let n = ptp.handle.read_bulk(ptp.bulk_in, &mut chunk, timeout)?;
|
||||||
|
trace!("Read additional chunk ({n} bytes)");
|
||||||
|
if n == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
payload.extend_from_slice(&chunk[..n]);
|
||||||
|
}
|
||||||
|
|
||||||
|
trace!(
|
||||||
|
"Finished reading container, total payload of {} bytes",
|
||||||
|
payload.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok((container_info, payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_session(&self, ptp: &mut Ptp, session_id: u32) -> anyhow::Result<()> {
|
||||||
|
debug!("Sending OpenSession command");
|
||||||
|
_ = self.send(
|
||||||
|
ptp,
|
||||||
|
CommandCode::OpenSession,
|
||||||
|
Some(&[session_id]),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_session(&self, ptp: &mut Ptp, _: u32) -> anyhow::Result<()> {
|
||||||
|
debug!("Sending CloseSession command");
|
||||||
|
_ = self.send(ptp, CommandCode::CloseSession, None, None, true)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_info(&self, ptp: &mut Ptp) -> anyhow::Result<DeviceInfo> {
|
||||||
|
debug!("Sending GetDeviceInfo command");
|
||||||
|
let response = self.send(ptp, CommandCode::GetDeviceInfo, None, None, true)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
let info = DeviceInfo::try_from(response.as_slice())?;
|
||||||
|
Ok(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_prop_value(&self, ptp: &mut Ptp, prop: PropCode) -> anyhow::Result<Vec<u8>> {
|
||||||
|
debug!("Sending GetDevicePropValue command for property {prop:?}");
|
||||||
|
let response = self.send(
|
||||||
|
ptp,
|
||||||
|
CommandCode::GetDevicePropValue,
|
||||||
|
Some(&[prop as u32]),
|
||||||
|
None,
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn export_backup(&self, ptp: &mut Ptp) -> anyhow::Result<Vec<u8>> {
|
||||||
|
const HANDLE: u32 = 0x0;
|
||||||
|
|
||||||
|
debug!("Sending GetObjectInfo command for backup");
|
||||||
|
let response = self.send(ptp, CommandCode::GetObjectInfo, Some(&[HANDLE]), None, true)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
debug!("Sending GetObject command for backup");
|
||||||
|
let response = self.send(ptp, CommandCode::GetObject, Some(&[HANDLE]), None, true)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_backup(&self, ptp: &mut Ptp, buffer: &[u8]) -> anyhow::Result<()> {
|
||||||
|
debug!("Preparing ObjectInfo header for backup");
|
||||||
|
let mut obj_info = vec![0u8; 1088];
|
||||||
|
let mut cursor = Cursor::new(&mut obj_info[..]);
|
||||||
|
cursor.write_u32::<LittleEndian>(0x0)?;
|
||||||
|
cursor.write_u16::<LittleEndian>(0x5000)?;
|
||||||
|
cursor.write_u16::<LittleEndian>(0x0)?;
|
||||||
|
cursor.write_u32::<LittleEndian>(u32::try_from(buffer.len())?)?;
|
||||||
|
|
||||||
|
debug!("Sending SendObjectInfo command for backup");
|
||||||
|
let response = self.send(
|
||||||
|
ptp,
|
||||||
|
CommandCode::SendObjectInfo,
|
||||||
|
Some(&[0x0, 0x0]),
|
||||||
|
Some(&obj_info),
|
||||||
|
true,
|
||||||
|
)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
debug!("Sending SendObject command for backup");
|
||||||
|
let response = self.send(
|
||||||
|
ptp,
|
||||||
|
CommandCode::SendObject,
|
||||||
|
Some(&[0x0]),
|
||||||
|
Some(buffer),
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
173
src/camera/ptp/enums.rs
Normal file
173
src/camera/ptp/enums.rs
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CommandCode {
|
||||||
|
GetDeviceInfo = 0x1001,
|
||||||
|
OpenSession = 0x1002,
|
||||||
|
CloseSession = 0x1003,
|
||||||
|
GetObjectInfo = 0x1008,
|
||||||
|
GetObject = 0x1009,
|
||||||
|
SendObjectInfo = 0x100C,
|
||||||
|
SendObject = 0x100D,
|
||||||
|
GetDevicePropValue = 0x1015,
|
||||||
|
SetDevicePropValue = 0x1016,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u16> for CommandCode {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0x1001 => Ok(Self::GetDeviceInfo),
|
||||||
|
0x1002 => Ok(Self::OpenSession),
|
||||||
|
0x1003 => Ok(Self::CloseSession),
|
||||||
|
0x1008 => Ok(Self::GetObjectInfo),
|
||||||
|
0x1009 => Ok(Self::GetObject),
|
||||||
|
0x100C => Ok(Self::SendObjectInfo),
|
||||||
|
0x100D => Ok(Self::SendObject),
|
||||||
|
0x1015 => Ok(Self::GetDevicePropValue),
|
||||||
|
0x1016 => Ok(Self::SetDevicePropValue),
|
||||||
|
v => bail!("Unknown command code '{v}'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u16)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ResponseCode {
|
||||||
|
Undefined = 0x2000,
|
||||||
|
Ok = 0x2001,
|
||||||
|
GeneralError = 0x2002,
|
||||||
|
SessionNotOpen = 0x2003,
|
||||||
|
InvalidTransactionId = 0x2004,
|
||||||
|
OperationNotSupported = 0x2005,
|
||||||
|
ParameterNotSupported = 0x2006,
|
||||||
|
IncompleteTransfer = 0x2007,
|
||||||
|
InvalidStorageId = 0x2008,
|
||||||
|
InvalidObjectHandle = 0x2009,
|
||||||
|
DevicePropNotSupported = 0x200A,
|
||||||
|
InvalidObjectFormatCode = 0x200B,
|
||||||
|
StoreFull = 0x200C,
|
||||||
|
ObjectWriteProtected = 0x200D,
|
||||||
|
StoreReadOnly = 0x200E,
|
||||||
|
AccessDenied = 0x200F,
|
||||||
|
NoThumbnailPresent = 0x2010,
|
||||||
|
SelfTestFailed = 0x2011,
|
||||||
|
PartialDeletion = 0x2012,
|
||||||
|
StoreNotAvailable = 0x2013,
|
||||||
|
SpecificationByFormatUnsupported = 0x2014,
|
||||||
|
NoValidObjectInfo = 0x2015,
|
||||||
|
InvalidCodeFormat = 0x2016,
|
||||||
|
UnknownVendorCode = 0x2017,
|
||||||
|
CaptureAlreadyTerminated = 0x2018,
|
||||||
|
DeviceBusy = 0x2019,
|
||||||
|
InvalidParentObject = 0x201A,
|
||||||
|
InvalidDevicePropFormat = 0x201B,
|
||||||
|
InvalidDevicePropValue = 0x201C,
|
||||||
|
InvalidParameter = 0x201D,
|
||||||
|
SessionAlreadyOpen = 0x201E,
|
||||||
|
TransactionCancelled = 0x201F,
|
||||||
|
SpecificationOfDestinationUnsupported = 0x2020,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::convert::TryFrom<u16> for ResponseCode {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
0x2000 => Ok(Self::Undefined),
|
||||||
|
0x2001 => Ok(Self::Ok),
|
||||||
|
0x2002 => Ok(Self::GeneralError),
|
||||||
|
0x2003 => Ok(Self::SessionNotOpen),
|
||||||
|
0x2004 => Ok(Self::InvalidTransactionId),
|
||||||
|
0x2005 => Ok(Self::OperationNotSupported),
|
||||||
|
0x2006 => Ok(Self::ParameterNotSupported),
|
||||||
|
0x2007 => Ok(Self::IncompleteTransfer),
|
||||||
|
0x2008 => Ok(Self::InvalidStorageId),
|
||||||
|
0x2009 => Ok(Self::InvalidObjectHandle),
|
||||||
|
0x200A => Ok(Self::DevicePropNotSupported),
|
||||||
|
0x200B => Ok(Self::InvalidObjectFormatCode),
|
||||||
|
0x200C => Ok(Self::StoreFull),
|
||||||
|
0x200D => Ok(Self::ObjectWriteProtected),
|
||||||
|
0x200E => Ok(Self::StoreReadOnly),
|
||||||
|
0x200F => Ok(Self::AccessDenied),
|
||||||
|
0x2010 => Ok(Self::NoThumbnailPresent),
|
||||||
|
0x2011 => Ok(Self::SelfTestFailed),
|
||||||
|
0x2012 => Ok(Self::PartialDeletion),
|
||||||
|
0x2013 => Ok(Self::StoreNotAvailable),
|
||||||
|
0x2014 => Ok(Self::SpecificationByFormatUnsupported),
|
||||||
|
0x2015 => Ok(Self::NoValidObjectInfo),
|
||||||
|
0x2016 => Ok(Self::InvalidCodeFormat),
|
||||||
|
0x2017 => Ok(Self::UnknownVendorCode),
|
||||||
|
0x2018 => Ok(Self::CaptureAlreadyTerminated),
|
||||||
|
0x2019 => Ok(Self::DeviceBusy),
|
||||||
|
0x201A => Ok(Self::InvalidParentObject),
|
||||||
|
0x201B => Ok(Self::InvalidDevicePropFormat),
|
||||||
|
0x201C => Ok(Self::InvalidDevicePropValue),
|
||||||
|
0x201D => Ok(Self::InvalidParameter),
|
||||||
|
0x201E => Ok(Self::SessionAlreadyOpen),
|
||||||
|
0x201F => Ok(Self::TransactionCancelled),
|
||||||
|
0x2020 => Ok(Self::SpecificationOfDestinationUnsupported),
|
||||||
|
v => bail!("Unknown response code '{v}'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u32)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum PropCode {
|
||||||
|
FujiUsbMode = 0xd16e,
|
||||||
|
FujiBatteryInfo2 = 0xD36B,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)]
|
||||||
|
pub enum UsbMode {
|
||||||
|
RawConversion,
|
||||||
|
Unsupported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<u32> for UsbMode {
|
||||||
|
fn from(val: u32) -> Self {
|
||||||
|
match val {
|
||||||
|
6 => Self::RawConversion,
|
||||||
|
_ => Self::Unsupported,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for UsbMode {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
let s = match self {
|
||||||
|
Self::RawConversion => "USB RAW CONV./BACKUP RESTORE",
|
||||||
|
Self::Unsupported => "Unsupported USB Mode",
|
||||||
|
};
|
||||||
|
write!(f, "{s}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
#[repr(u16)]
|
||||||
|
pub enum ContainerType {
|
||||||
|
Command = 1,
|
||||||
|
Data = 2,
|
||||||
|
Response = 3,
|
||||||
|
Event = 4,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<u16> for ContainerType {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||||
|
match value {
|
||||||
|
1 => Ok(Self::Command),
|
||||||
|
2 => Ok(Self::Data),
|
||||||
|
3 => Ok(Self::Response),
|
||||||
|
4 => Ok(Self::Event),
|
||||||
|
v => bail!("Invalid message type '{v}'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
53
src/camera/ptp/error.rs
Normal file
53
src/camera/ptp/error.rs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use crate::camera::ptp::enums::ResponseCode;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
Response(u16),
|
||||||
|
Malformed(String),
|
||||||
|
Usb(rusb::Error),
|
||||||
|
Io(io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
|
match *self {
|
||||||
|
Self::Response(r) => {
|
||||||
|
let name = ResponseCode::try_from(r)
|
||||||
|
.map_or_else(|_| "Unknown".to_string(), |c| format!("{c:?}"));
|
||||||
|
write!(f, "{name} (0x{r:04x})")
|
||||||
|
}
|
||||||
|
Self::Usb(ref e) => write!(f, "USB error: {e}"),
|
||||||
|
Self::Io(ref e) => write!(f, "IO error: {e}"),
|
||||||
|
Self::Malformed(ref e) => write!(f, "{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ::std::error::Error for Error {
|
||||||
|
fn cause(&self) -> Option<&dyn (::std::error::Error)> {
|
||||||
|
match *self {
|
||||||
|
Self::Usb(ref e) => Some(e),
|
||||||
|
Self::Io(ref e) => Some(e),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<rusb::Error> for Error {
|
||||||
|
fn from(e: rusb::Error) -> Self {
|
||||||
|
Self::Usb(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<io::Error> for Error {
|
||||||
|
fn from(e: io::Error) -> Self {
|
||||||
|
match e.kind() {
|
||||||
|
io::ErrorKind::UnexpectedEof => {
|
||||||
|
Self::Malformed("Unexpected end of message".to_string())
|
||||||
|
}
|
||||||
|
_ => Self::Io(e),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
src/camera/ptp/mod.rs
Normal file
4
src/camera/ptp/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pub mod enums;
|
||||||
|
pub mod error;
|
||||||
|
pub mod read;
|
||||||
|
pub mod structs;
|
127
src/camera/ptp/read.rs
Normal file
127
src/camera/ptp/read.rs
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
#![allow(clippy::redundant_closure_for_method_calls)]
|
||||||
|
|
||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use anyhow::bail;
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
|
||||||
|
pub trait Read: ReadBytesExt {
|
||||||
|
fn read_ptp_u8(&mut self) -> anyhow::Result<u8> {
|
||||||
|
Ok(self.read_u8()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i8(&mut self) -> anyhow::Result<i8> {
|
||||||
|
Ok(self.read_i8()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u16(&mut self) -> anyhow::Result<u16> {
|
||||||
|
Ok(self.read_u16::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i16(&mut self) -> anyhow::Result<i16> {
|
||||||
|
Ok(self.read_i16::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u32(&mut self) -> anyhow::Result<u32> {
|
||||||
|
Ok(self.read_u32::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i32(&mut self) -> anyhow::Result<i32> {
|
||||||
|
Ok(self.read_i32::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u64(&mut self) -> anyhow::Result<u64> {
|
||||||
|
Ok(self.read_u64::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i64(&mut self) -> anyhow::Result<i64> {
|
||||||
|
Ok(self.read_i64::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u128(&mut self) -> anyhow::Result<u128> {
|
||||||
|
Ok(self.read_u128::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i128(&mut self) -> anyhow::Result<i128> {
|
||||||
|
Ok(self.read_i128::<LittleEndian>()?)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_vec<T: Sized, U: Fn(&mut Self) -> anyhow::Result<T>>(
|
||||||
|
&mut self,
|
||||||
|
func: U,
|
||||||
|
) -> anyhow::Result<Vec<T>> {
|
||||||
|
let len = self.read_u32::<LittleEndian>()? as usize;
|
||||||
|
(0..len).map(|_| func(self)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u8_vec(&mut self) -> anyhow::Result<Vec<u8>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u8())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i8_vec(&mut self) -> anyhow::Result<Vec<i8>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i8())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u16_vec(&mut self) -> anyhow::Result<Vec<u16>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u16())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i16_vec(&mut self) -> anyhow::Result<Vec<i16>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i16())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u32_vec(&mut self) -> anyhow::Result<Vec<u32>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u32())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i32_vec(&mut self) -> anyhow::Result<Vec<i32>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i32())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u64_vec(&mut self) -> anyhow::Result<Vec<u64>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u64())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i64_vec(&mut self) -> anyhow::Result<Vec<i64>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i64())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_u128_vec(&mut self) -> anyhow::Result<Vec<u128>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_u128())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_i128_vec(&mut self) -> anyhow::Result<Vec<i128>> {
|
||||||
|
self.read_ptp_vec(|cur| cur.read_ptp_i128())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_ptp_str(&mut self) -> anyhow::Result<String> {
|
||||||
|
let len = self.read_u8()?;
|
||||||
|
if len > 0 {
|
||||||
|
let data: Vec<u16> = (0..(len - 1))
|
||||||
|
.map(|_| self.read_u16::<LittleEndian>())
|
||||||
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
self.read_u16::<LittleEndian>()?;
|
||||||
|
Ok(String::from_utf16(&data)?)
|
||||||
|
} else {
|
||||||
|
Ok(String::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn expect_end(&mut self) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: AsRef<[u8]>> Read for Cursor<T> {
|
||||||
|
fn expect_end(&mut self) -> anyhow::Result<()> {
|
||||||
|
let len = self.get_ref().as_ref().len();
|
||||||
|
if len as u64 != self.position() {
|
||||||
|
bail!(super::error::Error::Malformed(format!(
|
||||||
|
"Response {} bytes, expected {} bytes",
|
||||||
|
len,
|
||||||
|
self.position()
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
77
src/camera/ptp/structs.rs
Normal file
77
src/camera/ptp/structs.rs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
use std::io::Cursor;
|
||||||
|
|
||||||
|
use byteorder::{LittleEndian, ReadBytesExt};
|
||||||
|
|
||||||
|
use super::{enums::ContainerType, read::Read};
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DeviceInfo {
|
||||||
|
pub version: u16,
|
||||||
|
pub vendor_ex_id: u32,
|
||||||
|
pub vendor_ex_version: u16,
|
||||||
|
pub vendor_extension_desc: String,
|
||||||
|
pub functional_mode: u16,
|
||||||
|
pub operations_supported: Vec<u16>,
|
||||||
|
pub events_supported: Vec<u16>,
|
||||||
|
pub device_properties_supported: Vec<u16>,
|
||||||
|
pub capture_formats: Vec<u16>,
|
||||||
|
pub image_formats: Vec<u16>,
|
||||||
|
pub manufacturer: String,
|
||||||
|
pub model: String,
|
||||||
|
pub device_version: String,
|
||||||
|
pub serial_number: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&[u8]> for DeviceInfo {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn try_from(buf: &[u8]) -> Result<Self, Self::Error> {
|
||||||
|
let mut cur = Cursor::new(buf);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
version: cur.read_ptp_u16()?,
|
||||||
|
vendor_ex_id: cur.read_ptp_u32()?,
|
||||||
|
vendor_ex_version: cur.read_ptp_u16()?,
|
||||||
|
vendor_extension_desc: cur.read_ptp_str()?,
|
||||||
|
functional_mode: cur.read_ptp_u16()?,
|
||||||
|
operations_supported: cur.read_ptp_u16_vec()?,
|
||||||
|
events_supported: cur.read_ptp_u16_vec()?,
|
||||||
|
device_properties_supported: cur.read_ptp_u16_vec()?,
|
||||||
|
capture_formats: cur.read_ptp_u16_vec()?,
|
||||||
|
image_formats: cur.read_ptp_u16_vec()?,
|
||||||
|
manufacturer: cur.read_ptp_str()?,
|
||||||
|
model: cur.read_ptp_str()?,
|
||||||
|
device_version: cur.read_ptp_str()?,
|
||||||
|
serial_number: cur.read_ptp_str()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ContainerInfo {
|
||||||
|
pub kind: ContainerType,
|
||||||
|
pub payload_len: u32,
|
||||||
|
pub code: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerInfo {
|
||||||
|
pub const SIZE: usize =
|
||||||
|
size_of::<ContainerType>() + size_of::<u32>() + size_of::<u16>() + size_of::<u32>();
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerInfo {
|
||||||
|
pub fn parse<R: ReadBytesExt>(mut r: R) -> anyhow::Result<Self> {
|
||||||
|
let payload_len = r.read_u32::<LittleEndian>()? - Self::SIZE as u32;
|
||||||
|
let kind = r.read_u16::<LittleEndian>()?;
|
||||||
|
let kind = ContainerType::try_from(kind)?;
|
||||||
|
let code = r.read_u16::<LittleEndian>()?;
|
||||||
|
let _transaction_id = r.read_u32::<LittleEndian>()?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
kind,
|
||||||
|
payload_len,
|
||||||
|
code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
use crate::usb;
|
||||||
|
|
||||||
use super::common::file::{Input, Output};
|
use super::common::file::{Input, Output};
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
@@ -7,28 +9,41 @@ pub enum BackupCmd {
|
|||||||
#[command(alias = "e")]
|
#[command(alias = "e")]
|
||||||
Export {
|
Export {
|
||||||
/// Output file (use '-' to write to stdout)
|
/// Output file (use '-' to write to stdout)
|
||||||
output: Output,
|
output_file: Output,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Import backup
|
/// Import backup
|
||||||
#[command(alias = "i")]
|
#[command(alias = "i")]
|
||||||
Import {
|
Import {
|
||||||
/// Input file (use '-' to read from stdin)
|
/// Input file (use '-' to read from stdin)
|
||||||
input: Input,
|
input_file: Input,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
|
fn handle_export(device_id: Option<&str>, output: &Output) -> anyhow::Result<()> {
|
||||||
todo!()
|
let mut camera = usb::get_camera(device_id)?;
|
||||||
|
|
||||||
|
let mut writer = output.get_writer()?;
|
||||||
|
let backup = camera.export_backup()?;
|
||||||
|
writer.write_all(&backup)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> {
|
fn handle_import(device_id: Option<&str>, input: &Input) -> anyhow::Result<()> {
|
||||||
todo!()
|
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.import_backup(&backup)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> {
|
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> anyhow::Result<()> {
|
||||||
match cmd {
|
match cmd {
|
||||||
BackupCmd::Export { output } => handle_export(device_id, &output),
|
BackupCmd::Export { output_file } => handle_export(device_id, &output_file),
|
||||||
BackupCmd::Import { input } => handle_import(device_id, &input),
|
BackupCmd::Import { input_file } => handle_import(device_id, &input_file),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -18,7 +18,7 @@ impl FromStr for Input {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
match self {
|
||||||
Self::Stdin => Ok(Box::new(io::stdin())),
|
Self::Stdin => Ok(Box::new(io::stdin())),
|
||||||
Self::Path(path) => Ok(Box::new(File::open(path)?)),
|
Self::Path(path) => Ok(Box::new(File::open(path)?)),
|
||||||
@@ -44,7 +44,7 @@ impl FromStr for Output {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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 {
|
match self {
|
||||||
Self::Stdout => Ok(Box::new(io::stdout())),
|
Self::Stdout => Ok(Box::new(io::stdout())),
|
||||||
Self::Path(path) => Ok(Box::new(File::create(path)?)),
|
Self::Path(path) => Ok(Box::new(File::create(path)?)),
|
||||||
|
@@ -1,27 +1,4 @@
|
|||||||
use anyhow::bail;
|
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum SimulationSelector {
|
|
||||||
Slot(u8),
|
|
||||||
Name(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::str::FromStr for SimulationSelector {
|
|
||||||
type Err = anyhow::Error;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
if let Ok(slot) = s.parse::<u8>() {
|
|
||||||
return Ok(Self::Slot(slot));
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.is_empty() {
|
|
||||||
bail!("Simulation name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self::Name(s.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Args, Debug)]
|
#[derive(Args, Debug)]
|
||||||
pub struct FilmSimulationOptions {}
|
pub struct FilmSimulationOptions {}
|
||||||
|
@@ -1,6 +1,12 @@
|
|||||||
use clap::Subcommand;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::sdk::{XSDK, XSdkInterface};
|
use clap::Subcommand;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
camera::{Camera, ptp::enums::UsbMode},
|
||||||
|
usb,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone, Copy)]
|
#[derive(Subcommand, Debug, Clone, Copy)]
|
||||||
pub enum DeviceCmd {
|
pub enum DeviceCmd {
|
||||||
@@ -13,35 +19,119 @@ pub enum DeviceCmd {
|
|||||||
Info,
|
Info,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_list(json: bool) -> Result<(), anyhow::Error> {
|
#[derive(Serialize)]
|
||||||
let sdk = XSDK.lock().unwrap();
|
pub struct CameraItemRepr {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub usb_id: String,
|
||||||
|
pub vendor_id: String,
|
||||||
|
pub product_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
let cameras = sdk.get_cameras(XSdkInterface::Usb, None)?;
|
impl From<&Camera> for CameraItemRepr {
|
||||||
|
fn from(camera: &Camera) -> Self {
|
||||||
|
Self {
|
||||||
|
name: camera.name(),
|
||||||
|
usb_id: camera.connected_usb_id(),
|
||||||
|
vendor_id: format!("0x{:04x}", camera.vendor_id()),
|
||||||
|
product_id: format!("0x{:04x}", camera.product_id()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let valid_cameras: Vec<_> = cameras
|
impl fmt::Display for CameraItemRepr {
|
||||||
.into_iter()
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
.enumerate()
|
write!(
|
||||||
.filter(|(_, cam)| cam.valid)
|
f,
|
||||||
|
"{} ({}:{}) (USB ID: {})",
|
||||||
|
self.name, self.vendor_id, self.product_id, self.usb_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_list(json: bool) -> anyhow::Result<()> {
|
||||||
|
let cameras: Vec<CameraItemRepr> = usb::get_connected_cameras()?
|
||||||
|
.iter()
|
||||||
|
.map(std::convert::Into::into)
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if json {
|
if json {
|
||||||
println!("{}", serde_json::to_string_pretty(&valid_cameras)?);
|
println!("{}", serde_json::to_string_pretty(&cameras)?);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
for (id, cam) in valid_cameras {
|
if cameras.is_empty() {
|
||||||
println!("[{}] {}", id, cam);
|
println!("No supported cameras connected.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Connected cameras:");
|
||||||
|
for d in cameras {
|
||||||
|
println!("- {d}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
fn handle_info(json: bool, device: Option<u32>) -> Result<(), anyhow::Error> {
|
|
||||||
todo!()
|
#[derive(Serialize)]
|
||||||
|
pub struct CameraRepr {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub device: CameraItemRepr,
|
||||||
|
|
||||||
|
pub manufacturer: String,
|
||||||
|
pub model: String,
|
||||||
|
pub device_version: String,
|
||||||
|
pub serial_number: String,
|
||||||
|
pub mode: UsbMode,
|
||||||
|
pub battery: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(cmd: DeviceCmd, json: bool, device: Option<u32>) -> Result<(), anyhow::Error> {
|
impl fmt::Display for CameraRepr {
|
||||||
match cmd {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
DeviceCmd::List => handle_list(json),
|
writeln!(f, "Name: {}", self.device.name)?;
|
||||||
DeviceCmd::Info => handle_info(json, device),
|
writeln!(f, "USB ID: {}", self.device.usb_id)?;
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"Vendor ID: {}, Product ID: {}",
|
||||||
|
self.device.vendor_id, self.device.product_id
|
||||||
|
)?;
|
||||||
|
writeln!(f, "Manufacturer: {}", self.manufacturer)?;
|
||||||
|
writeln!(f, "Model: {}", self.model)?;
|
||||||
|
writeln!(f, "Version: {}", self.device_version)?;
|
||||||
|
writeln!(f, "Serial Number: {}", self.serial_number)?;
|
||||||
|
writeln!(f, "Mode: {}", self.mode)?;
|
||||||
|
write!(f, "Battery: {}%", self.battery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_info(json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
|
||||||
|
let mut camera = usb::get_camera(device_id)?;
|
||||||
|
|
||||||
|
let info = camera.get_info()?;
|
||||||
|
let mode = camera.get_usb_mode()?;
|
||||||
|
let battery = camera.get_battery_info()?;
|
||||||
|
|
||||||
|
let repr = CameraRepr {
|
||||||
|
device: (&camera).into(),
|
||||||
|
manufacturer: info.manufacturer.clone(),
|
||||||
|
model: info.model.clone(),
|
||||||
|
device_version: info.device_version.clone(),
|
||||||
|
serial_number: info.serial_number,
|
||||||
|
mode,
|
||||||
|
battery,
|
||||||
|
};
|
||||||
|
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&repr)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{repr}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(cmd: DeviceCmd, json: bool, device_id: Option<&str>) -> anyhow::Result<()> {
|
||||||
|
match cmd {
|
||||||
|
DeviceCmd::List => handle_list(json),
|
||||||
|
DeviceCmd::Info => handle_info(json, device_id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,7 +5,7 @@ pub mod device;
|
|||||||
pub mod render;
|
pub mod render;
|
||||||
pub mod simulation;
|
pub mod simulation;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{ArgAction, Parser, Subcommand};
|
||||||
|
|
||||||
use backup::BackupCmd;
|
use backup::BackupCmd;
|
||||||
use device::DeviceCmd;
|
use device::DeviceCmd;
|
||||||
@@ -23,17 +23,13 @@ pub struct Cli {
|
|||||||
#[arg(long, short = 'j', global = true)]
|
#[arg(long, short = 'j', global = true)]
|
||||||
pub json: bool,
|
pub json: bool,
|
||||||
|
|
||||||
/// Only log warnings and errors
|
/// Log extra debugging information (multiple instances increase verbosity)
|
||||||
#[arg(long, short = 'q', global = true, conflicts_with = "verbose")]
|
#[arg(long, short = 'v', action = ArgAction::Count, global = true)]
|
||||||
pub quiet: bool,
|
pub verbose: u8,
|
||||||
|
|
||||||
/// Log extra debugging information
|
|
||||||
#[arg(long, short = 'v', global = true, conflicts_with = "quiet")]
|
|
||||||
pub verbose: bool,
|
|
||||||
|
|
||||||
/// Manually specify target device
|
/// Manually specify target device
|
||||||
#[arg(long, short = 'd', global = true)]
|
#[arg(long, short = 'd', global = true)]
|
||||||
pub device: Option<u32>,
|
pub device: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
|
@@ -2,7 +2,7 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use super::common::{
|
use super::common::{
|
||||||
file::{Input, Output},
|
file::{Input, Output},
|
||||||
film::{FilmSimulationOptions, SimulationSelector},
|
film::FilmSimulationOptions,
|
||||||
};
|
};
|
||||||
use clap::Args;
|
use clap::Args;
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ use clap::Args;
|
|||||||
pub struct RenderCmd {
|
pub struct RenderCmd {
|
||||||
/// Simulation number or name
|
/// Simulation number or name
|
||||||
#[arg(long, conflicts_with = "simulation_file")]
|
#[arg(long, conflicts_with = "simulation_file")]
|
||||||
simulation: Option<SimulationSelector>,
|
simulation: Option<u8>,
|
||||||
|
|
||||||
/// Path to exported simulation
|
/// Path to exported simulation
|
||||||
#[arg(long, conflicts_with = "simulation")]
|
#[arg(long, conflicts_with = "simulation")]
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
use super::common::{
|
use super::common::{
|
||||||
file::{Input, Output},
|
file::{Input, Output},
|
||||||
film::{FilmSimulationOptions, SimulationSelector},
|
film::FilmSimulationOptions,
|
||||||
};
|
};
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
@@ -14,14 +14,14 @@ pub enum SimulationCmd {
|
|||||||
#[command(alias = "g")]
|
#[command(alias = "g")]
|
||||||
Get {
|
Get {
|
||||||
/// Simulation number or name
|
/// Simulation number or name
|
||||||
simulation: SimulationSelector,
|
simulation: u8,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Set simulation parameters
|
/// Set simulation parameters
|
||||||
#[command(alias = "s")]
|
#[command(alias = "s")]
|
||||||
Set {
|
Set {
|
||||||
/// Simulation number or name
|
/// Simulation number or name
|
||||||
simulation: SimulationSelector,
|
simulation: u8,
|
||||||
|
|
||||||
#[command(flatten)]
|
#[command(flatten)]
|
||||||
film_simulation_options: FilmSimulationOptions,
|
film_simulation_options: FilmSimulationOptions,
|
||||||
@@ -31,7 +31,7 @@ pub enum SimulationCmd {
|
|||||||
#[command(alias = "e")]
|
#[command(alias = "e")]
|
||||||
Export {
|
Export {
|
||||||
/// Simulation number or name
|
/// Simulation number or name
|
||||||
simulation: SimulationSelector,
|
simulation: u8,
|
||||||
|
|
||||||
/// Output file (use '-' to write to stdout)
|
/// Output file (use '-' to write to stdout)
|
||||||
output_file: Output,
|
output_file: Output,
|
||||||
|
13
src/log.rs
13
src/log.rs
@@ -6,13 +6,12 @@ use log4rs::{
|
|||||||
encode::pattern::PatternEncoder,
|
encode::pattern::PatternEncoder,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> {
|
pub fn init(verbose: u8) -> anyhow::Result<()> {
|
||||||
let level = if quiet {
|
let level = match verbose {
|
||||||
LevelFilter::Warn
|
0 => LevelFilter::Warn,
|
||||||
} else if verbose {
|
1 => LevelFilter::Info,
|
||||||
LevelFilter::Debug
|
2 => LevelFilter::Debug,
|
||||||
} else {
|
_ => LevelFilter::Trace,
|
||||||
LevelFilter::Info
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let encoder = Box::new(PatternEncoder::new("{d} {h({l})} {M}::{L} - {m}{n}"));
|
let encoder = Box::new(PatternEncoder::new("{d} {h({l})} {M}::{L} - {m}{n}"));
|
||||||
|
10
src/main.rs
10
src/main.rs
@@ -4,19 +4,21 @@
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use cli::Commands;
|
use cli::Commands;
|
||||||
|
|
||||||
|
mod camera;
|
||||||
mod cli;
|
mod cli;
|
||||||
mod log;
|
mod log;
|
||||||
mod sdk;
|
mod usb;
|
||||||
|
|
||||||
fn main() -> Result<(), anyhow::Error> {
|
fn main() -> anyhow::Result<()> {
|
||||||
let cli = cli::Cli::parse();
|
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 {
|
match cli.command {
|
||||||
Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?,
|
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!(),
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
143
src/sdk/error.rs
143
src/sdk/error.rs
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
404
src/sdk/mod.rs
404
src/sdk/mod.rs
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
43
src/usb/mod.rs
Normal file
43
src/usb/mod.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
use anyhow::{anyhow, bail};
|
||||||
|
|
||||||
|
use crate::camera::Camera;
|
||||||
|
|
||||||
|
pub fn get_connected_cameras() -> anyhow::Result<Vec<Camera>> {
|
||||||
|
let mut connected_cameras = Vec::new();
|
||||||
|
|
||||||
|
for device in rusb::devices()?.iter() {
|
||||||
|
if let Ok(camera) = Camera::from_device(&device) {
|
||||||
|
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::from_device(&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.")),
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user