laser_dac/backend.rs
1//! DAC backend traits and implementations for the streaming API.
2//!
3//! This module provides the backend trait hierarchy that all DAC backends must
4//! implement:
5//!
6//! - [`DacBackend`] — common device lifecycle (connect, disconnect, shutter, stop)
7//! - [`FifoBackend`] — FIFO/queue-based DACs (Ether Dream, IDN, LaserCube, AVB)
8//! - [`FrameSwapBackend`] — double-buffered frame DACs (Helios)
9//!
10//! The [`BackendKind`] enum wraps either variant for use in the stream scheduler.
11
12use crate::buffer_estimate::BufferEstimator;
13use crate::device::{DacCapabilities, DacType};
14use crate::point::LaserPoint;
15
16// Re-export error types for backwards compatibility
17pub use crate::error::{Error, Result};
18
19// =============================================================================
20// Write Outcome
21// =============================================================================
22
23/// Write result from a backend point/frame submission.
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum WriteOutcome {
26 /// The data was accepted and written.
27 Written,
28 /// The device cannot accept more data right now.
29 WouldBlock,
30}
31
32// =============================================================================
33// DacBackend Trait — common device lifecycle
34// =============================================================================
35
36/// Common backend trait for all DAC device types.
37///
38/// Provides device lifecycle management (connect, disconnect, stop, shutter)
39/// and capability/type queries. All specific backend traits extend this.
40pub trait DacBackend: Send + 'static {
41 /// Returns the DAC type for this backend.
42 fn dac_type(&self) -> DacType;
43
44 /// Returns the device capabilities.
45 fn caps(&self) -> &DacCapabilities;
46
47 /// Connect to the device.
48 fn connect(&mut self) -> Result<()>;
49
50 /// Disconnect from the device.
51 fn disconnect(&mut self) -> Result<()>;
52
53 /// Returns whether the device is connected.
54 fn is_connected(&self) -> bool;
55
56 /// Stop output (if supported by the device).
57 fn stop(&mut self) -> Result<()>;
58
59 /// Open/close the shutter (if supported by the device).
60 fn set_shutter(&mut self, open: bool) -> Result<()>;
61}
62
63// =============================================================================
64// FifoBackend Trait — queue/FIFO based DACs
65// =============================================================================
66
67/// Backend trait for FIFO/queue-based DACs.
68///
69/// These DACs accept arbitrary-sized chunks of points into a queue or buffer.
70/// The stream scheduler tops up the buffer to maintain a target level.
71///
72/// Implementations: Ether Dream, IDN, LaserCube Network, LaserCube USB, AVB.
73pub trait FifoBackend: DacBackend {
74 /// Attempt to write points at the given PPS.
75 ///
76 /// # Contract
77 ///
78 /// This is the core backpressure mechanism. Implementations must:
79 ///
80 /// 1. Return `WriteOutcome::WouldBlock` when the device cannot accept more data
81 /// (buffer full, not ready, etc.).
82 /// 2. Return `WriteOutcome::Written` when the points were accepted.
83 /// 3. Return `Err(...)` only for actual errors (disconnection, protocol errors).
84 fn try_write_points(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome>;
85
86 /// The protocol-owned [`BufferEstimator`] strategy.
87 ///
88 /// Read-only: backends mutate their concrete strategy internally through
89 /// protocol-specific event hooks. Adapters (and any other observers) only
90 /// query estimated fullness via this getter.
91 fn estimator(&self) -> &dyn BufferEstimator;
92
93 /// Clear the device-side queue (drop all buffered-but-unplayed points) and
94 /// reset queue-depth bookkeeping.
95 ///
96 /// Called by the scheduler when re-arming an
97 /// [`OutputModel::BlockingFifo`](crate::device::OutputModel::BlockingFifo)
98 /// device whose hardware ring does not drain while output is disabled
99 /// (e.g. LaserCube USB), so stale points do not replay on re-arm. The
100 /// default is a no-op: FIFO devices that keep draining while disarmed empty
101 /// their queue on their own and have nothing to clear.
102 fn reset_device_buffer(&mut self) -> Result<()> {
103 Ok(())
104 }
105}
106
107// =============================================================================
108// FrameSwapBackend Trait — double-buffered frame DACs
109// =============================================================================
110
111/// Backend trait for double-buffered frame-swap DACs.
112///
113/// These DACs accept complete frames that replace the previous frame atomically.
114/// The device holds at most one pending frame at a time.
115///
116/// Implementations: Helios.
117pub trait FrameSwapBackend: DacBackend {
118 /// Maximum number of points the device can accept in a single frame.
119 fn frame_capacity(&self) -> usize;
120
121 /// Returns true if the device is ready to accept a new frame.
122 ///
123 /// For Helios, this queries the USB device status.
124 fn is_ready_for_frame(&mut self) -> bool;
125
126 /// Write a complete frame at the given PPS.
127 ///
128 /// The caller should check `is_ready_for_frame()` first, but implementations
129 /// may still return `WouldBlock` for race conditions.
130 fn write_frame(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome>;
131
132 /// Write a complete frame immediately after a successful readiness check.
133 ///
134 /// The default implementation preserves the conservative `write_frame()`
135 /// behavior. Backends with expensive or fragile readiness probes can override
136 /// this to avoid a duplicate readiness check.
137 fn write_frame_ready(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome> {
138 self.write_frame(pps, points)
139 }
140}
141
142// =============================================================================
143// BackendKind — type-erased wrapper
144// =============================================================================
145
146/// Type-erased backend wrapper for use in the stream scheduler.
147///
148/// Wraps either a [`FifoBackend`] or a [`FrameSwapBackend`], providing
149/// delegation for common [`DacBackend`] methods and a unified write path.
150pub enum BackendKind {
151 /// A FIFO/queue-based backend.
152 Fifo(Box<dyn FifoBackend>),
153 /// A double-buffered frame-swap backend.
154 FrameSwap(Box<dyn FrameSwapBackend>),
155}
156
157impl BackendKind {
158 // =========================================================================
159 // DacBackend delegation
160 // =========================================================================
161
162 /// Returns the DAC type.
163 pub fn dac_type(&self) -> DacType {
164 match self {
165 BackendKind::Fifo(b) => b.dac_type(),
166 BackendKind::FrameSwap(b) => b.dac_type(),
167 }
168 }
169
170 /// Returns the device capabilities.
171 pub fn caps(&self) -> &DacCapabilities {
172 match self {
173 BackendKind::Fifo(b) => b.caps(),
174 BackendKind::FrameSwap(b) => b.caps(),
175 }
176 }
177
178 /// Connect to the device.
179 pub fn connect(&mut self) -> Result<()> {
180 match self {
181 BackendKind::Fifo(b) => b.connect(),
182 BackendKind::FrameSwap(b) => b.connect(),
183 }
184 }
185
186 /// Disconnect from the device.
187 pub fn disconnect(&mut self) -> Result<()> {
188 match self {
189 BackendKind::Fifo(b) => b.disconnect(),
190 BackendKind::FrameSwap(b) => b.disconnect(),
191 }
192 }
193
194 /// Returns whether the device is connected.
195 pub fn is_connected(&self) -> bool {
196 match self {
197 BackendKind::Fifo(b) => b.is_connected(),
198 BackendKind::FrameSwap(b) => b.is_connected(),
199 }
200 }
201
202 /// Stop output.
203 pub fn stop(&mut self) -> Result<()> {
204 match self {
205 BackendKind::Fifo(b) => b.stop(),
206 BackendKind::FrameSwap(b) => b.stop(),
207 }
208 }
209
210 /// Open/close the shutter.
211 pub fn set_shutter(&mut self, open: bool) -> Result<()> {
212 match self {
213 BackendKind::Fifo(b) => b.set_shutter(open),
214 BackendKind::FrameSwap(b) => b.set_shutter(open),
215 }
216 }
217
218 // =========================================================================
219 // Write dispatch
220 // =========================================================================
221
222 /// Write points to the backend (dispatches to the appropriate method).
223 ///
224 /// - For FIFO backends: calls `try_write_points()`
225 /// - For frame-swap backends: calls `write_frame()`
226 pub fn try_write(&mut self, pps: u32, points: &[LaserPoint]) -> Result<WriteOutcome> {
227 match self {
228 BackendKind::Fifo(b) => b.try_write_points(pps, points),
229 BackendKind::FrameSwap(b) => b.write_frame(pps, points),
230 }
231 }
232
233 pub(crate) fn try_write_frame_ready(
234 &mut self,
235 pps: u32,
236 points: &[LaserPoint],
237 ) -> Result<WriteOutcome> {
238 match self {
239 BackendKind::Fifo(b) => b.try_write_points(pps, points),
240 BackendKind::FrameSwap(b) => b.write_frame_ready(pps, points),
241 }
242 }
243
244 // =========================================================================
245 // Query helpers
246 // =========================================================================
247
248 /// The protocol-owned [`BufferEstimator`] for FIFO backends.
249 ///
250 /// Frame-swap backends never queue points, so they return `None`.
251 pub fn estimator(&self) -> Option<&dyn BufferEstimator> {
252 match self {
253 BackendKind::Fifo(b) => Some(b.estimator()),
254 BackendKind::FrameSwap(_) => None,
255 }
256 }
257
258 /// Clear the device-side queue and reset queue-depth bookkeeping.
259 ///
260 /// Delegates to [`FifoBackend::reset_device_buffer`]; frame-swap backends
261 /// never queue points, so this is a no-op for them.
262 pub fn reset_device_buffer(&mut self) -> Result<()> {
263 match self {
264 BackendKind::Fifo(b) => b.reset_device_buffer(),
265 BackendKind::FrameSwap(_) => Ok(()),
266 }
267 }
268
269 /// Returns `true` if this is a frame-swap backend.
270 pub fn is_frame_swap(&self) -> bool {
271 self.caps().output_model == crate::device::OutputModel::UsbFrameSwap
272 }
273
274 /// Returns true if the device is ready to accept a new frame.
275 ///
276 /// For FIFO backends, always returns `true` (they handle backpressure via `try_write`).
277 /// For frame-swap backends, queries the device readiness.
278 pub fn is_ready_for_frame(&mut self) -> bool {
279 match self {
280 BackendKind::Fifo(_) => true,
281 BackendKind::FrameSwap(b) => b.is_ready_for_frame(),
282 }
283 }
284
285 /// Returns the frame capacity for frame-swap backends, or `None` for FIFO.
286 pub fn frame_capacity(&self) -> Option<usize> {
287 match self {
288 BackendKind::Fifo(_) => None,
289 BackendKind::FrameSwap(b) => Some(b.frame_capacity()),
290 }
291 }
292}
293
294// =============================================================================
295// Re-exports from protocol-specific backends
296// =============================================================================
297
298#[cfg(feature = "helios")]
299pub use crate::protocols::helios::HeliosBackend;
300
301#[cfg(feature = "ether-dream")]
302pub use crate::protocols::ether_dream::EtherDreamBackend;
303
304#[cfg(feature = "idn")]
305pub use crate::protocols::idn::IdnBackend;
306
307#[cfg(feature = "lasercube-network")]
308pub use crate::protocols::lasercube_network::LaserCubeNetworkBackend;
309
310#[cfg(feature = "lasercube-usb")]
311pub use crate::protocols::lasercube_usb::LaserCubeUsbBackend;
312
313#[cfg(feature = "oscilloscope")]
314pub use crate::protocols::oscilloscope::OscilloscopeBackend;
315
316#[cfg(feature = "avb")]
317pub use crate::protocols::avb::AvbBackend;
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use crate::device::{DacCapabilities, DacType, OutputModel};
323
324 /// Stub FIFO backend with a configurable `OutputModel`.
325 struct StubFifo {
326 caps: DacCapabilities,
327 estimator: crate::buffer_estimate::SoftwareDecayEstimator,
328 }
329 impl DacBackend for StubFifo {
330 fn dac_type(&self) -> DacType {
331 DacType::Custom("stub".into())
332 }
333 fn caps(&self) -> &DacCapabilities {
334 &self.caps
335 }
336 fn connect(&mut self) -> Result<()> {
337 Ok(())
338 }
339 fn disconnect(&mut self) -> Result<()> {
340 Ok(())
341 }
342 fn is_connected(&self) -> bool {
343 true
344 }
345 fn stop(&mut self) -> Result<()> {
346 Ok(())
347 }
348 fn set_shutter(&mut self, _open: bool) -> Result<()> {
349 Ok(())
350 }
351 }
352 impl FifoBackend for StubFifo {
353 fn try_write_points(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
354 Ok(WriteOutcome::Written)
355 }
356
357 fn estimator(&self) -> &dyn BufferEstimator {
358 &self.estimator
359 }
360 }
361
362 /// Stub frame-swap backend (always reports `UsbFrameSwap`).
363 struct StubFrameSwap;
364 impl DacBackend for StubFrameSwap {
365 fn dac_type(&self) -> DacType {
366 DacType::Custom("stub-fs".into())
367 }
368 fn caps(&self) -> &DacCapabilities {
369 static CAPS: std::sync::OnceLock<DacCapabilities> = std::sync::OnceLock::new();
370 CAPS.get_or_init(|| DacCapabilities {
371 output_model: OutputModel::UsbFrameSwap,
372 ..DacCapabilities::default()
373 })
374 }
375 fn connect(&mut self) -> Result<()> {
376 Ok(())
377 }
378 fn disconnect(&mut self) -> Result<()> {
379 Ok(())
380 }
381 fn is_connected(&self) -> bool {
382 true
383 }
384 fn stop(&mut self) -> Result<()> {
385 Ok(())
386 }
387 fn set_shutter(&mut self, _open: bool) -> Result<()> {
388 Ok(())
389 }
390 }
391 impl FrameSwapBackend for StubFrameSwap {
392 fn frame_capacity(&self) -> usize {
393 4096
394 }
395 fn is_ready_for_frame(&mut self) -> bool {
396 true
397 }
398 fn write_frame(&mut self, _pps: u32, _points: &[LaserPoint]) -> Result<WriteOutcome> {
399 Ok(WriteOutcome::Written)
400 }
401 }
402
403 #[test]
404 fn is_frame_swap_matches_output_model_usb_frame_swap() {
405 for model in [
406 OutputModel::NetworkFifo,
407 OutputModel::UdpTimed,
408 OutputModel::UsbFrameSwap,
409 ] {
410 let caps = DacCapabilities {
411 output_model: model.clone(),
412 ..DacCapabilities::default()
413 };
414 let kind = BackendKind::Fifo(Box::new(StubFifo {
415 caps,
416 estimator: crate::buffer_estimate::SoftwareDecayEstimator::new(),
417 }));
418 assert_eq!(
419 kind.is_frame_swap(),
420 model == OutputModel::UsbFrameSwap,
421 "Fifo wrapper with model {:?}",
422 model
423 );
424 }
425
426 let fs = BackendKind::FrameSwap(Box::new(StubFrameSwap));
427 assert!(fs.is_frame_swap(), "FrameSwap wrapper should report true");
428 assert_eq!(fs.caps().output_model, OutputModel::UsbFrameSwap);
429 }
430}