Compare commits
1 Commits
1f26a91dcd
...
sdk
Author | SHA1 | Date | |
---|---|---|---|
151756c3e8
|
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -237,11 +237,13 @@ name = "fujicli"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"bitflags",
|
||||||
"clap",
|
"clap",
|
||||||
|
"libc",
|
||||||
"libptp",
|
"libptp",
|
||||||
"log",
|
"log",
|
||||||
"log4rs",
|
"log4rs",
|
||||||
"rusb",
|
"once_cell",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
@@ -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'
|
||||||
@@ -15,10 +16,12 @@ codegen-units = 1
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.100"
|
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"
|
||||||
|
12
build.rs
Normal file
12
build.rs
Normal 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");
|
||||||
|
}
|
50
flake.nix
50
flake.nix
@@ -32,6 +32,50 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -59,17 +103,21 @@
|
|||||||
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;
|
inherit fujicli fujicli-debug fujifilm-sdk-bin;
|
||||||
};
|
};
|
||||||
|
|
||||||
formatter.${system} = treefmt.config.build.wrapper;
|
formatter.${system} = treefmt.config.build.wrapper;
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
use crate::usb;
|
|
||||||
|
|
||||||
use super::common::file::{Input, Output};
|
use super::common::file::{Input, Output};
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
|
|
||||||
@@ -9,43 +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(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
|
fn handle_export(device_id: Option<&str>, output: &Output) -> Result<(), anyhow::Error> {
|
||||||
let camera = usb::get_camera(device_id)?;
|
todo!()
|
||||||
let mut ptp = camera.ptp_session()?;
|
|
||||||
|
|
||||||
let mut writer = output.get_writer()?;
|
|
||||||
let backup = camera.export_backup(&mut ptp)?;
|
|
||||||
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) -> Result<(), anyhow::Error> {
|
||||||
let camera = usb::get_camera(device_id)?;
|
todo!()
|
||||||
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(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle(cmd: BackupCmd, device_id: Option<&str>) -> Result<(), anyhow::Error> {
|
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,12 +1,6 @@
|
|||||||
use std::fmt;
|
|
||||||
|
|
||||||
use clap::Subcommand;
|
use clap::Subcommand;
|
||||||
use serde::Serialize;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::sdk::{XSDK, XSdkInterface};
|
||||||
hardware::{CameraImpl, UsbMode},
|
|
||||||
usb,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug, Clone, Copy)]
|
#[derive(Subcommand, Debug, Clone, Copy)]
|
||||||
pub enum DeviceCmd {
|
pub enum DeviceCmd {
|
||||||
@@ -19,122 +13,35 @@ pub enum DeviceCmd {
|
|||||||
Info,
|
Info,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct CameraItemRepr {
|
|
||||||
pub name: String,
|
|
||||||
pub id: String,
|
|
||||||
pub vendor_id: String,
|
|
||||||
pub product_id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Box<dyn CameraImpl>> for CameraItemRepr {
|
|
||||||
fn from(camera: &Box<dyn CameraImpl>) -> 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> {
|
fn handle_list(json: bool) -> Result<(), anyhow::Error> {
|
||||||
let cameras: Vec<CameraItemRepr> = usb::get_connected_camers()?
|
let sdk = XSDK.lock().unwrap();
|
||||||
.iter()
|
|
||||||
.map(std::convert::Into::into)
|
let cameras = sdk.get_cameras(XSdkInterface::Usb, None)?;
|
||||||
|
|
||||||
|
let valid_cameras: Vec<_> = cameras
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.filter(|(_, cam)| cam.valid)
|
||||||
.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: UsbMode,
|
|
||||||
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<(), 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> {
|
|
||||||
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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)]
|
||||||
|
@@ -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<GlobalContext>) -> Result<Box<dyn CameraImpl>, 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<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}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Ptp {
|
|
||||||
ptp: libptp::Camera<GlobalContext>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for Ptp {
|
|
||||||
type Target = libptp::Camera<GlobalContext>;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.ptp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DerefMut for Ptp {
|
|
||||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
|
||||||
&mut self.ptp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<libptp::Camera<GlobalContext>> for Ptp {
|
|
||||||
fn from(ptp: libptp::Camera<GlobalContext>) -> Self {
|
|
||||||
Self { ptp }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type SessionCloseFn =
|
|
||||||
Box<dyn FnOnce(u32, &mut libptp::Camera<GlobalContext>) -> Result<(), anyhow::Error>>;
|
|
||||||
|
|
||||||
pub struct PtpSession {
|
|
||||||
ptp: libptp::Camera<GlobalContext>,
|
|
||||||
session_id: u32,
|
|
||||||
close_fn: Option<SessionCloseFn>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Deref for PtpSession {
|
|
||||||
type Target = libptp::Camera<GlobalContext>;
|
|
||||||
|
|
||||||
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<rusb::GlobalContext>;
|
|
||||||
|
|
||||||
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<PtpSession, anyhow::Error>;
|
|
||||||
|
|
||||||
fn get_info(&self, ptp: &mut Ptp) -> Result<DeviceInfo, anyhow::Error> {
|
|
||||||
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<PtpSession, anyhow::Error> {
|
|
||||||
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<SessionCloseFn> = 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<Vec<u8>, 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<u32, anyhow::Error> {
|
|
||||||
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<UsbMode, anyhow::Error> {
|
|
||||||
let result = self.get_prop_value_scalar(ptp, DevicePropCode::FujiUsbMode)?;
|
|
||||||
Ok(result.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_battery_info(&self, ptp: &mut PtpSession) -> Result<u32, anyhow::Error> {
|
|
||||||
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<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_else(|| anyhow::anyhow!("Failed to parse battery percentage"))?
|
|
||||||
.parse()?;
|
|
||||||
|
|
||||||
Ok(percentage)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn export_backup(&self, ptp: &mut PtpSession) -> Result<Vec<u8>, 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::<u32>()].copy_from_slice(&padding0.to_le_bytes());
|
|
||||||
offset += size_of::<u32>();
|
|
||||||
obj_info[offset..offset + size_of::<u16>()].copy_from_slice(&object_format.to_le_bytes());
|
|
||||||
offset += size_of::<u16>();
|
|
||||||
obj_info[offset..offset + size_of::<u16>()].copy_from_slice(&padding1.to_le_bytes());
|
|
||||||
offset += size_of::<u16>();
|
|
||||||
obj_info[offset..offset + size_of::<u32>()]
|
|
||||||
.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<rusb::GlobalContext>,
|
|
||||||
session_counter: std::sync::atomic::AtomicU32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl $struct_name {
|
|
||||||
pub fn new_boxed(
|
|
||||||
rusb_device: &rusb::Device<rusb::GlobalContext>,
|
|
||||||
) -> Result<Box<dyn CameraImpl>, 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<rusb::GlobalContext> {
|
|
||||||
&self.device
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ptp(&self) -> Ptp {
|
|
||||||
libptp::Camera::new(&self.device).unwrap().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ptp_session(&self) -> Result<PtpSession, anyhow::Error> {
|
|
||||||
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");
|
|
@@ -5,20 +5,18 @@ 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<(), anyhow::Error> {
|
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.as_deref();
|
let device_id = cli.device;
|
||||||
|
|
||||||
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
Normal file
143
src/sdk/error.rs
Normal 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
404
src/sdk/mod.rs
Normal 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
109
src/sdk/private/mod.rs
Normal 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;
|
||||||
|
}
|
@@ -1,60 +0,0 @@
|
|||||||
use anyhow::{anyhow, bail};
|
|
||||||
|
|
||||||
use crate::hardware::{CameraImpl, SUPPORTED_CAMERAS};
|
|
||||||
|
|
||||||
pub fn get_connected_camers() -> Result<Vec<Box<dyn crate::hardware::CameraImpl>>, 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<Box<dyn CameraImpl>, 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<Box<dyn CameraImpl>, 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.")),
|
|
||||||
}
|
|
||||||
}
|
|
Reference in New Issue
Block a user