feat: device commands
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -241,6 +241,8 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"log4rs",
|
"log4rs",
|
||||||
"rusb",
|
"rusb",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@@ -19,3 +19,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"
|
rusb = "0.9.4"
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
serde_json = "1.0.145"
|
||||||
|
@@ -1,12 +0,0 @@
|
|||||||
use clap::Subcommand;
|
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
|
||||||
pub enum DeviceCmd {
|
|
||||||
/// List devices
|
|
||||||
#[command(alias = "l")]
|
|
||||||
List,
|
|
||||||
|
|
||||||
/// Dump device info
|
|
||||||
#[command(alias = "i")]
|
|
||||||
Info,
|
|
||||||
}
|
|
139
src/cli/device/mod.rs
Normal file
139
src/cli/device/mod.rs
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
use clap::Subcommand;
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
use crate::usb;
|
||||||
|
|
||||||
|
#[derive(Subcommand, Debug)]
|
||||||
|
pub enum DeviceCmd {
|
||||||
|
/// List devices
|
||||||
|
#[command(alias = "l")]
|
||||||
|
List,
|
||||||
|
|
||||||
|
/// Dump device info
|
||||||
|
#[command(alias = "i")]
|
||||||
|
Info,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct DeviceItemRepr {
|
||||||
|
pub name: String,
|
||||||
|
pub id: String,
|
||||||
|
pub vendor_id: String,
|
||||||
|
pub product_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&usb::Device> for DeviceItemRepr {
|
||||||
|
fn from(device: &usb::Device) -> Self {
|
||||||
|
DeviceItemRepr {
|
||||||
|
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 DeviceItemRepr {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} ({}:{}) (ID: {})",
|
||||||
|
self.name, self.vendor_id, self.product_id, self.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_list(json: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
let devices: Vec<DeviceItemRepr> = usb::get_connected_devices()?
|
||||||
|
.iter()
|
||||||
|
.map(|d| d.into())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if json {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&devices)?);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
if devices.is_empty() {
|
||||||
|
println!("No supported devices connected.");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Connected devices:");
|
||||||
|
for d in devices {
|
||||||
|
println!("- {}", d);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct DeviceRepr {
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub device: DeviceItemRepr,
|
||||||
|
|
||||||
|
pub manufacturer: String,
|
||||||
|
pub model: String,
|
||||||
|
pub device_version: String,
|
||||||
|
pub serial_number: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceRepr {
|
||||||
|
pub fn from_info(device: &usb::Device, info: &libptp::DeviceInfo) -> Self {
|
||||||
|
DeviceRepr {
|
||||||
|
device: device.into(),
|
||||||
|
manufacturer: info.Manufacturer.clone(),
|
||||||
|
model: info.Model.clone(),
|
||||||
|
device_version: info.DeviceVersion.clone(),
|
||||||
|
serial_number: info.SerialNumber.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for DeviceRepr {
|
||||||
|
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, "Device Version: {}", self.device_version)?;
|
||||||
|
write!(f, "Serial Number: {}", self.serial_number)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_info(
|
||||||
|
json: bool,
|
||||||
|
device_id: Option<&str>,
|
||||||
|
) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
|
let device = usb::get_device(device_id)?;
|
||||||
|
|
||||||
|
let mut camera = device.camera()?;
|
||||||
|
let info = device.model.get_device_info(&mut camera)?;
|
||||||
|
let repr = DeviceRepr::from_info(&device, &info);
|
||||||
|
|
||||||
|
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 {
|
||||||
|
DeviceCmd::List => handle_list(json),
|
||||||
|
DeviceCmd::Info => handle_info(json, device_id),
|
||||||
|
}
|
||||||
|
}
|
@@ -1,8 +1,9 @@
|
|||||||
mod backup;
|
|
||||||
mod common;
|
mod common;
|
||||||
mod device;
|
|
||||||
mod render;
|
pub mod backup;
|
||||||
mod simulation;
|
pub mod device;
|
||||||
|
pub mod render;
|
||||||
|
pub mod simulation;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
3
src/hardware/common.rs
Normal file
3
src/hardware/common.rs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
pub const TIMEOUT: Duration = Duration::from_millis(500);
|
36
src/hardware/mod.rs
Normal file
36
src/hardware/mod.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use libptp::{DeviceInfo, StandardCommandCode};
|
||||||
|
use log::debug;
|
||||||
|
use rusb::GlobalContext;
|
||||||
|
|
||||||
|
mod common;
|
||||||
|
mod xt5;
|
||||||
|
|
||||||
|
pub trait Camera {
|
||||||
|
fn vendor_id(&self) -> u16;
|
||||||
|
fn product_id(&self) -> u16;
|
||||||
|
fn name(&self) -> &'static str;
|
||||||
|
|
||||||
|
fn get_device_info(
|
||||||
|
&self,
|
||||||
|
camera: &mut libptp::Camera<GlobalContext>,
|
||||||
|
) -> Result<DeviceInfo, Box<dyn Error + Send + Sync>> {
|
||||||
|
debug!("Using default GetDeviceInfo command for {}", self.name());
|
||||||
|
|
||||||
|
let response = camera.command(
|
||||||
|
StandardCommandCode::GetDeviceInfo,
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
Some(common::TIMEOUT),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
debug!("Received response with {} bytes", response.len());
|
||||||
|
|
||||||
|
let device_info = DeviceInfo::decode(&response)?;
|
||||||
|
|
||||||
|
Ok(device_info)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const SUPPORTED_MODELS: &[&dyn Camera] = &[&xt5::FujifilmXT5];
|
18
src/hardware/xt5.rs
Normal file
18
src/hardware/xt5.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
use super::Camera;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FujifilmXT5;
|
||||||
|
|
||||||
|
impl Camera for FujifilmXT5 {
|
||||||
|
fn vendor_id(&self) -> u16 {
|
||||||
|
0x04cb
|
||||||
|
}
|
||||||
|
|
||||||
|
fn product_id(&self) -> u16 {
|
||||||
|
0x02fc
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"FUJIFILM X-T5"
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,5 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use log4rs::{
|
use log4rs::{
|
||||||
Config,
|
Config,
|
||||||
@@ -6,7 +8,7 @@ use log4rs::{
|
|||||||
encode::pattern::PatternEncoder,
|
encode::pattern::PatternEncoder,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(quiet: bool, verbose: bool) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn init(quiet: bool, verbose: bool) -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
let level = if quiet {
|
let level = if quiet {
|
||||||
LevelFilter::Warn
|
LevelFilter::Warn
|
||||||
} else if verbose {
|
} else if verbose {
|
||||||
|
19
src/main.rs
19
src/main.rs
@@ -1,14 +1,23 @@
|
|||||||
use clap::Parser;
|
use std::error::Error;
|
||||||
mod cli;
|
|
||||||
mod log;
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
use clap::Parser;
|
||||||
|
use cli::Commands;
|
||||||
|
|
||||||
|
mod cli;
|
||||||
|
mod hardware;
|
||||||
|
mod log;
|
||||||
|
mod usb;
|
||||||
|
|
||||||
|
fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
|
||||||
let cli = cli::Cli::parse();
|
let cli = cli::Cli::parse();
|
||||||
|
|
||||||
log::init(cli.quiet, cli.verbose)?;
|
log::init(cli.quiet, cli.verbose)?;
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
_ => {}
|
Commands::Device(device_cmd) => {
|
||||||
|
cli::device::handle(device_cmd, cli.json, cli.device.as_deref())?
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
108
src/usb/mod.rs
Normal file
108
src/usb/mod.rs
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
use std::error::Error;
|
||||||
|
|
||||||
|
use rusb::GlobalContext;
|
||||||
|
|
||||||
|
use crate::hardware::SUPPORTED_MODELS;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Device {
|
||||||
|
pub model: &'static dyn crate::hardware::Camera,
|
||||||
|
pub rusb_device: rusb::Device<GlobalContext>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Device {
|
||||||
|
pub fn camera(&self) -> Result<libptp::Camera<GlobalContext>, Box<dyn Error + Send + Sync>> {
|
||||||
|
let handle = self.rusb_device.open()?;
|
||||||
|
let device = handle.device();
|
||||||
|
let camera = libptp::Camera::new(&device)?;
|
||||||
|
Ok(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.model.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 get_connected_devices() -> Result<Vec<Device>, Box<dyn Error + Send + Sync>> {
|
||||||
|
let mut connected_devices = Vec::new();
|
||||||
|
|
||||||
|
for device in rusb::devices()?.iter() {
|
||||||
|
let descriptor = match device.device_descriptor() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(_) => continue,
|
||||||
|
};
|
||||||
|
|
||||||
|
for model in SUPPORTED_MODELS.iter() {
|
||||||
|
if descriptor.vendor_id() == model.vendor_id()
|
||||||
|
&& descriptor.product_id() == model.product_id()
|
||||||
|
{
|
||||||
|
let connected_device = Device {
|
||||||
|
model: *model,
|
||||||
|
rusb_device: device,
|
||||||
|
};
|
||||||
|
|
||||||
|
connected_devices.push(connected_device);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(connected_devices)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_connected_device_by_id(id: &str) -> Result<Device, 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 model in SUPPORTED_MODELS.iter() {
|
||||||
|
if descriptor.vendor_id() == model.vendor_id()
|
||||||
|
&& descriptor.product_id() == model.product_id()
|
||||||
|
{
|
||||||
|
return Ok(Device {
|
||||||
|
model: *model,
|
||||||
|
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_device(device_id: Option<&str>) -> Result<Device, Box<dyn Error + Send + Sync>> {
|
||||||
|
match device_id {
|
||||||
|
Some(id) => get_connected_device_by_id(id),
|
||||||
|
None => get_connected_devices()?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| "No supported devices connected.".into()),
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user