diff --git a/Cargo.lock b/Cargo.lock index 65b6a81..393ad24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -237,11 +237,13 @@ name = "fujicli" version = "0.1.0" dependencies = [ "anyhow", + "bitflags", "clap", + "libc", "libptp", "log", "log4rs", - "rusb", + "once_cell", "serde", "serde_json", ] diff --git a/Cargo.toml b/Cargo.toml index 9f56185..0192e7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ description = "A CLI to manage Fujifilm devices, simulations, backups, and rende authors = [ "Nikolaos Karaolidis ", ] +build = "build.rs" [profile.release] panic = 'abort' @@ -15,10 +16,12 @@ codegen-units = 1 [dependencies] anyhow = "1.0.100" +bitflags = "2.9.4" clap = { version = "4.5.48", features = ["derive", "wrap_help"] } +libc = "0.2.177" libptp = "0.6.5" log = "0.4.28" log4rs = "1.4.0" -rusb = "0.9.4" +once_cell = "1.21.3" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..97a3aa7 --- /dev/null +++ b/build.rs @@ -0,0 +1,12 @@ +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"); +} diff --git a/flake.nix b/flake.nix index d2703eb..526fe1c 100755 --- a/flake.nix +++ b/flake.nix @@ -32,6 +32,50 @@ src = ./.; cargoLock.lockFile = ./Cargo.lock; + + nativeBuildInputs = with pkgs; [ + makeWrapper + fujifilm-sdk-bin + ]; + + XSDK_PATH = "${pkgs.fujifilm-sdk-bin}/lib"; + + postInstall = '' + wrapProgram $out/bin/fujicli \ + --prefix LD_LIBRARY_PATH : ${pkgs.fujifilm-sdk-bin}/lib \ + --prefix LD_LIBRARY_PATH : ${pkgs.stdenv.cc.cc.lib}/lib + ''; + }; + + fujifilm-sdk-bin = pkgs.stdenv.mkDerivation { + pname = "fujifilm-sdk-bin"; + version = "1.33.0"; + + src = pkgs.fetchzip { + url = "https://dl.fujifilm-x.com/global/special/camera-control-sdk/download/SDK13300.zip"; + sha256 = "sha256-sKWzRt174WoIRs1ZEO+F2kUfLCRx6cT4XxfxCOAP/R4="; + }; + + nativeBuildInputs = with pkgs; [ + coreutils + gnutar + ]; + + installPhase = '' + mkdir -p extract + tar -xzf $src/REDISTRIBUTABLES/Linux/Linux64PC.tar.gz -C extract + + mkdir -p $out/lib + cp extract/Linux64PC/* $out/lib/ + + soname=$(readelf -d "$out/lib/XAPI.so" | grep SONAME | awk -F'[][]' '{print $2}') + ln -sf $out/lib/XAPI.so $out/lib/libXAPI.so + ln -sf $out/lib/XAPI.so $out/lib/$soname + ''; + + meta = with pkgs.lib; { + license = licenses.unfree; + }; }; }; } @@ -59,17 +103,21 @@ clippy cargo-udeps cargo-outdated + fujifilm-sdk-bin + stdenv.cc.cc.lib ]; shellHook = '' TOP="$(git rev-parse --show-toplevel)" export CARGO_HOME="$TOP/.cargo" + export XSDK_PATH="${pkgs.fujifilm-sdk-bin}/lib" + export LD_LIBRARY_PATH="${pkgs.stdenv.cc.cc.lib}/lib:${pkgs.fujifilm-sdk-bin}/lib"; ''; }; packages.${system} = with pkgs; { default = fujicli; - inherit fujicli; + inherit fujicli fujicli-debug fujifilm-sdk-bin; }; formatter.${system} = treefmt.config.build.wrapper; diff --git a/src/cli/backup/mod.rs b/src/cli/backup/mod.rs index 741310c..ead1720 100644 --- a/src/cli/backup/mod.rs +++ b/src/cli/backup/mod.rs @@ -1,5 +1,3 @@ -use crate::usb; - use super::common::file::{Input, Output}; use clap::Subcommand; @@ -9,43 +7,28 @@ pub enum BackupCmd { #[command(alias = "e")] Export { /// Output file (use '-' to write to stdout) - output_file: Output, + output: Output, }, /// Import backup #[command(alias = "i")] Import { /// Input file (use '-' to read from stdin) - input_file: Input, + input: Input, }, } fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> { - let camera = usb::get_camera(device_id)?; - let mut ptp = camera.ptp_session()?; - - let mut writer = output.get_writer()?; - let backup = camera.export_backup(&mut ptp)?; - writer.write_all(&backup)?; - - Ok(()) + todo!() } fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> { - let camera = usb::get_camera(device_id)?; - let mut ptp = camera.ptp_session()?; - - let mut reader = input.get_reader()?; - let mut buffer = Vec::new(); - reader.read_to_end(&mut buffer)?; - camera.import_backup(&mut ptp, &buffer)?; - - Ok(()) + todo!() } pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> { match cmd { - BackupCmd::Export { output_file } => handle_export(device_id, &output_file), - BackupCmd::Import { input_file } => handle_import(device_id, &input_file), + BackupCmd::Export { output } => handle_export(device_id, &output), + BackupCmd::Import { input } => handle_import(device_id, &input), } } diff --git a/src/cli/device/mod.rs b/src/cli/device/mod.rs index 2c9a0bc..7aa5497 100644 --- a/src/cli/device/mod.rs +++ b/src/cli/device/mod.rs @@ -1,12 +1,6 @@ -use std::fmt; - use clap::Subcommand; -use serde::Serialize; -use crate::{ - hardware::{CameraImpl, UsbMode}, - usb, -}; +use crate::sdk::{XSDK, XSdkInterface}; #[derive(Subcommand, Debug, Clone, Copy)] pub enum DeviceCmd { @@ -19,122 +13,35 @@ pub enum DeviceCmd { Info, } -#[derive(Serialize)] -pub struct CameraItemRepr { - pub name: String, - pub id: String, - pub vendor_id: String, - pub product_id: String, -} - -impl From<&Box> for CameraItemRepr { - fn from(camera: &Box) -> Self { - Self { - id: camera.usb_id(), - name: camera.id().name.to_string(), - vendor_id: format!("0x{:04x}", camera.id().vendor), - product_id: format!("0x{:04x}", camera.id().product), - } - } -} - -impl fmt::Display for CameraItemRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "{} ({}:{}) (ID: {})", - self.name, self.vendor_id, self.product_id, self.id - ) - } -} - fn handle_list(json: bool) -> Result<(), anyhow::Error> { - let cameras: Vec = usb::get_connected_camers()? - .iter() - .map(std::convert::Into::into) + let sdk = XSDK.lock().unwrap(); + + let cameras = sdk.get_cameras(XSdkInterface::Usb, None)?; + + let valid_cameras: Vec<_> = cameras + .into_iter() + .enumerate() + .filter(|(_, cam)| cam.valid) .collect(); if json { - println!("{}", serde_json::to_string_pretty(&cameras)?); + println!("{}", serde_json::to_string_pretty(&valid_cameras)?); return Ok(()); } - if cameras.is_empty() { - println!("No supported cameras connected."); - return Ok(()); - } - - println!("Connected cameras:"); - for d in cameras { - println!("- {d}"); + for (id, cam) in valid_cameras { + println!("[{}] {}", id, cam); } Ok(()) } - -#[derive(Serialize)] -pub struct CameraRepr { - #[serde(flatten)] - pub device: CameraItemRepr, - - pub manufacturer: String, - pub model: String, - pub device_version: String, - pub serial_number: String, - pub mode: UsbMode, - pub battery: u32, +fn handle_info(json: bool, device: Option) -> Result<(), anyhow::Error> { + todo!() } -impl fmt::Display for CameraRepr { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "Name: {}", self.device.name)?; - writeln!(f, "ID: {}", self.device.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>) -> Result<(), anyhow::Error> { - let camera = usb::get_camera(device_id)?; - let mut ptp = camera.ptp(); - - let info = camera.get_info(&mut ptp)?; - - let mut ptp = camera.open_session(ptp)?; - let mode = camera.get_usb_mode(&mut ptp)?; - let battery = camera.get_battery_info(&mut ptp)?; - - let repr = CameraRepr { - device: (&camera).into(), - manufacturer: info.Manufacturer.clone(), - model: info.Model.clone(), - device_version: info.DeviceVersion.clone(), - serial_number: info.SerialNumber, - 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>) -> Result<(), anyhow::Error> { +pub fn handle(cmd: DeviceCmd, json: bool, device: Option) -> Result<(), anyhow::Error> { match cmd { DeviceCmd::List => handle_list(json), - DeviceCmd::Info => handle_info(json, device_id), + DeviceCmd::Info => handle_info(json, device), } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 80e574d..0e02e90 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -33,7 +33,7 @@ pub struct Cli { /// Manually specify target device #[arg(long, short = 'd', global = true)] - pub device: Option, + pub device: Option, } #[derive(Subcommand, Debug)] diff --git a/src/hardware/mod.rs b/src/hardware/mod.rs deleted file mode 100644 index 1543df5..0000000 --- a/src/hardware/mod.rs +++ /dev/null @@ -1,384 +0,0 @@ -use std::{ - fmt, - ops::{Deref, DerefMut}, - time::Duration, -}; - -use anyhow::bail; -use libptp::{DeviceInfo, StandardCommandCode}; -use log::{debug, error}; -use rusb::{DeviceDescriptor, GlobalContext}; -use serde::Serialize; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CameraId { - pub name: &'static str, - pub vendor: u16, - pub product: u16, -} - -type CameraFactory = fn(rusb::Device) -> Result, anyhow::Error>; - -pub struct SupportedCamera { - pub id: CameraId, - pub factory: CameraFactory, -} - -pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera { - id: FUJIFILM_XT5, - factory: |d| FujifilmXT5::new_boxed(&d), -}]; - -impl SupportedCamera { - pub fn matches_descriptor(&self, descriptor: &DeviceDescriptor) -> bool { - descriptor.vendor_id() == self.id.vendor && descriptor.product_id() == self.id.product - } -} - -pub const TIMEOUT: Duration = Duration::from_millis(500); - -#[repr(u32)] -#[derive(Debug, Clone, Copy)] -pub enum DevicePropCode { - FujiUsbMode = 0xd16e, - FujiBatteryInfo2 = 0xD36B, -} - -#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq)] -pub enum UsbMode { - RawConversion, - Unsupported, -} - -impl From 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}") - } -} - -pub struct Ptp { - ptp: libptp::Camera, -} - -impl Deref for Ptp { - type Target = libptp::Camera; - - fn deref(&self) -> &Self::Target { - &self.ptp - } -} - -impl DerefMut for Ptp { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ptp - } -} - -impl From> for Ptp { - fn from(ptp: libptp::Camera) -> Self { - Self { ptp } - } -} - -type SessionCloseFn = - Box) -> Result<(), anyhow::Error>>; - -pub struct PtpSession { - ptp: libptp::Camera, - session_id: u32, - close_fn: Option, -} - -impl Deref for PtpSession { - type Target = libptp::Camera; - - fn deref(&self) -> &Self::Target { - &self.ptp - } -} - -impl DerefMut for PtpSession { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.ptp - } -} - -impl Drop for PtpSession { - fn drop(&mut self) { - if let Some(close_fn) = self.close_fn.take() { - if let Err(e) = close_fn(self.session_id, &mut self.ptp) { - error!("Error closing session {}: {}", self.session_id, e); - } - } - } -} - -pub trait CameraImpl { - fn id(&self) -> &'static CameraId; - - fn device(&self) -> &rusb::Device; - - fn usb_id(&self) -> String { - let bus = self.device().bus_number(); - let address = self.device().address(); - format!("{bus}.{address}") - } - - fn ptp(&self) -> Ptp; - - fn ptp_session(&self) -> Result; - - fn get_info(&self, ptp: &mut Ptp) -> Result { - debug!("Sending GetDeviceInfo command"); - let response = ptp.command(StandardCommandCode::GetDeviceInfo, &[], None, Some(TIMEOUT))?; - debug!("Received response with {} bytes", response.len()); - - let info = DeviceInfo::decode(&response)?; - Ok(info) - } - - fn next_session_id(&self) -> u32; - - fn open_session(&self, ptp: Ptp) -> Result { - let session_id = self.next_session_id(); - let mut ptp = ptp.ptp; - - debug!("Opening session with id {session_id}"); - ptp.command( - StandardCommandCode::OpenSession, - &[session_id], - None, - Some(TIMEOUT), - )?; - debug!("Session {session_id} open"); - - let close_fn: Option = Some(Box::new(move |_, ptp| { - debug!("Closing session with id {session_id}"); - ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?; - debug!("Session {session_id} closed"); - Ok(()) - })); - - Ok(PtpSession { - ptp, - session_id, - close_fn, - }) - } - - fn get_prop_value_raw( - &self, - ptp: &mut PtpSession, - prop: DevicePropCode, - ) -> Result, anyhow::Error> { - debug!("Getting property {prop:?}"); - - let response = ptp.command( - StandardCommandCode::GetDevicePropValue, - &[prop as u32], - None, - Some(TIMEOUT), - )?; - - debug!("Received response with {} bytes", response.len()); - - Ok(response) - } - - fn get_prop_value_scalar( - &self, - ptp: &mut PtpSession, - prop: DevicePropCode, - ) -> Result { - let data = self.get_prop_value_raw(ptp, prop)?; - - match data.len() { - 1 => Ok(u32::from(data[0])), - 2 => Ok(u32::from(u16::from_le_bytes([data[0], data[1]]))), - 4 => Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])), - n => bail!("Cannot parse property {prop:?} as scalar: {n} bytes"), - } - } - - fn get_usb_mode(&self, ptp: &mut PtpSession) -> Result { - let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?; - Ok(result.into()) - } - - fn get_battery_info(&self, ptp: &mut PtpSession) -> Result { - let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?; - debug!("Raw battery data: {data:?}"); - - if data.len() < 3 { - bail!("Battery info payload too short"); - } - - let utf16: Vec = data[1..] - .chunks(2) - .map(|chunk| u16::from_le_bytes([chunk[0], chunk[1]])) - .take_while(|&c| c != 0) - .collect(); - - debug!("Decoded UTF-16 units: {utf16:?}"); - - 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::anyhow!("Failed to parse battery percentage"))? - .parse()?; - - Ok(percentage) - } - - fn export_backup(&self, ptp: &mut PtpSession) -> Result, anyhow::Error> { - const HANDLE: u32 = 0x0; - - debug!("Getting object info for backup"); - - let info = ptp.command( - StandardCommandCode::GetObjectInfo, - &[HANDLE], - None, - Some(TIMEOUT), - )?; - - debug!("Got object info, {} bytes", info.len()); - - debug!("Downloading backup object"); - - let object = ptp.command( - StandardCommandCode::GetObject, - &[HANDLE], - None, - Some(TIMEOUT), - )?; - - debug!("Downloaded backup object ({} bytes)", object.len()); - - Ok(object) - } - - fn import_backup(&self, ptp: &mut PtpSession, buffer: &[u8]) -> Result<(), anyhow::Error> { - todo!("This is currently broken"); - - debug!("Preparing ObjectInfo header for backup"); - - let mut obj_info = vec![0u8; 1088]; - let mut offset = 0; - - let padding0: u32 = 0x0; - let object_format: u16 = 0x5000; - let padding1: u16 = 0x0; - - obj_info[offset..offset + size_of::()].copy_from_slice(&padding0.to_le_bytes()); - offset += size_of::(); - obj_info[offset..offset + size_of::()].copy_from_slice(&object_format.to_le_bytes()); - offset += size_of::(); - obj_info[offset..offset + size_of::()].copy_from_slice(&padding1.to_le_bytes()); - offset += size_of::(); - obj_info[offset..offset + size_of::()] - .copy_from_slice(&u32::try_from(buffer.len())?.to_le_bytes()); - - let param0: u32 = 0x0; - let param1: u32 = 0x0; - - debug!("Sending ObjectInfo for backup"); - - ptp.command( - libptp::StandardCommandCode::SendObjectInfo, - &[param0, param1], - Some(&obj_info), - Some(TIMEOUT), - )?; - - debug!("Sending backup payload ({} bytes)", buffer.len()); - - ptp.command( - libptp::StandardCommandCode::SendObject, - &[], - Some(buffer), - Some(TIMEOUT), - )?; - - Ok(()) - } -} - -macro_rules! default_camera_impl { - ( - $const_name:ident, - $struct_name:ident, - $vendor:expr, - $product:expr, - $display_name:expr - ) => { - pub const $const_name: CameraId = CameraId { - name: $display_name, - vendor: $vendor, - product: $product, - }; - - pub struct $struct_name { - device: rusb::Device, - session_counter: std::sync::atomic::AtomicU32, - } - - impl $struct_name { - pub fn new_boxed( - rusb_device: &rusb::Device, - ) -> Result, anyhow::Error> { - let session_counter = std::sync::atomic::AtomicU32::new(1); - - let handle = rusb_device.open()?; - let device = handle.device(); - - Ok(Box::new(Self { - device, - session_counter, - })) - } - } - - impl CameraImpl for $struct_name { - fn id(&self) -> &'static CameraId { - &$const_name - } - - fn device(&self) -> &rusb::Device { - &self.device - } - - fn ptp(&self) -> Ptp { - libptp::Camera::new(&self.device).unwrap().into() - } - - fn ptp_session(&self) -> Result { - let ptp = self.ptp(); - self.open_session(ptp) - } - - fn next_session_id(&self) -> u32 { - self.session_counter - .fetch_add(1, std::sync::atomic::Ordering::SeqCst) - } - } - }; -} - -default_camera_impl!(FUJIFILM_XT5, FujifilmXT5, 0x04cb, 0x02fc, "FUJIFILM XT-5"); diff --git a/src/main.rs b/src/main.rs index 62e449c..e8d2bd7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,20 +5,18 @@ use clap::Parser; use cli::Commands; mod cli; -mod hardware; mod log; -mod usb; +mod sdk; fn main() -> Result<(), anyhow::Error> { let cli = cli::Cli::parse(); log::init(cli.quiet, cli.verbose)?; - let device_id = cli.device.as_deref(); + let device_id = cli.device; 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!(), } diff --git a/src/sdk/error.rs b/src/sdk/error.rs new file mode 100644 index 0000000..5acbb1e --- /dev/null +++ b/src/sdk/error.rs @@ -0,0 +1,143 @@ +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) + } +} diff --git a/src/sdk/mod.rs b/src/sdk/mod.rs new file mode 100644 index 0000000..2fecdda --- /dev/null +++ b/src/sdk/mod.rs @@ -0,0 +1,404 @@ +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, + pub ip_address: Option, + 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 { + 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::()?) + } + }, + framework, + valid: c.valid, + }) + } +} + +fn option_cstring_as_mut_ptr(cstring: &Option) -> *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), 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> = 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 { + 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, 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> = + 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::, _>>()?; + + 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); + } + } +} diff --git a/src/sdk/private/mod.rs b/src/sdk/private/mod.rs new file mode 100644 index 0000000..62c09d6 --- /dev/null +++ b/src/sdk/private/mod.rs @@ -0,0 +1,109 @@ +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; +} diff --git a/src/usb/mod.rs b/src/usb/mod.rs deleted file mode 100644 index 51ea467..0000000 --- a/src/usb/mod.rs +++ /dev/null @@ -1,60 +0,0 @@ -use anyhow::{anyhow, bail}; - -use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS}; - -pub fn get_connected_camers() -> Result>, anyhow::Error> { - let mut connected_cameras = Vec::new(); - - for device in rusb::devices()?.iter() { - let Ok(descriptor) = device.device_descriptor() else { - continue; - }; - - for camera in SUPPORTED_CAMERAS { - if camera.matches_descriptor(&descriptor) { - let camera = (camera.factory)(device)?; - connected_cameras.push(camera); - break; - } - } - } - - Ok(connected_cameras) -} - -pub fn get_connected_camera_by_id(id: &str) -> Result, anyhow::Error> { - 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 { - let descriptor = device.device_descriptor()?; - - for camera in SUPPORTED_CAMERAS { - if camera.matches_descriptor(&descriptor) { - let camera = (camera.factory)(device)?; - return Ok(camera); - } - } - - bail!("Device found at {id} but is not supported"); - } - } - - bail!("No device found with id: {id}"); -} - -pub fn get_camera(device_id: Option<&str>) -> Result, anyhow::Error> { - match device_id { - Some(id) => get_connected_camera_by_id(id), - None => get_connected_camers()? - .into_iter() - .next() - .ok_or_else(|| anyhow!("No supported devices connected.")), - } -}