Compare commits

..

4 Commits

Author SHA1 Message Date
151756c3e8 tmp: use sdk (broken)
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-14 23:45:09 +01:00
913ca06512 chore: add flake
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-14 23:42:18 +01:00
e3720b8afb chore: update README
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-13 23:36:52 +01:00
5e0a987386 chore: refactor ptp handling
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-10-13 20:39:22 +01:00
22 changed files with 987 additions and 517 deletions

2
.envrc
View File

@@ -1 +1 @@
use flake self#rust use flake

10
.gitignore vendored
View File

@@ -16,3 +16,13 @@ target/
# and can be added to the global gitignore or merged into this file. For a more nuclear # and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# ---> Nix
# Ignore build outputs from performing a nix-build or `nix build` command
result
result-*
# Ignore automatically generated direnv output
.direnv
.cargo/

5
Cargo.lock generated
View File

@@ -236,11 +236,14 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
name = "fujicli" name = "fujicli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow",
"bitflags",
"clap", "clap",
"libc",
"libptp", "libptp",
"log", "log",
"log4rs", "log4rs",
"rusb", "once_cell",
"serde", "serde",
"serde_json", "serde_json",
] ]

View File

@@ -6,6 +6,7 @@ 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'
@@ -14,10 +15,13 @@ lto = true
codegen-units = 1 codegen-units = 1
[dependencies] [dependencies]
anyhow = "1.0.100"
bitflags = "2.9.4"
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" libptp = "0.6.5"
log = "0.4.28" log = "0.4.28"
log4rs = "1.4.0" log4rs = "1.4.0"
rusb = "0.9.4" once_cell = "1.21.3"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"

View File

