Skip to main content

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}