Skip to main content

vmb_core/
camera.rs

1//! Safe wrapper around an opened Vimba X camera.
2//!
3//! [`Camera`] is generic over any [`VmbRuntime`] and drives the
4//! announce / queue / capture-start dance entirely through the port —
5//! with zero FFI or `unsafe` in this module.
6
7use std::path::Path;
8use std::sync::Arc;
9
10use crate::callback::FrameCallback;
11use crate::frame::Frame;
12use crate::port::VmbRuntime;
13use crate::types::{CameraHandle, FrameCallbackId, FrameSlotId};
14use crate::{Result, VmbError};
15
16/// Command features issued during the capture lifecycle. These GenICam
17/// names are stable across Vimba SDK versions; hoisting them to consts
18/// makes SDK-upgrade audits a one-grep review.
19const FEATURE_ACQUISITION_START: &str = "AcquisitionStart";
20const FEATURE_ACQUISITION_STOP: &str = "AcquisitionStop";
21
22/// An in-progress capture session — the set of announced slots plus the
23/// installed callback identifier, both of which must be unwound on
24/// teardown.
25struct CaptureSession {
26    callback_id: FrameCallbackId,
27    #[allow(dead_code)]
28    slots: Vec<FrameSlotId>,
29}
30
31/// Open handle to a Vimba camera.
32///
33/// Dropping the camera cleanly ends any running capture (via
34/// [`Camera::stop_capture`]) and closes the adapter-side resources.
35pub struct Camera<R: VmbRuntime> {
36    runtime: Arc<R>,
37    handle: CameraHandle,
38    id: String,
39    session: Option<CaptureSession>,
40}
41
42impl<R: VmbRuntime> Camera<R> {
43    /// Open a camera by its transport-layer ID. Usually called via
44    /// [`VmbSystem::open_camera`](crate::system::VmbSystem::open_camera).
45    pub fn open(runtime: Arc<R>, id: &str) -> Result<Self> {
46        let handle = runtime.open_camera(id)?;
47        Ok(Self {
48            runtime,
49            handle,
50            id: id.to_string(),
51            session: None,
52        })
53    }
54
55    /// The camera ID originally passed to [`Camera::open`].
56    pub fn id(&self) -> &str {
57        &self.id
58    }
59
60    /// Load a Vimba settings XML (day/night profile).
61    pub fn load_settings(&self, path: &Path) -> Result<()> {
62        self.runtime.load_settings(self.handle, path)
63    }
64
65    /// Start continuous capture.
66    ///
67    /// The closure is invoked for every received frame; it MUST be fast
68    /// and immediately copy the frame bytes (the adapter re-queues the
69    /// buffer as soon as the callback returns). The closure may run on
70    /// any thread the adapter chooses and must be `Send + Sync`.
71    ///
72    /// `num_buffers` is the number of frame buffers to pre-announce; 4
73    /// is a reasonable default.
74    ///
75    /// # Cleanup contract
76    ///
77    /// All resources claimed between the first `announce_frame` and the
78    /// final `Ok(())` are unwound on any error path before returning.
79    /// This guarantees `self.session` is only populated when the
80    /// adapter is fully primed — preventing a latent use-after-free
81    /// where the SDK could otherwise hold pointers into callback
82    /// allocations that get dropped when `Camera::drop` skips
83    /// `stop_capture`.
84    pub fn start_capture<F>(&mut self, num_buffers: usize, callback: F) -> Result<()>
85    where
86        F: for<'a> Fn(&Frame<'a>) + Send + Sync + 'static,
87    {
88        if self.session.is_some() {
89            return Err(VmbError::CaptureAlreadyRunning);
90        }
91
92        let payload = self.runtime.payload_size(self.handle)?;
93        let callback = Arc::new(FrameCallback::new(callback));
94        let callback_id = self.runtime.install_frame_callback(callback);
95
96        let mut slots: Vec<FrameSlotId> = Vec::with_capacity(num_buffers);
97
98        let result: Result<()> = (|| {
99            for _ in 0..num_buffers {
100                let slot = self.runtime.announce_frame(self.handle, payload)?;
101                slots.push(slot);
102            }
103            self.runtime.capture_start(self.handle)?;
104            for slot in &slots {
105                self.runtime.queue_frame(self.handle, *slot, callback_id)?;
106            }
107            self.runtime
108                .run_feature_command(self.handle, FEATURE_ACQUISITION_START)?;
109            Ok(())
110        })();
111
112        match result {
113            Ok(()) => {
114                self.session = Some(CaptureSession { callback_id, slots });
115                Ok(())
116            }
117            Err(e) => {
118                // Best-effort teardown — the domain always calls all
119                // three even if one wasn't reached, since each is
120                // documented as a safe no-op otherwise.
121                self.runtime.capture_end(self.handle);
122                self.runtime.capture_queue_flush(self.handle);
123                self.runtime.frame_revoke_all(self.handle);
124                self.runtime.uninstall_frame_callback(callback_id);
125                Err(e)
126            }
127        }
128    }
129
130    /// Stop an in-progress capture. Safe to call when no capture is
131    /// running — the call is a no-op in that case.
132    pub fn stop_capture(&mut self) -> Result<()> {
133        let Some(session) = self.session.take() else {
134            return Ok(());
135        };
136        // Best-effort teardown. Errors on these calls are deliberately
137        // swallowed because we cannot recover from a partial teardown
138        // failure mid-shutdown.
139        let _ = self
140            .runtime
141            .run_feature_command(self.handle, FEATURE_ACQUISITION_STOP);
142        self.runtime.capture_end(self.handle);
143        self.runtime.capture_queue_flush(self.handle);
144        self.runtime.frame_revoke_all(self.handle);
145        self.runtime.uninstall_frame_callback(session.callback_id);
146        Ok(())
147    }
148}
149
150impl<R: VmbRuntime> Drop for Camera<R> {
151    fn drop(&mut self) {
152        if self.session.is_some() {
153            let _ = self.stop_capture();
154        }
155        self.runtime.close_camera(self.handle);
156    }
157}
158
159impl<R: VmbRuntime> std::fmt::Debug for Camera<R> {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("Camera")
162            .field("id", &self.id)
163            .field("handle", &self.handle)
164            .field("capture_running", &self.session.is_some())
165            .finish()
166    }
167}