@@ -1,3 +1,21 @@
# fujicli # fujicli
A CLI that performs on-device image rendering similar to Fujifilm X RAW Studio A CLI to manage Fujifilm devices, simulations, backups, and rendering.
## Status
This tool has only been tested with the **Fujifilm X-T5**, as it is the sole camera I own. While the underlying PTP commands may be compatible with other models, **compatibility is not guaranteed**.
**Use this software at your own risk.** I am not responsible for any damage, loss of data, or other adverse outcomes - physical or psychological - to your camera or equipment resulting from the use of this program.
This project is currently under heavy development. Contributions are welcome. If you own a different Fujifilm camera, testing and reporting compatibility is highly appreciated.
## Resources
This project builds upon the following fantastic reverse-engineering efforts:
* [fujihack](https://github.com/fujihack/fujihack)
* [fudge](https://github.com/petabyt/fudge)
* [libpict](https://github.com/petabyt/libpict)
* [fp](https://github.com/petabyt/fp)
* [libgphoto2](https://github.com/gphoto/libgphoto2)

12
build.rs Normal file
View File

@@ -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");
}

72
flake.lock generated Normal file
View File

@@ -0,0 +1,72 @@
{
"nodes": {
"lib": {
"inputs": {
"nixpkgs": [
"nixpkgs"
],
"treefmt-nix": [
"treefmt-nix"
]
},
"locked": {
"lastModified": 1758632667,
"narHash": "sha256-C0aBPv8vqTI1QNVhygZxL0f49UERx2UejVdtyz67jhs=",
"ref": "refs/heads/main",
"rev": "5e0737c20f3c265dbff604170a6433cc1e1a4b41",
"revCount": 8,
"type": "git",
"url": "https://git.karaolidis.com/karaolidis/nix-lib.git"
},
"original": {
"type": "git",
"url": "https://git.karaolidis.com/karaolidis/nix-lib.git"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1760284886,
"narHash": "sha256-TK9Kr0BYBQ/1P5kAsnNQhmWWKgmZXwUQr4ZMjCzWf2c=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "cf3f5c4def3c7b5f1fc012b3d839575dbe552d43",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"lib": "lib",
"nixpkgs": "nixpkgs",
"treefmt-nix": "treefmt-nix"
}
},
"treefmt-nix": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1760120816,
"narHash": "sha256-gq9rdocpmRZCwLS5vsHozwB6b5nrOBDNc2kkEaTXHfg=",
"owner": "numtide",
"repo": "treefmt-nix",
"rev": "761ae7aff00907b607125b2f57338b74177697ed",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "treefmt-nix",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

140
flake.nix Executable file
View File

@@ -0,0 +1,140 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
treefmt-nix = {
url = "github:numtide/treefmt-nix";
inputs.nixpkgs.follows = "nixpkgs";
};
lib = {
# FIXME: https://github.com/NixOS/nix/issues/12281
url = "git+https://git.karaolidis.com/karaolidis/nix-lib.git";
inputs = {
nixpkgs.follows = "nixpkgs";
treefmt-nix.follows = "treefmt-nix";
};
};
};
outputs =
inputs:
{
overlays.default =
final: prev:
let
pkgs = final;
in
{
fujicli = pkgs.rustPlatform.buildRustPackage {
pname = "fujicli";
version = "0.1.0";
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;
};
};
};
}
// (
let
system = "x86_64-linux";
pkgs = import inputs.nixpkgs {
inherit system;
config.allowUnfree = true;
overlays = [
inputs.lib.overlays.default
inputs.self.overlays.default
];
};
treefmt = inputs.treefmt-nix.lib.evalModule pkgs ./treefmt.nix;
in
{
devShells.${system}.default = pkgs.mkShell {
packages = with pkgs; [
cargo
rustc
rustfmt
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 fujicli-debug fujifilm-sdk-bin;
};
formatter.${system} = treefmt.config.build.wrapper;
checks.${system} =
let
packages = pkgs.lib.mapAttrs' (
name: pkgs.lib.nameValuePair "package-${name}"
) inputs.self.packages.${system};
devShells = pkgs.lib.mapAttrs' (
name: pkgs.lib.nameValuePair "devShell-${name}"
) inputs.self.devShells.${system};
formatter.formatting = treefmt.config.build.check inputs.self;
in
packages // devShells // formatter;
}
);
}

View File

@@ -1,7 +1,3 @@
use std::{error::Error, fmt};
use crate::usb;
use super::common::file::{Input, Output}; use super::common::file::{Input, Output};
use clap::Subcommand; use clap::Subcommand;
@@ -11,46 +7,28 @@ 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_file: Output, output: 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_file: Input, input: Input,
}, },
} }
fn handle_export( fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
device_id: Option<&str>, todo!()
output: Output,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let camera = usb::get_camera(device_id)?;
let mut writer = output.get_writer()?;
todo!();
Ok(())
} }
fn handle_import( fn handle_import(device_id: Option<&str>, input: &Input) -> Result<(), anyhow::Error> {
device_id: Option<&str>, todo!()
input: Input,
) -> Result<(), Box<dyn Error + Send + Sync>> {
let camera = usb::get_camera(device_id)?;
let mut reader = input.get_reader()?;
todo!();
Ok(())
} }
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), Box<dyn Error + Send + Sync>> { pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> {
match cmd { match cmd {
BackupCmd::Export { output_file } => handle_export(device_id, output_file), BackupCmd::Export { output } => handle_export(device_id, &output),
BackupCmd::Import { input_file } => handle_import(device_id, input_file), BackupCmd::Import { input } => handle_import(device_id, &input),
} }
} }

View File

