use crate::{
abilities::Abilities,
file::{CameraFile, CameraFilePath},
filesys::{CameraFS, StorageInfo},
helper::{as_ref, char_slice_to_cow, chars_to_string, libtool_lock, to_c_string, UninitBox},
port::PortInfo,
try_gp_internal,
widget::{GroupWidget, Widget, WidgetBase},
Context, Error, Result,
};
use std::{ffi, os::raw::c_char, time::Duration};
#[derive(Debug)]
pub enum CameraEvent {
Unknown(String),
Timeout,
NewFile(CameraFilePath),
FileChanged(CameraFilePath),
NewFolder(CameraFilePath),
CaptureComplete,
}
pub struct Camera {
pub(crate) camera: *mut libgphoto2_sys::Camera,
pub(crate) context: Context,
}
impl Drop for Camera {
fn drop(&mut self) {
let _lock = libtool_lock();
try_gp_internal!(gp_camera_unref(self.camera).unwrap());
}
}
as_ref!(Camera -> libgphoto2_sys::Camera, *self.camera);
impl Camera {
pub(crate) fn new(camera: *mut libgphoto2_sys::Camera, context: Context) -> Self {
Self { camera, context }
}
pub fn capture_image(&self) -> Result<CameraFilePath> {
let mut inner = UninitBox::uninit();
try_gp_internal!(gp_camera_capture(
self.camera,
libgphoto2_sys::CameraCaptureType::GP_CAPTURE_IMAGE,
inner.as_mut_ptr(),
self.context.inner
)?);
Ok(CameraFilePath { inner: unsafe { inner.assume_init() } })
}
pub fn capture_preview(&self) -> Result<CameraFile> {
let camera_file = CameraFile::new()?;
try_gp_internal!(gp_camera_capture_preview(
self.camera,
camera_file.inner,
self.context.inner
)?);
Ok(camera_file)
}
pub fn abilities(&self) -> Abilities {
let mut inner = UninitBox::uninit();
try_gp_internal!(gp_camera_get_abilities(self.camera, inner.as_mut_ptr()).unwrap());
Abilities { inner: unsafe { inner.assume_init() } }
}
pub fn summary(&self) -> Result<String> {
try_gp_internal!(gp_camera_get_summary(self.camera, &out summary, self.context.inner)?);
Ok(char_slice_to_cow(&summary.text).into_owned())
}
pub fn about(&self) -> Result<String> {
try_gp_internal!(gp_camera_get_about(self.camera, &out about, self.context.inner)?);
Ok(char_slice_to_cow(&about.text).into_owned())
}
pub fn manual(&self) -> Result<String> {
try_gp_internal!(gp_camera_get_manual(self.camera, &out manual, self.context.inner)?);
Ok(char_slice_to_cow(&manual.text).into_owned())
}
pub fn storages(&self) -> Result<Vec<StorageInfo>> {
try_gp_internal!(gp_camera_get_storageinfo(
self.camera,
&out storages_ptr,
&out storages_len,
self.context.inner
)?);
let storages = unsafe {
std::slice::from_raw_parts(
storages_ptr.cast::<StorageInfo>(),
storages_len.try_into()?,
)
};
let result = storages.to_vec();
unsafe {
libc::free(storages_ptr.cast());
}
Ok(result)
}
pub fn fs(&self) -> CameraFS<'_> {
CameraFS::new(self)
}
pub fn wait_event(&self, timeout: Duration) -> Result<CameraEvent> {
use libgphoto2_sys::CameraEventType;
let duration_milliseconds = timeout.as_millis();
try_gp_internal!(gp_camera_wait_for_event(
self.camera,
duration_milliseconds.try_into()?,
&out event_type,
&out event_data,
self.context.inner
)?);
Ok(match event_type {
CameraEventType::GP_EVENT_UNKNOWN => {
let s = chars_to_string(event_data.cast::<c_char>());
unsafe {
libc::free(event_data);
}
CameraEvent::Unknown(s)
}
CameraEventType::GP_EVENT_TIMEOUT => CameraEvent::Timeout,
CameraEventType::GP_EVENT_FILE_ADDED
| CameraEventType::GP_EVENT_FOLDER_ADDED
| CameraEventType::GP_EVENT_FILE_CHANGED => {
let file_path = CameraFilePath {
inner: Box::new(unsafe { *event_data.cast::<libgphoto2_sys::CameraFilePath>() }),
};
unsafe {
libc::free(event_data);
}
match event_type {
CameraEventType::GP_EVENT_FILE_ADDED => CameraEvent::NewFile(file_path),
CameraEventType::GP_EVENT_FOLDER_ADDED => CameraEvent::NewFolder(file_path),
CameraEventType::GP_EVENT_FILE_CHANGED => CameraEvent::FileChanged(file_path),
_ => unreachable!(),
}
}
CameraEventType::GP_EVENT_CAPTURE_COMPLETE => CameraEvent::CaptureComplete,
})
}
pub fn port_info(&self) -> Result<PortInfo<'_>> {
try_gp_internal!(gp_camera_get_port_info(self.camera, &out port_info)?);
Ok(unsafe { PortInfo::new(port_info) })
}
pub fn config(&self) -> Result<GroupWidget> {
try_gp_internal!(gp_camera_get_config(self.camera, &out root_widget, self.context.inner)?);
Widget::new_owned(root_widget).try_into::<GroupWidget>()
}
pub fn config_key<T: TryFrom<Widget>>(&self, key: &str) -> Result<T>
where
Error: From<T::Error>,
{
try_gp_internal!(gp_camera_get_single_config(
self.camera,
to_c_string!(key),
&out widget,
self.context.inner
)?);
Ok(Widget::new_owned(widget).try_into()?)
}
pub fn set_all_config(&self, config: &GroupWidget) -> Result<()> {
try_gp_internal!(gp_camera_set_config(self.camera, config.inner, self.context.inner)?);
Ok(())
}
pub fn set_config(&self, config: &WidgetBase) -> Result<()> {
try_gp_internal!(gp_camera_set_single_config(
self.camera,
to_c_string!(config.name()),
config.inner,
self.context.inner
)?);
Ok(())
}
}
#[cfg(all(test, feature = "test"))]
mod tests {
fn sample_camera() -> super::Camera {
crate::sample_context().autodetect_camera().unwrap()
}
#[test]
fn test_abilities() {
let abilities = sample_camera().abilities();
insta::assert_debug_snapshot!(abilities);
}
#[test]
fn test_summary() {
let mut summary = sample_camera().summary().unwrap_or_default();
let prefix = "Date & Time(0x5011):(readwrite) (type=0xffff)";
summary = summary
.lines()
.map(|line| {
let mut line = line.to_owned();
if line.starts_with(prefix) {
line.replace_range(prefix.len().., " (omitted timestamp)");
}
line + "\n"
})
.collect();
insta::assert_snapshot!(summary);
}
#[test]
fn test_about() {
let about = sample_camera().about().unwrap_or_default();
insta::assert_snapshot!(about);
}
#[test]
fn test_manual() {
let manual = sample_camera().manual().unwrap_or_default();
insta::assert_snapshot!(manual);
}
#[test]
fn test_storages() {
let storages = sample_camera().storages().unwrap();
insta::assert_debug_snapshot!(storages);
}
#[test]
fn test_fs() {
use crate::filesys::{CameraFS, FileInfo};
use std::collections::BTreeMap;
#[derive(Debug)]
#[allow(dead_code)]
struct FolderDbg {
folders: BTreeMap<String, FolderDbg>,
files: BTreeMap<String, FileInfo>,
}
impl FolderDbg {
fn collect(fs: &CameraFS, path: &str) -> FolderDbg {
FolderDbg {
folders: fs
.list_folders(path)
.unwrap()
.map(|folder_name| {
let folder = Self::collect(fs, &format!("{path}/{folder_name}"));
(folder_name, folder)
})
.collect(),
files: fs
.list_files(path)
.unwrap()
.map(|file_name| {
let mut file_info = fs.file_info(path, &file_name).unwrap();
file_info.inner.file.mtime = 42;
(file_name, file_info)
})
.collect(),
}
}
}
let camera = sample_camera();
let captured_file_path = camera.capture_image().unwrap();
insta::assert_debug_snapshot!(captured_file_path);
let captured_file =
camera.fs().download(&captured_file_path.folder(), &captured_file_path.name()).unwrap();
unsafe {
libgphoto2_sys::gp_file_set_mtime(captured_file.inner, 42);
}
insta::assert_debug_snapshot!(captured_file);
assert_eq!(
captured_file.get_data().unwrap().as_ref(),
libgphoto2_sys::test_utils::SAMPLE_IMAGE
);
let fs = camera.fs();
let storages = camera.storages().unwrap();
let storage_folders = storages
.iter()
.map(|storage| {
let base_dir = storage.base_directory().unwrap();
let folder_tree = FolderDbg::collect(&fs, &base_dir);
(base_dir, folder_tree)
})
.collect::<BTreeMap<_, _>>();
insta::assert_debug_snapshot!(storage_folders);
}
#[test]
fn test_port_info() {
let camera = sample_camera();
let port_info = camera.port_info().unwrap();
insta::assert_debug_snapshot!(port_info);
}
#[test]
fn test_config() {
use crate::widget::{DateWidget, TextWidget};
let widget_tree = sample_camera().config().unwrap();
widget_tree
.get_child_by_label("Date & Time")
.unwrap()
.try_into::<TextWidget>()
.unwrap()
.set_value("(omitted timestamp)")
.unwrap();
widget_tree
.get_child_by_name("datetime")
.unwrap()
.try_into::<DateWidget>()
.unwrap()
.set_timestamp(42);
insta::assert_debug_snapshot!(widget_tree);
}
}