laser_dac/
backend.rs

1//! DAC backend abstraction and point conversion.
2//!
3//! Provides a unified [`DacBackend`] trait for all DAC types and handles
4//! point conversion from [`LaserFrame`] to device-specific formats.
5
6use crate::error::{Error, Result};
7use crate::types::{DacType, LaserFrame};
8
9// =============================================================================
10// DAC Backend Trait
11// =============================================================================
12
13/// Result of attempting to write a frame to a DAC.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub enum WriteResult {
17    /// Frame was successfully written.
18    Written,
19    /// Device was busy, frame was dropped.
20    DeviceBusy,
21}
22
23mod private {
24    pub trait Sealed {}
25}
26
27/// Unified interface for all DAC backends.
28///
29/// Each backend handles its own point conversion and device-specific protocol.
30///
31/// This trait is sealed and cannot be implemented outside of this crate.
32pub trait DacBackend: private::Sealed + Send + 'static {
33    /// Get the DAC type.
34    fn dac_type(&self) -> DacType;
35
36    /// Connect to the DAC.
37    fn connect(&mut self) -> Result<()>;
38
39    /// Disconnect from the DAC.
40    fn disconnect(&mut self) -> Result<()>;
41
42    /// Check if connected to the DAC.
43    fn is_connected(&self) -> bool;
44
45    /// Write a frame to the DAC.
46    ///
47    /// Returns `Ok(WriteResult::Written)` on success, `Ok(WriteResult::DeviceBusy)`
48    /// if the device couldn't accept the frame, or `Err` on connection failure.
49    fn write_frame(&mut self, frame: &LaserFrame) -> Result<WriteResult>;
50
51    /// Stop laser output.
52    fn stop(&mut self) -> Result<()>;
53
54    /// Set shutter state (open = laser enabled, closed = laser disabled).
55    fn set_shutter(&mut self, open: bool) -> Result<()>;
56}
57
58// =============================================================================
59// Conditional Backend Implementations
60// =============================================================================
61
62#[cfg(feature = "helios")]
63mod helios_backend {
64    use super::*;
65    use crate::protocols::helios::{
66        DeviceStatus, Frame, HeliosDac, HeliosDacController, Point as HeliosPoint,
67    };
68
69    /// Helios DAC backend (USB).
70    pub struct HeliosBackend {
71        dac: Option<HeliosDac>,
72        device_index: usize,
73    }
74
75    impl super::private::Sealed for HeliosBackend {}
76
77    impl HeliosBackend {
78        /// Create a new Helios backend for the given device index.
79        pub fn new(device_index: usize) -> Self {
80            Self {
81                dac: None,
82                device_index,
83            }
84        }
85
86        /// Create a backend from an already-discovered DAC.
87        pub fn from_dac(dac: HeliosDac) -> Self {
88            Self {
89                dac: Some(dac),
90                device_index: 0,
91            }
92        }
93
94        /// Discover all Helios DACs on the system.
95        pub fn discover() -> Result<Vec<HeliosDac>> {
96            let controller = HeliosDacController::new()
97                .map_err(|e| Error::context("Failed to create controller", e))?;
98            controller
99                .list_devices()
100                .map_err(|e| Error::context("Failed to list devices", e))
101        }
102    }
103
104    impl DacBackend for HeliosBackend {
105        fn dac_type(&self) -> DacType {
106            DacType::Helios
107        }
108
109        fn connect(&mut self) -> Result<()> {
110            if self.dac.is_some() {
111                // Already have a DAC, try to open it if idle
112                if let Some(dac) = self.dac.take() {
113                    let dac = dac
114                        .open()
115                        .map_err(|e| Error::context("Failed to open device", e))?;
116                    self.dac = Some(dac);
117                }
118                return Ok(());
119            }
120
121            // Discover and open the device at the specified index
122            let controller = HeliosDacController::new()
123                .map_err(|e| Error::context("Failed to create controller", e))?;
124            let mut dacs = controller
125                .list_devices()
126                .map_err(|e| Error::context("Failed to list devices", e))?;
127
128            if self.device_index >= dacs.len() {
129                return Err(Error::msg(format!(
130                    "Device index {} out of range (found {} devices)",
131                    self.device_index,
132                    dacs.len()
133                )));
134            }
135
136            let dac = dacs.remove(self.device_index);
137            let dac = dac
138                .open()
139                .map_err(|e| Error::context("Failed to open device", e))?;
140            self.dac = Some(dac);
141            Ok(())
142        }
143
144        fn disconnect(&mut self) -> Result<()> {
145            // HeliosDac doesn't have an explicit close; it closes when dropped
146            self.dac = None;
147            Ok(())
148        }
149
150        fn is_connected(&self) -> bool {
151            matches!(self.dac, Some(HeliosDac::Open { .. }))
152        }
153
154        fn write_frame(&mut self, frame: &LaserFrame) -> Result<WriteResult> {
155            let dac = self
156                .dac
157                .as_mut()
158                .ok_or_else(|| Error::msg("Not connected"))?;
159
160            // Check device status
161            match dac.status() {
162                Ok(DeviceStatus::Ready) => {}
163                Ok(DeviceStatus::NotReady) => return Ok(WriteResult::DeviceBusy),
164                Err(e) => return Err(Error::context("Failed to get status", e)),
165            }
166
167            // Convert LaserFrame to Helios Frame
168            let helios_points: Vec<HeliosPoint> = frame.points.iter().map(|p| p.into()).collect();
169
170            let helios_frame = Frame::new(frame.pps, helios_points);
171
172            dac.write_frame(helios_frame)
173                .map_err(|e| Error::context("Failed to write frame", e))?;
174
175            Ok(WriteResult::Written)
176        }
177
178        fn stop(&mut self) -> Result<()> {
179            if let Some(ref dac) = self.dac {
180                dac.stop()
181                    .map_err(|e| Error::context("Failed to stop", e))?;
182            }
183            Ok(())
184        }
185
186        fn set_shutter(&mut self, _open: bool) -> Result<()> {
187            // The helios-dac crate doesn't expose a shutter control method
188            // Shutter state is implicitly controlled by output state
189            Ok(())
190        }
191    }
192}
193
194#[cfg(feature = "helios")]
195pub use helios_backend::HeliosBackend;
196
197#[cfg(feature = "ether-dream")]
198mod ether_dream_backend {
199    use super::*;
200    use crate::protocols::ether_dream::dac::{stream, LightEngine, Playback, PlaybackFlags};
201    use crate::protocols::ether_dream::protocol::{DacBroadcast, DacPoint};
202    use std::net::IpAddr;
203    use std::time::Duration;
204
205    /// Ether Dream DAC backend (network).
206    pub struct EtherDreamBackend {
207        broadcast: DacBroadcast,
208        ip_addr: IpAddr,
209        stream: Option<stream::Stream>,
210    }
211
212    impl super::private::Sealed for EtherDreamBackend {}
213
214    impl EtherDreamBackend {
215        pub fn new(broadcast: DacBroadcast, ip_addr: IpAddr) -> Self {
216            Self {
217                broadcast,
218                ip_addr,
219                stream: None,
220            }
221        }
222    }
223
224    impl DacBackend for EtherDreamBackend {
225        fn dac_type(&self) -> DacType {
226            DacType::EtherDream
227        }
228
229        fn connect(&mut self) -> Result<()> {
230            let stream =
231                stream::connect_timeout(&self.broadcast, self.ip_addr, Duration::from_secs(5))
232                    .map_err(|e| Error::context("Failed to connect", e))?;
233
234            self.stream = Some(stream);
235            Ok(())
236        }
237
238        fn disconnect(&mut self) -> Result<()> {
239            if let Some(ref mut stream) = self.stream {
240                let _ = stream.queue_commands().stop().submit();
241            }
242            self.stream = None;
243            Ok(())
244        }
245
246        fn is_connected(&self) -> bool {
247            self.stream.is_some()
248        }
249
250        fn write_frame(&mut self, frame: &LaserFrame) -> Result<WriteResult> {
251            let stream = self
252                .stream
253                .as_mut()
254                .ok_or_else(|| Error::msg("Not connected"))?;
255
256            let points: Vec<DacPoint> = frame.points.iter().map(|p| p.into()).collect();
257            if points.is_empty() {
258                return Ok(WriteResult::DeviceBusy);
259            }
260
261            // Check light engine state first - must handle emergency stop before any other operations
262            let light_engine = stream.dac().status.light_engine;
263
264            match light_engine {
265                LightEngine::EmergencyStop => {
266                    stream
267                        .queue_commands()
268                        .clear_emergency_stop()
269                        .submit()
270                        .map_err(|e| Error::context("Failed to clear emergency stop", e))?;
271
272                    // Ping to refresh state
273                    stream
274                        .queue_commands()
275                        .ping()
276                        .submit()
277                        .map_err(|e| Error::context("Failed to ping after clearing e-stop", e))?;
278
279                    // Check if e-stop cleared
280                    if stream.dac().status.light_engine == LightEngine::EmergencyStop {
281                        return Err(Error::msg(
282                            "DAC stuck in emergency stop - check hardware interlock",
283                        ));
284                    }
285                    // Fall through - DAC should now be in Ready state, playback will be Idle
286                }
287                LightEngine::Warmup | LightEngine::Cooldown => {
288                    return Ok(WriteResult::DeviceBusy);
289                }
290                LightEngine::Ready => {
291                    // Normal operation - continue
292                }
293            }
294
295            // Check buffer space
296            let buffer_capacity = stream.dac().buffer_capacity;
297            let buffer_fullness = stream.dac().status.buffer_fullness;
298            let available = buffer_capacity as usize - buffer_fullness as usize - 1;
299
300            if available == 0 {
301                return Ok(WriteResult::DeviceBusy);
302            }
303
304            let point_rate = if frame.pps > 0 {
305                frame.pps
306            } else {
307                stream.dac().max_point_rate / 16
308            };
309
310            // Minimum points that must be buffered before sending begin command.
311            const MIN_POINTS_BEFORE_BEGIN: u16 = 500;
312            let target_buffer_points =
313                (point_rate / 20).max(MIN_POINTS_BEFORE_BEGIN as u32) as usize;
314            let target_len = target_buffer_points
315                .min(available)
316                .max(points.len().min(available));
317
318            let mut points_to_send = points;
319            if points_to_send.len() > available {
320                points_to_send.truncate(available);
321            } else if points_to_send.len() < target_len {
322                let seed = points_to_send.clone();
323                let mut idx = 0;
324                while points_to_send.len() < target_len {
325                    points_to_send.push(seed[idx % seed.len()]);
326                    idx += 1;
327                }
328            }
329
330            let playback_flags = stream.dac().status.playback_flags;
331            let playback = stream.dac().status.playback;
332            let current_point_rate = stream.dac().status.point_rate;
333
334            let mut force_begin = false;
335            if playback_flags.contains(PlaybackFlags::UNDERFLOWED) {
336                stream
337                    .queue_commands()
338                    .prepare_stream()
339                    .submit()
340                    .map_err(|e| Error::context("Failed to recover stream", e))?;
341                force_begin = true;
342            }
343
344            let result =
345                if force_begin {
346                    // After underflow recovery, send data first, then begin separately
347                    stream
348                        .queue_commands()
349                        .data(points_to_send.clone())
350                        .submit()
351                        .map_err(|e| Error::context("Failed to send data", e))?;
352
353                    // Check buffer fullness after sending data
354                    let buffer_fullness = stream.dac().status.buffer_fullness;
355
356                    if buffer_fullness >= MIN_POINTS_BEFORE_BEGIN {
357                        stream.queue_commands().begin(0, point_rate).submit()
358                    } else {
359                        Ok(())
360                    }
361                } else {
362                    match playback {
363                        Playback::Idle => {
364                            stream
365                                .queue_commands()
366                                .prepare_stream()
367                                .submit()
368                                .map_err(|e| Error::context("Failed to prepare stream", e))?;
369
370                            // Send data first
371                            stream
372                                .queue_commands()
373                                .data(points_to_send.clone())
374                                .submit()
375                                .map_err(|e| Error::context("Failed to send data", e))?;
376
377                            // Check buffer fullness after sending data, only begin if enough points buffered
378                            let buffer_fullness = stream.dac().status.buffer_fullness;
379
380                            if buffer_fullness >= MIN_POINTS_BEFORE_BEGIN {
381                                stream.queue_commands().begin(0, point_rate).submit()
382                            } else {
383                                Ok(())
384                            }
385                        }
386                        Playback::Prepared => {
387                            // Send data first
388                            stream
389                                .queue_commands()
390                                .data(points_to_send.clone())
391                                .submit()
392                                .map_err(|e| Error::context("Failed to send data", e))?;
393
394                            // Check buffer fullness after sending data, only begin if enough points buffered
395                            let buffer_fullness = stream.dac().status.buffer_fullness;
396
397                            if buffer_fullness >= MIN_POINTS_BEFORE_BEGIN {
398                                stream.queue_commands().begin(0, point_rate).submit()
399                            } else {
400                                Ok(())
401                            }
402                        }
403                        Playback::Playing => {
404                            let send_result = if current_point_rate != point_rate {
405                                stream
406                                    .queue_commands()
407                                    .update(0, point_rate)
408                                    .data(points_to_send.clone())
409                                    .submit()
410                            } else {
411                                stream
412                                    .queue_commands()
413                                    .data(points_to_send.clone())
414                                    .submit()
415                            };
416
417                            // Handle underflow: if DAC went Idle while we thought it was Playing,
418                            // we get NAK_INVALID. Recover by re-preparing the stream.
419                            if send_result.is_err() {
420                                let current_playback = stream.dac().status.playback;
421
422                                if current_playback == Playback::Idle {
423                                    // DAC underflowed and went back to Idle - need full restart
424                                    stream.queue_commands().prepare_stream().submit().map_err(
425                                        |e| Error::context("Failed to recover from underflow", e),
426                                    )?;
427
428                                    stream
429                                        .queue_commands()
430                                        .data(points_to_send.clone())
431                                        .submit()
432                                        .map_err(|e| {
433                                            Error::context("Failed to send data after recovery", e)
434                                        })?;
435
436                                    let buffer_fullness = stream.dac().status.buffer_fullness;
437                                    if buffer_fullness >= MIN_POINTS_BEFORE_BEGIN {
438                                        stream.queue_commands().begin(0, point_rate).submit()
439                                    } else {
440                                        Ok(())
441                                    }
442                                } else {
443                                    // Some other error - propagate it
444                                    send_result
445                                }
446                            } else {
447                                send_result
448                            }
449                        }
450                    }
451                };
452
453            result.map_err(|e| Error::context("Failed to write frame", e))?;
454            Ok(WriteResult::Written)
455        }
456
457        fn stop(&mut self) -> Result<()> {
458            if let Some(ref mut stream) = self.stream {
459                stream
460                    .queue_commands()
461                    .stop()
462                    .submit()
463                    .map_err(|e| Error::context("Failed to stop", e))?;
464            }
465            Ok(())
466        }
467
468        fn set_shutter(&mut self, _open: bool) -> Result<()> {
469            // Ether Dream doesn't have explicit shutter control
470            Ok(())
471        }
472    }
473}
474
475#[cfg(feature = "ether-dream")]
476pub use ether_dream_backend::EtherDreamBackend;
477
478#[cfg(feature = "idn")]
479mod idn_backend {
480    use super::*;
481    use crate::protocols::idn::dac::{stream, ServerInfo, ServiceInfo};
482    use crate::protocols::idn::protocol::PointXyrgbi;
483
484    /// IDN DAC backend (ILDA Digital Network).
485    pub struct IdnBackend {
486        server: ServerInfo,
487        service: ServiceInfo,
488        stream: Option<stream::Stream>,
489    }
490
491    impl super::private::Sealed for IdnBackend {}
492
493    impl IdnBackend {
494        pub fn new(server: ServerInfo, service: ServiceInfo) -> Self {
495            Self {
496                server,
497                service,
498                stream: None,
499            }
500        }
501    }
502
503    impl DacBackend for IdnBackend {
504        fn dac_type(&self) -> DacType {
505            DacType::Idn
506        }
507
508        fn connect(&mut self) -> Result<()> {
509            let stream = stream::connect(&self.server, self.service.service_id)
510                .map_err(|e| Error::context("Failed to connect", e))?;
511
512            self.stream = Some(stream);
513            Ok(())
514        }
515
516        fn disconnect(&mut self) -> Result<()> {
517            if let Some(ref mut stream) = self.stream {
518                let _ = stream.close();
519            }
520            self.stream = None;
521            Ok(())
522        }
523
524        fn is_connected(&self) -> bool {
525            self.stream.is_some()
526        }
527
528        fn write_frame(&mut self, frame: &LaserFrame) -> Result<WriteResult> {
529            let stream = self
530                .stream
531                .as_mut()
532                .ok_or_else(|| Error::msg("Not connected"))?;
533
534            stream.set_scan_speed(frame.pps);
535            let points: Vec<PointXyrgbi> = frame.points.iter().map(|p| p.into()).collect();
536
537            stream
538                .write_frame(&points)
539                .map_err(|e| Error::context("Failed to write frame", e))?;
540
541            Ok(WriteResult::Written)
542        }
543
544        fn stop(&mut self) -> Result<()> {
545            if let Some(ref mut stream) = self.stream {
546                let blank_point = PointXyrgbi::new(0, 0, 0, 0, 0, 0);
547                let blank_frame = vec![blank_point; 10];
548                let _ = stream.write_frame(&blank_frame);
549            }
550            Ok(())
551        }
552
553        fn set_shutter(&mut self, _open: bool) -> Result<()> {
554            // IDN doesn't have explicit shutter control
555            Ok(())
556        }
557    }
558}
559
560#[cfg(feature = "idn")]
561pub use idn_backend::IdnBackend;
562
563#[cfg(feature = "lasercube-wifi")]
564mod lasercube_wifi_backend {
565    use super::*;
566    use crate::protocols::lasercube_wifi::dac::{stream, Addressed};
567    use crate::protocols::lasercube_wifi::protocol::{DeviceInfo, Point as LasercubePoint};
568    use std::net::SocketAddr;
569
570    /// LaserCube WiFi DAC backend.
571    pub struct LasercubeWifiBackend {
572        addressed: Addressed,
573        stream: Option<stream::Stream>,
574    }
575
576    impl super::private::Sealed for LasercubeWifiBackend {}
577
578    impl LasercubeWifiBackend {
579        pub fn new(addressed: Addressed) -> Self {
580            Self {
581                addressed,
582                stream: None,
583            }
584        }
585
586        pub fn from_discovery(info: &DeviceInfo, source_addr: SocketAddr) -> Self {
587            Self::new(Addressed::from_discovery(info, source_addr))
588        }
589    }
590
591    impl DacBackend for LasercubeWifiBackend {
592        fn dac_type(&self) -> DacType {
593            DacType::LasercubeWifi
594        }
595
596        fn connect(&mut self) -> Result<()> {
597            let stream = stream::connect(&self.addressed)
598                .map_err(|e| Error::context("Failed to connect", e))?;
599
600            self.stream = Some(stream);
601            Ok(())
602        }
603
604        fn disconnect(&mut self) -> Result<()> {
605            if let Some(ref mut stream) = self.stream {
606                let _ = stream.stop();
607            }
608            self.stream = None;
609            Ok(())
610        }
611
612        fn is_connected(&self) -> bool {
613            self.stream.is_some()
614        }
615
616        fn write_frame(&mut self, frame: &LaserFrame) -> Result<WriteResult> {
617            let stream = self
618                .stream
619                .as_mut()
620                .ok_or_else(|| Error::msg("Not connected"))?;
621
622            let lc_points: Vec<LasercubePoint> = frame.points.iter().map(|p| p.into()).collect();
623
624            stream
625                .write_frame(&lc_points, frame.pps)
626                .map_err(|e| Error::context("Failed to write frame", e))?;
627
628            Ok(WriteResult::Written)
629        }
630
631        fn stop(&mut self) -> Result<()> {
632            if let Some(ref mut stream) = self.stream {
633                stream
634                    .stop()
635                    .map_err(|e| Error::context("Failed to stop", e))?;
636            }
637            Ok(())
638        }
639
640        fn set_shutter(&mut self, open: bool) -> Result<()> {
641            if let Some(ref mut stream) = self.stream {
642                stream
643                    .set_output(open)
644                    .map_err(|e| Error::context("Failed to set shutter", e))?;
645            }
646            Ok(())
647        }
648    }
649}
650
651#[cfg(feature = "lasercube-wifi")]
652pub use lasercube_wifi_backend::LasercubeWifiBackend;
653
654#[cfg(feature = "lasercube-usb")]
655mod lasercube_usb_backend {
656    use super::*;
657    use crate::protocols::lasercube_usb::dac::Stream;
658    use crate::protocols::lasercube_usb::protocol::Sample as LasercubeUsbSample;
659    use crate::protocols::lasercube_usb::{discover_dacs, rusb};
660
661    /// LaserCube USB DAC backend (LaserDock).
662    pub struct LasercubeUsbBackend {
663        device: Option<rusb::Device<rusb::Context>>,
664        stream: Option<Stream<rusb::Context>>,
665    }
666
667    impl super::private::Sealed for LasercubeUsbBackend {}
668
669    impl LasercubeUsbBackend {
670        pub fn new(device: rusb::Device<rusb::Context>) -> Self {
671            Self {
672                device: Some(device),
673                stream: None,
674            }
675        }
676
677        pub fn from_stream(stream: Stream<rusb::Context>) -> Self {
678            Self {
679                device: None,
680                stream: Some(stream),
681            }
682        }
683
684        pub fn discover_devices() -> Result<Vec<rusb::Device<rusb::Context>>> {
685            discover_dacs().map_err(|e| Error::context("Failed to discover devices", e))
686        }
687    }
688
689    impl DacBackend for LasercubeUsbBackend {
690        fn dac_type(&self) -> DacType {
691            DacType::LasercubeUsb
692        }
693
694        fn connect(&mut self) -> Result<()> {
695            if self.stream.is_some() {
696                return Ok(());
697            }
698
699            let device = self
700                .device
701                .take()
702                .ok_or_else(|| Error::msg("No device available"))?;
703
704            let mut stream =
705                Stream::open(device).map_err(|e| Error::context("Failed to open device", e))?;
706
707            stream
708                .enable_output()
709                .map_err(|e| Error::context("Failed to enable output", e))?;
710
711            self.stream = Some(stream);
712            Ok(())
713        }
714
715        fn disconnect(&mut self) -> Result<()> {
716            if let Some(ref mut stream) = self.stream {
717                let _ = stream.stop();
718            }
719            self.stream = None;
720            Ok(())
721        }
722
723        fn is_connected(&self) -> bool {
724            self.stream.is_some()
725        }
726
727        fn write_frame(&mut self, frame: &LaserFrame) -> Result<WriteResult> {
728            let stream = self
729                .stream
730                .as_mut()
731                .ok_or_else(|| Error::msg("Not connected"))?;
732
733            let samples: Vec<LasercubeUsbSample> = frame.points.iter().map(|p| p.into()).collect();
734
735            stream
736                .write_frame(&samples, frame.pps)
737                .map_err(|e| Error::context("Failed to write frame", e))?;
738
739            Ok(WriteResult::Written)
740        }
741
742        fn stop(&mut self) -> Result<()> {
743            if let Some(ref mut stream) = self.stream {
744                stream
745                    .stop()
746                    .map_err(|e| Error::context("Failed to stop", e))?;
747            }
748            Ok(())
749        }
750
751        fn set_shutter(&mut self, open: bool) -> Result<()> {
752            if let Some(ref mut stream) = self.stream {
753                if open {
754                    stream
755                        .enable_output()
756                        .map_err(|e| Error::context("Failed to enable output", e))?;
757                } else {
758                    stream
759                        .disable_output()
760                        .map_err(|e| Error::context("Failed to disable output", e))?;
761                }
762            }
763            Ok(())
764        }
765    }
766}
767
768#[cfg(feature = "lasercube-usb")]
769pub use lasercube_usb_backend::LasercubeUsbBackend;