@@ -1,4 +1,4 @@
use std::{error::Error, fs::File, io, path::PathBuf, str::FromStr}; use std::{fs::File, io, path::PathBuf, str::FromStr};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum Input { pub enum Input {
@@ -7,21 +7,21 @@ pub enum Input {
} }
impl FromStr for Input { impl FromStr for Input {
type Err = Box<dyn Error + Send + Sync>; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" { if s == "-" {
Ok(Input::Stdin) Ok(Self::Stdin)
} else { } else {
Ok(Input::Path(PathBuf::from(s))) Ok(Self::Path(PathBuf::from(s)))
} }
} }
} }
impl Input { impl Input {
pub fn get_reader(&self) -> Result<Box<dyn io::Read>, Box<dyn Error + Send + Sync>> { pub fn get_reader(&self) -> Result<Box<dyn io::Read>, anyhow::Error> {
match self { match self {
Input::Stdin => Ok(Box::new(io::stdin())), Self::Stdin => Ok(Box::new(io::stdin())),
Input::Path(path) => Ok(Box::new(File::open(path)?)), Self::Path(path) => Ok(Box::new(File::open(path)?)),
} }
} }
} }
@@ -33,21 +33,21 @@ pub enum Output {
} }
impl FromStr for Output { impl FromStr for Output {
type Err = Box<dyn Error + Send + Sync>; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" { if s == "-" {
Ok(Output::Stdout) Ok(Self::Stdout)
} else { } else {
Ok(Output::Path(PathBuf::from(s))) Ok(Self::Path(PathBuf::from(s)))
} }
} }
} }
impl Output { impl Output {
pub fn get_writer(&self) -> Result<Box<dyn io::Write>, Box<dyn Error + Send + Sync>> { pub fn get_writer(&self) -> Result<Box<dyn io::Write>, anyhow::Error> {
match self { match self {
Output::Stdout => Ok(Box::new(io::stdout())), Self::Stdout => Ok(Box::new(io::stdout())),
Output::Path(path) => Ok(Box::new(File::create(path)?)), Self::Path(path) => Ok(Box::new(File::create(path)?)),
} }
} }
} }

View File

@@ -1,5 +1,4 @@
use std::error::Error; use anyhow::bail;
use clap::Args; use clap::Args;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -9,18 +8,18 @@ pub enum SimulationSelector {
} }
impl std::str::FromStr for SimulationSelector { impl std::str::FromStr for SimulationSelector {
type Err = Box<dyn Error + Send + Sync>; type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Ok(slot) = s.parse::<u8>() { if let Ok(slot) = s.parse::<u8>() {
return Ok(SimulationSelector::Slot(slot)); return Ok(Self::Slot(slot));
} }
if s.is_empty() { if s.is_empty() {
Err("Simulation name cannot be empty".into()) bail!("Simulation name cannot be empty")
} else {
Ok(SimulationSelector::Name(s.to_string()))
} }
Ok(Self::Name(s.to_string()))
} }
} }

View File

