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}