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