@@ -1,11 +1,8 @@
use std::{error::Error, fmt};
use clap::Subcommand; use clap::Subcommand;
use serde::Serialize;
use crate::{hardware::FujiUsbMode, usb}; use crate::sdk::{XSDK, XSdkInterface};
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug, Clone, Copy)]
pub enum DeviceCmd { pub enum DeviceCmd {
/// List cameras /// List cameras
#[command(alias = "l")] #[command(alias = "l")]
@@ -16,123 +13,35 @@ pub enum DeviceCmd {
Info, Info,
} }
#[derive(Serialize)] fn handle_list(json: bool) -> Result<(), anyhow::Error> {
pub struct CameraItemRepr { let sdk = XSDK.lock().unwrap();
pub name: String,
pub id: String,
pub vendor_id: String,
pub product_id: String,
}
impl From<&usb::Camera> for CameraItemRepr { let cameras = sdk.get_cameras(XSdkInterface::Usb, None)?;
fn from(device: &usb::Camera) -> Self {
CameraItemRepr {
id: device.id(),
name: device.name(),
vendor_id: format!("0x{:04x}", device.vendor_id()),
product_id: format!("0x{:04x}", device.product_id()),
}
}
}
impl fmt::Display for CameraItemRepr { let valid_cameras: Vec<_> = cameras
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { .into_iter()
write!( .enumerate()
f, .filter(|(_, cam)| cam.valid)
"{} ({}:{}) (ID: {})",
self.name, self.vendor_id, self.product_id, self.id
)
}
}
fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()?
.iter()
.map(|d| d.into())
.collect(); .collect();
if json { if json {
println!("{}", serde_json::to_string_pretty(&cameras)?); println!("{}", serde_json::to_string_pretty(&valid_cameras)?);
return Ok(()); return Ok(());
} }
if cameras.is_empty() { for (id, cam) in valid_cameras {
println!("No supported cameras connected."); println!("[{}] {}", id, cam);
return Ok(());
}
println!("Connected cameras:");
for d in cameras {
println!("- {}", d);
} }
Ok(()) Ok(())
} }
fn handle_info(json: bool, device: Option<u32>) -> Result<(), anyhow::Error> {
#[derive(Serialize)] todo!()
pub struct CameraRepr {
#[serde(flatten)]
pub device: CameraItemRepr,
pub manufacturer: String,
pub model: String,
pub device_version: String,
pub serial_number: String,
pub mode: FujiUsbMode,
pub battery: u32,
} }
impl fmt::Display for CameraRepr { pub fn handle(cmd: DeviceCmd, json: bool, device: Option<u32>) -> Result<(), anyhow::Error> {
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<(), Box<dyn Error + Send + Sync>> {
let camera = usb::get_camera(device_id)?;
let info = camera.get_info()?;
let mode = camera.get_fuji_usb_mode()?;
let battery = camera.get_fuji_battery_info()?;
let repr = CameraRepr {
device: (&camera).into(),
manufacturer: info.Manufacturer.clone(),
model: info.Model.clone(),
device_version: info.DeviceVersion.clone(),
serial_number: info.SerialNumber.clone(),
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<(), Box<dyn Error + Send + Sync>> {
match cmd { match cmd {
DeviceCmd::List => handle_list(json), DeviceCmd::List => handle_list(json),
DeviceCmd::Info => handle_info(json, device_id), DeviceCmd::Info => handle_info(json, device),
} }
} }

View File

@@ -33,7 +33,7 @@ pub struct Cli {
/// Manually specify target device /// Manually specify target device
#[arg(long, short = 'd', global = true)] #[arg(long, short = 'd', global = true)]
pub device: Option<String>, pub device: Option<u32>,
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]

View File

@@ -1,194 +0,0 @@
use std::{error::Error, fmt, time::Duration};
use libptp::{DeviceInfo, StandardCommandCode};
use log::debug;
use rusb::{DeviceDescriptor, GlobalContext};
use serde::Serialize;
mod xt5;
pub const TIMEOUT: Duration = Duration::from_millis(500);
#[repr(u32)]
#[derive(Debug, Clone, Copy)]
pub enum DevicePropCode {
FujiUsbMode = 0xd16e,
FujiBatteryInfo1 = 0xD36A,
FujiBatteryInfo2 = 0xD36B,
}
#[derive(Debug, Clone, Copy, Serialize)]
pub enum FujiUsbMode {
RawConversion, // mode == 6
Unsupported,
}
impl From<u32> for FujiUsbMode {
fn from(val: u32) -> Self {
match val {
6 => FujiUsbMode::RawConversion,
_ => FujiUsbMode::Unsupported,
}
}
}
impl fmt::Display for FujiUsbMode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
FujiUsbMode::RawConversion => "USB RAW CONV./BACKUP RESTORE",
FujiUsbMode::Unsupported => "Unsupported USB Mode",
};
write!(f, "{}", s)
}
}
pub trait CameraImpl {
fn name(&self) -> &'static str;
fn next_session_id(&self) -> u32;
fn open_session(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
session_id: u32,
) -> Result<(), Box<dyn Error + Send + Sync>> {
debug!("Opening new session with id {}", session_id);
ptp.command(
StandardCommandCode::OpenSession,
&[session_id],
None,
Some(TIMEOUT),
)?;
Ok(())
}
fn close_session(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<(), Box<dyn Error + Send + Sync>> {
debug!("Closing session");
ptp.command(StandardCommandCode::CloseSession, &[], None, Some(TIMEOUT))?;
Ok(())
}
fn get_prop_value_raw(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
prop: DevicePropCode,
) -> Result<Vec<u8>, Box<dyn Error + Send + Sync>> {
let session_id = self.next_session_id();
self.open_session(ptp, session_id)?;
debug!("Getting property {:?}", prop);
let response = ptp.command(
StandardCommandCode::GetDevicePropValue,
&[prop as u32],
None,
Some(TIMEOUT),
);
self.close_session(ptp)?;
let response = response?;
debug!("Received response with {} bytes", response.len());
Ok(response)
}
fn get_prop_value_scalar(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
prop: DevicePropCode,
) -> Result<u32, Box<dyn Error + Send + Sync>> {
let data = self.get_prop_value_raw(ptp, prop)?;
match data.len() {
1 => Ok(data[0] as u32),
2 => Ok(u16::from_le_bytes([data[0], data[1]]) as u32),
4 => Ok(u32::from_le_bytes([data[0], data[1], data[2], data[3]])),
n => Err(format!("Cannot parse property {:?} as scalar: {} bytes", prop, n).into()),
}
}
fn get_fuji_usb_mode(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<FujiUsbMode, Box<dyn Error + Send + Sync>> {
let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?;
Ok(result.into())
}
fn get_fuji_battery_info(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<u32, Box<dyn Error + Send + Sync>> {
let data = self.get_prop_value_raw(ptp, DevicePropCode::FujiBatteryInfo2)?;
debug!("Raw battery data: {:?}", data);
if data.len() < 3 {
return Err("Battery info payload too short".into());
}
let utf16: Vec<u16> = 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("Failed to parse battery percentage")?
.parse()?;
Ok(percentage)
}
fn get_info(
&self,
ptp: &mut libptp::Camera<GlobalContext>,
) -> Result<DeviceInfo, Box<dyn Error + Send + Sync>> {
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)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CameraId {
pub vendor: u16,
pub product: u16,
}
pub struct SupportedCamera {
pub id: CameraId,
pub factory: fn() -> Box<dyn CameraImpl>,
}
pub const SUPPORTED_CAMERAS: &[SupportedCamera] = &[SupportedCamera {
id: xt5::FUJIFILM_XT5,
factory: || Box::new(xt5::FujifilmXT5::new()),
}];
impl From<&SupportedCamera> for Box<dyn CameraImpl> {
fn from(camera: &SupportedCamera) -> Self {
(camera.factory)()
}
}
impl SupportedCamera {
pub fn matches_descriptor(&self, descriptor: &DeviceDescriptor) -> bool {
descriptor.vendor_id() == self.id.vendor && descriptor.product_id() == self.id.product
}
}

View File

@@ -1,31 +0,0 @@
use std::sync::atomic::{AtomicU32, Ordering};
use super::{CameraId, CameraImpl};
pub const FUJIFILM_XT5: CameraId = CameraId {
vendor: 0x04cb,
product: 0x02fc,
};
#[derive(Debug)]
pub struct FujifilmXT5 {
session_counter: AtomicU32,
}
impl FujifilmXT5 {
pub fn new() -> Self {
Self {
session_counter: AtomicU32::new(1),
}
}
}
impl CameraImpl for FujifilmXT5 {
fn name(&self) -> &'static str {
"FUJIFILM X-T5"
}
fn next_session_id(&self) -> u32 {
self.session_counter.fetch_add(1, Ordering::SeqCst)
}
}

View File

@@ -1,5 +1,3 @@
use std::error::Error;
use log::LevelFilter; use log::LevelFilter;
use log4rs::{ use log4rs::{
Config, Config,
@@ -8,7 +6,7 @@ use log4rs::{
encode::pattern::PatternEncoder, encode::pattern::PatternEncoder,
}; };
pub fn init(quiet: bool, verbose: bool) -> Result<(), Box<dyn Error + Send + Sync>> { pub fn init(quiet: bool, verbose: bool) -> Result<(), anyhow::Error> {
let level = if quiet { let level = if quiet {
LevelFilter::Warn LevelFilter::Warn
} else if verbose { } else if verbose {

View File

@@ -1,22 +1,22 @@
use std::error::Error; #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_docs_in_private_items)]
use clap::Parser; use clap::Parser;
use cli::Commands; use cli::Commands;
mod cli; mod cli;
mod hardware;
mod log; mod log;
mod usb; mod sdk;
fn main() -> Result<(), Box<dyn Error + Send + Sync>> { fn main() -> Result<(), anyhow::Error> {
let cli = cli::Cli::parse(); let cli = cli::Cli::parse();
log::init(cli.quiet, cli.verbose)?; log::init(cli.quiet, cli.verbose)?;
let device_id = cli.device;
match cli.command { match cli.command {
Commands::Device(device_cmd) => { Commands::Device(device_cmd) => cli::device::handle(device_cmd, cli.json, device_id)?,
cli::device::handle(device_cmd, cli.json, cli.device.as_deref())?
}
_ => todo!(), _ => todo!(),
} }

143
src/sdk/error.rs Normal file
View File

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

404
src/sdk/mod.rs Normal file
View File

@@ -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<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);
}
}
}

109
src/sdk/private/mod.rs Normal file
View File

@@ -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;
}

View File

@@ -1,119 +0,0 @@
use std::error::Error;
use libptp::DeviceInfo;
use rusb::GlobalContext;
use crate::hardware::{FujiUsbMode, SUPPORTED_CAMERAS};
pub struct Camera {
camera_impl: Box<dyn crate::hardware::CameraImpl>,
rusb_device: rusb::Device<GlobalContext>,
}
impl Camera {
pub fn id(&self) -> String {
let bus = self.rusb_device.bus_number();
let address = self.rusb_device.address();
format!("{}.{}", bus, address)
}
pub fn name(&self) -> String {
self.camera_impl.name().to_string()
}
pub fn vendor_id(&self) -> u16 {
let descriptor = self.rusb_device.device_descriptor().unwrap();
descriptor.vendor_id()
}
pub fn product_id(&self) -> u16 {
let descriptor = self.rusb_device.device_descriptor().unwrap();
descriptor.product_id()
}
pub fn ptp(&self) -> Result<libptp::Camera<GlobalContext>, Box<dyn Error + Send + Sync>> {
let handle = self.rusb_device.open()?;
let device = handle.device();
let ptp = libptp::Camera::new(&device)?;
Ok(ptp)
}
pub fn get_info(&self) -> Result<DeviceInfo, Box<dyn Error + Send + Sync>> {
let mut ptp = self.ptp()?;
self.camera_impl.get_info(&mut ptp)
}
pub fn get_fuji_usb_mode(&self) -> Result<FujiUsbMode, Box<dyn Error + Send + Sync>> {
let mut ptp = self.ptp()?;
self.camera_impl.get_fuji_usb_mode(&mut ptp)
}
pub fn get_fuji_battery_info(&self) -> Result<u32, Box<dyn Error + Send + Sync>> {
let mut ptp = self.ptp()?;
self.camera_impl.get_fuji_battery_info(&mut ptp)
}
}
pub fn get_connected_camers() -> Result<Vec<Camera>, Box<dyn Error + Send + Sync>> {
let mut connected_cameras = Vec::new();
for device in rusb::devices()?.iter() {
let descriptor = match device.device_descriptor() {
Ok(d) => d,
Err(_) => continue,
};
for camera in SUPPORTED_CAMERAS.iter() {
if camera.matches_descriptor(&descriptor) {
let camera = Camera {
camera_impl: camera.into(),
rusb_device: device,
};
connected_cameras.push(camera);
break;
}
}
}
Ok(connected_cameras)
}
pub fn get_connected_camera_by_id(id: &str) -> Result<Camera, Box<dyn Error + Send + Sync>> {
let parts: Vec<&str> = id.split('.').collect();
if parts.len() != 2 {
return Err(format!("Invalid device id format: {}", id).into());
}
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.iter() {
if camera.matches_descriptor(&descriptor) {
return Ok(Camera {
camera_impl: camera.into(),
rusb_device: device,
});
}
}
return Err(format!("Device found at {} but is not supported", id).into());
}
}
Err(format!("No device found with id: {}", id).into())
}
pub fn get_camera(device_id: Option<&str>) -> Result<Camera, Box<dyn Error + Send + Sync>> {
match device_id {
Some(id) => get_connected_camera_by_id(id),
None => get_connected_camers()?
.into_iter()
.next()
.ok_or_else(|| "No supported devices connected.".into()),
}
}

15
treefmt.nix Executable file
View File

@@ -0,0 +1,15 @@
{ ... }:
{
projectRootFile = "flake.nix";
programs = {
nixfmt = {
enable = true;
strict = true;
};
rustfmt.enable = true;
};
settings.global.excludes = [ ".envrc" ];
}