use crate::{
abilities::Abilities,
file::{CameraFile, CameraFilePath},
filesys::{CameraFS, StorageInfo},
helper::{as_ref, char_slice_to_cow, chars_to_string, to_c_string, UninitBox},
port::PortInfo,
task::{BackgroundPtr, Task},
try_gp_internal,
widget::{GroupWidget, Widget, WidgetBase},
Context, Error, Result,
};
use std::{ffi, os::raw::c_char, time::Duration};
#[derive(Debug)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub enum CameraEvent {
Unknown(String),
Timeout,
NewFile(CameraFilePath),
FileChanged(CameraFilePath),
NewFolder(CameraFilePath),
CaptureComplete,
}
pub struct Camera {
pub(crate) camera: BackgroundPtr<libgphoto2_sys::Camera>,
pub(crate) context: Context,
}
impl Clone for Camera {
fn clone(&self) -> Self {
try_gp_internal!(gp_camera_ref(*self.camera).unwrap());
Self { camera: self.camera, context: self.context.clone() }
}
}
impl Drop for Camera {
fn drop(&mut self) {
let camera = self.camera;
unsafe {
Task::new(move || -> Result<()> {
try_gp_internal!(gp_camera_unref(*camera)?);
Ok(())
})
}
.wait()
.unwrap()
}
}
as_ref!(Camera -> libgphoto2_sys::Camera, **self.camera);
impl Camera {
pub(crate) fn new(camera: BackgroundPtr<libgphoto2_sys::Camera>, context: Context) -> Self {
Self { camera, context }
}
pub fn capture_image(&self) -> Task<Result<CameraFilePath>> {
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
let mut inner = UninitBox::uninit();
try_gp_internal!(gp_camera_capture(
*camera,
libgphoto2_sys::CameraCaptureType::GP_CAPTURE_IMAGE,
inner.as_mut_ptr(),
*context
)?);
Ok(CameraFilePath { inner: inner.assume_init() })
})
}
.context(context)
}
pub fn trigger_capture(&self) -> Task<Result<()>> {
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
try_gp_internal!(gp_camera_trigger_capture(*camera, *context)?);
Ok(())
})
}
.context(context)
}
pub fn capture_preview(&self) -> Task<Result<CameraFile>> {
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
let camera_file = CameraFile::new()?;
try_gp_internal!(gp_camera_capture_preview(*camera, *camera_file.inner, *context)?);
Ok(camera_file)
})
}
.context(context)
}
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) -> Task<Result<Vec<StorageInfo>>> {
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
try_gp_internal!(gp_camera_get_storageinfo(
*camera,
&out storages_ptr,
&out storages_len,
*context
)?);
let storages = std::slice::from_raw_parts(
storages_ptr.cast::<StorageInfo>(),
storages_len.try_into()?,
);
let result = storages.to_vec();
libc::free(storages_ptr.cast());
Ok(result)
})
}
.context(context)
}
pub fn fs(&self) -> CameraFS<'_> {
CameraFS::new(self)
}
pub fn wait_event(&self, timeout: Duration) -> Task<Result<CameraEvent>> {
use libgphoto2_sys::CameraEventType;
let duration_milliseconds = timeout.as_millis();
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
try_gp_internal!(gp_camera_wait_for_event(
*camera,
duration_milliseconds.try_into()?,
&out event_type,
&out event_data,
*context
)?);
Ok(match event_type {
CameraEventType::GP_EVENT_UNKNOWN => {
let s = chars_to_string(event_data.cast::<c_char>());
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(*event_data.cast::<libgphoto2_sys::CameraFilePath>()),
};
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,
})
})
}
.context(context)
}
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) -> Task<Result<GroupWidget>> {
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
try_gp_internal!(gp_camera_get_config(*camera, &out root_widget, *context)?);
Widget::new_owned(BackgroundPtr(root_widget)).try_into::<GroupWidget>()
})
}
.context(context)
}
pub fn config_key<T: TryFrom<Widget> + 'static + Send>(&self, key: &str) -> Task<Result<T>>
where
Error: From<T::Error>,
{
let key = key.to_owned();
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
try_gp_internal!(gp_camera_get_single_config(
*camera,
to_c_string!(&*key),
&out widget,
*context
)?);
Ok(Widget::new_owned(BackgroundPtr(widget)).try_into()?)
})
}
.context(context)
}
pub fn set_all_config(&self, config: &GroupWidget) -> Task<Result<()>> {
let config = config.clone();
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
try_gp_internal!(gp_camera_set_config(*camera, *config.inner, *context)?);
Ok(())
})
}
.context(self.context.inner)
}
pub fn set_config(&self, config: &WidgetBase) -> Task<Result<()>> {
let config = config.clone();
let camera = self.camera;
let context = self.context.inner;
unsafe {
Task::new(move || {
try_gp_internal!(gp_camera_set_single_config(
*camera,
to_c_string!(config.name()),
*config.inner,
*context
)?);
Ok(())
})
}
.context(context)
}
}
impl AsRef<Context> for &Camera {
fn as_ref(&self) -> &Context {
&self.context
}
}
#[cfg(all(test, feature = "test"))]
mod tests {
const _: () = {
const fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<super::Camera>()
};
fn sample_camera() -> super::Camera {
crate::sample_context().autodetect_camera().wait().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().wait().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)
.wait()
.unwrap()
.map(|folder_name| {
let folder = Self::collect(fs, &format!("{path}/{folder_name}"));
(folder_name, folder)
})
.collect(),
files: fs
.list_files(path)
.wait()
.unwrap()
.map(|file_name| {
let mut file_info = fs.file_info(path, &file_name).wait().unwrap();
file_info.inner.file.mtime = 42;
(file_name, file_info)
})
.collect(),
}
}
}
let camera = sample_camera();
let captured_file_path = camera.capture_image().wait().unwrap();
insta::assert_debug_snapshot!(captured_file_path);
let captured_file = camera
.fs()
.download(&captured_file_path.folder(), &captured_file_path.name())
.wait()
.unwrap();
unsafe {
libgphoto2_sys::gp_file_set_mtime(*captured_file.inner, 42);
}
insta::assert_debug_snapshot!(captured_file);
assert_eq!(
captured_file.get_data(&camera.context).wait().unwrap().as_ref(),
libgphoto2_sys::test_utils::SAMPLE_IMAGE
);
let fs = camera.fs();
let storages = camera.storages().wait().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().wait().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);
}
}